缩减 iOS 包体积(Unity)

本文提供查看 iOS 包体积的方法,分两部分进行研究,具体缩减包体积的方法会在以后的文章介绍:

检查包体积的大小

真正在 AppStore 中显示的下载体积可以在 iTunes Connect -> 我的 App -> 活动 -> 构建版本 中查看 , 点击 App Store 文件大小 即可看到各个机型的文件大小。

appstore-size
appstore-size-2

如何查看具体的体积信息

在编译完成后的 Products 文件夹中右键可以看到生成的应用程序文件,右键点击显示包文件内容,显示的即为所有的包文件内容,但是可以发现其中还有一个 Unix executable 的文件,名字与 product 的名字一样,我这里这个文件名为 moonvrplayer,我们需要对这个文件再做具体的分析,这时候可以用 一个 LinkMap Parser的工具帮助分析该文件的组成

xcode-preview

  1. XCode 开启编译选项 Write Link Map File
    XCode -> Project -> Build Settings -> 搜map -> 把Write Link Map File选项设为yes,并指定好 linkMap 的存储位置
  2. 编译后,到编译目录里找到该 txt 文件,文件名和路径就是上述的Path to Link Map File
    位于~/Library/Developer/Xcode/DerivedData/XXX-eumsvrzbvgfofvbfsoqokmjprvuh/Build/Intermediates/XXX.build/Debug-iphoneos/XXX.build/, 这个LinkMap里展示了整个可执行文件的全貌,列出了编译后的每一个.o目标文件的信息(包括静态链接库.a里的),以及每一个目标文件的代码段,数据段存储详情。

分析此文件的工具我们可以使用 LinkMap Parser

命令如下:

1
node size.js /Users/rockvr/Desktop/moonvrplayer-LinkMap-normal-arm64.txt  -hl > ~/Desktop/size.txt

size.js 为 gist的这个文件,moonvrplayer-LinkMap-normal-arm64.txt 为 linkmap 文件,~/Desktop/size.txt 为输出的文件

可以看出所使用的库的体积,针对比较大的文件进行缩减
size-txt

Unity Assets 大小查看

针对 Unity 资源的大小,我们可以参考官方文档的说明
打开 Editor Log ,查看所有 asset 的大小并进行针对性的缩减

unity-asset-size


OpenGL ES 3.0 绘制三角形

本文实现 在 iOS 平台上 使用 OpenGL ES 绘制一个简单的三角形 且 不使用 GLKit,原因是方便跨平台共享代码,也易于理解更底层的知识.

源代码参见 Github OpenGLES3

使用的各组件信息如下:

OpenGL ES : 3.0
iOS : 10.2
Device : iPhone 6s 模拟器
xCode : 8.3
除了EGL的版本信息之外,其他的即使使用其他版本应该无太大关系,需要注意 从 iOS 7.0 之后才支持 3.0 版本。

重点代码分析

  • 创建EAGL的 layer ,EAGL 可以理解为 Apple 的 EGL(Embedded GL) 实现
1
2
3
+(Class)layerClass {
return [CAEAGLLayer class];
}
  • 创建 EAGL 的 context
1
context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
  • 初始化 layer
1
2
3
CAEAGLLayer *layer = (CAEAGLLayer *)self.layer;
layer.opaque = YES;
layer.contentsScale = [UIScreen mainScreen].scale;
  • 接下来是 初始化 renderBuffer 和 frameBuffer,关于 frameBuffer 的信息参考 苹果官方文档

  • 下一步绑定(attach) vertextShader 与 fragmentShader 的信息,入门的时候可以先不用理解shader代码的含义

  • 最终交换前后端帧缓冲区

1
[context presentRenderbuffer:GL_RENDERBUFFER];

参考资料:

  1. OpenGL ES 3.0 数据可视化

  2. Apple OpenGL ES Programming Guide

  3. OpenGL ES 3.0 Programming Guide (2nd Edition)


React Native 集成 unity3D 场景 - iOS

需求

最近开始啦一个新的项目,需求是:通过 Unity 3D 制作场景等,同时 iOS 上也会有自己的 2D 界面,两者之间可以相互跳转。
通过两天的学习和尝试,最终成功的在 React Native 项目(以下简称 RN 项目)中集成了 Unity3D 的导出项目(称作 Native 或者 Unity 项目),现记录一下踩坑过程。

思路

一开始便有两种思路:

  1. 拷贝 Unity 项目的文件到 RN 项目中,修改 RN 项目的配置。(以 RN 项目为主)
  2. 在 Unity 项目的基础上,新建 RN 项目所需的文件。(以 Unity 项目为主)

实现

首先选择的是方案一,主要考虑到我想以 2D 界面为主,Unity 项目为辅,基于这个想法才想将各种文件都拷贝至已经生成的 RN 项目中,然后开始修改 Build Setting,添加各种依赖,但是由于我的 Unity3D 项目比较复杂,里边集成了 Google Cardboard SDK,还有很多第三方SDK,所以导致最终一直有几个方法找不到,参见我在 Unity 社区提出的问题,尝试了各种办法之后放弃了,如果你有解决办法,还请告知。

最终选择的是方案二,这里我描述一下集成过程:

  • 通过 Unity 导出 iOS 项目
  • 新建 项目文件夹,按如下架构组织,ios 文件夹中放的是 Unity导出的所有文件,package.json 为初始化文件,index.ios.js 为 RN 项目的文件。此部分可以参考 RN 的官方文档, 根据文档一直操作到 Create a index.ios.js file 部分,后面的可以暂时不用管。
    项目文件组织

在这个过程中 cocoaPod 有一个坑,那就是下载速度特别的慢,经过查阅各种资料,所谓的换源什么的方法对我都没有什么用,我是直接用 git clone https://github.com/CocoaPods/Specs.git >~/.cocoapods/repos/master 的方式看着一点点下载完然后更新再 pod install的,前后大约耗时 30 分钟,请耐心等待

  • pod install 执行成功后,切记打开.xcworkspace 文件,而不是原来的 .xcodeproj 文件,此时应该可以build成功

这里有另外一个坑:由于我之前自己有改过 Build Setting 的 Other C Flags 设置,所以pod install的时候,pod需要添加的项没有添加进来,而其实 pod 是有 warning 的,却被我忽略了,解决办法是在这个设置上添加 ${inherited}即可,之后执行各种命令,warning 也是不能忽略的。

集成成功后,下一步要解决的问题是跳转问题,由于项目刚刚开始做,我先做了一个简单的页面模拟跳转过程,尝试是否能跳转成功。这里边涉及的功能点是:

  1. RN 页面调用 Native 代码
  2. Native 页面跳转 Unity 页面,之所以一定要实现这样的功能,是考虑到项目以后UI部分全部都会用 RN 去写,而Unity项目生成的为 Native 的页面,所以找到其桥梁势是很有必要的,而不是只把RN页面作为Native的简单嵌套(尽管RN的实质是这样的)

1. RN 页面如何调用 Native 代码?

首先请参考官方文档参考
这里的重点是要实现 RCTBridgeModule ,将方法暴露给RN端。
ios/下新建 NavigateBridge.mNavigateBridge.h , 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// NavigateBridge.m 
#import "NavigateBridge.h"
#import "UnityAppController.h"

@implementation NavigateBridge

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(startUnity)
{
RCTLogInfo(@"I will start Unity!");
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter]postNotificationName:@"openUnity" object:nil];
});

}

@end
1
2
3
4
5
6
7
// NavigateBridge.h 
#import <React/RCTBridgeModule.h>
#import <React/RCTLog.h>

@interface NavigateBridge : NSObject<RCTBridgeModule>

@end

根据官方文档的说明,startUnity 为暴露给 RN 端的方法,所以在 index.ios.js 的代码如下:重点是 NavigateManager 的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import React, {Component} from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
Alert,
Button,
NativeModules
} from 'react-native';


var NavigateManager = NativeModules.NavigateBridge;

const onButtonPress = () => {
NavigateManager.startUnity();
};


export default class untitled extends Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!哈哈哈哈哈·12345
</Text>
<Text style={styles.instructions}>
To get started, edit index.ios.js 不错的
</Text>
<Button onPress={onButtonPress}
title="Press me!"
/>
<Text style={styles.instructions}>
Press Cmd+R to reload,{'\n'}
Cmd+D or shake for dev menu
</Text>
</View>
);
}
}


const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});

AppRegistry.registerComponent('moonplayer', () => untitled);

至此,可以实现 RN 页面调用 Native 的方法,不过此时运行起来还是直接进入了Unity页面,请继续往下看。

2. Native 如何调起 Unity 界面?

我的解决方案是不修改程序的启动入口,而直接干预 unity 的启动过程。
ios/Classes 文件夹中,main.mm 为整个App的入口,可以看到直接启动了 UnityAppController ,在该目录下找到 UnityAppController.mm, 首先我们要注意到这是一个 UIApplicationDelegate 的对象(参见头文件定义),所以其实它承担的就是平时我们熟悉的 AppDelegate的角色。
在头文件中可以看到很多重要的定义,比如 UIWindow,UIViewController 等,有些目前用不到,但是说不定以后需要用,所以需要注意。

我们注意到在mm文件中,applicationDidBecomeActive 方法里有调用 startUnity 方法,根据网上资料可以得知,startUnity 是打开 Unity界面的 函数,所以我们要干预的就是调用此方法的地方。
找到

1
2
_startUnityScheduled = true;
[self performSelector:@selector(startUnity) withObject:application afterDelay:0];

将 startUnity 替换为 startMainPagewithObject:application 也记得替换 该方法定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)startMainPage {
NSURL *jsCodeLocation;

jsCodeLocation = [NSURL URLWithString:@"http://10.1.20.47:8081/index.ios.bundle?platform=ios"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"moonplayer"
initialProperties:nil
launchOptions:nil];
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];

self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
}

http://10.1.20.47:8081 这个地址根据你使用的是模拟器还是真机而更改为 localhost 或者是你电脑的 ip地址,我用的是真机,所以用的是自己电脑的ip地址,同时我也修改了 RCTWebSocketExecutor.m,请参考官方文档
根据文档很好理解代码所做的事情,注意的是 modeleName 需要与 index.ios.js 中注册的 module name 一致才可以,至此,程序启动后是先跳转到了我们的 RN 首页。

上面代码的 NavigateBridge.m 已经写好了点击按钮后触发的事件,即发出了一个通知,我们在 UnityAppController.mm 中监听此通知:

didFinishLaunchingWithOptions 方法的最后添加:

1
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(openUnity:) name:@"openUnity" object:nil];

同时 openUnity 方法的定义如下:

1
2
3
4
-(void) openUnity:(NSNotification *)notification{
NSLog(@"receive notification");
[self startUnity:[UIApplication sharedApplication]];
}

至此即可实现 Native 界面跳转至 Unity3D 的场景,而且加载速度很快