目录:
Android打包原理
Android apk文件夹目录结构
Android打包脚本gradle执行流程流程
App安装流程
RN打包原理
RN bundle文件结构
metro打包流程
RN拆包原理
Android、RN打包比较
Android、RN打包比较
在分析一件事物原理的时候我喜欢从结果开始逆向推理过程,这样更容易理解
Android打包原理
Android apk文件夹目录结构
无图无真相,通过AS或者解压打包好的apk目录如上图
lib
lib目录下存放的是底层代码C/C++编译出来的so文件,如音视频、人脸识别等,其中x86、arm代表兼容不同的芯片类型。
由于java性能有限,很多对性能要求比较高的功能都需要通过C/C++实现,so同样增强了app的安全性,不容易被反编译。
so库由于无法被压缩是整个apk包体积中占比最大的,很多app只兼容armev7来减少包大小(google play开放了bundle功能,可根据用户手机选择下载对应型号的app),也有通过动态下发部分功能不是很重要的so库来达到优化包大小的目的。
res
Android的资源文件目录,包含布局、动画
assets
这里保存的是打包过程不能被压缩需要保留的原始文件,比如字体文件,RN的本地bundle文件也经常放这里。
dex文件
.dex文件是Android系统运行在Dalvik Virtual Machine上的可执行文件,也是Android爱普的核心。项目的Java源码通过javac生成class文件,在通过dx工具生成为classes.dex文件。
AndroidManifest.xml
这个文件可以意为清单文件或者全局配置文件。里面有很多应用的配置信息,权限、版本号、四大组件的注册也在其中。
META-INF文件夹
该目录主要作用就是用于保证APK的完整性和安全性。主要有三个文件:
MANIFEST.MF:保存了整个apk文件中所有文件的文件名+SHA-1后的base64编码值。象征着apk的完整性。
CERT.RSA:保存了公匙和加密方式的信息。
CERT.SF:这个文件与MANIFEST.MF的结构一样,只是其编码会被私匙加密。每次安装时,通过该文件夹中的文件,就可以完成验证的过程。如果apk包被改变了,而篡改者没有私匙生成的CERT.SF,则无法完成校验。
resource.arsc文件
该文件是所有文件中结构最复杂的。
它记录了资源文件,资源文件位置和资源id的映射关系。并且将所有的string都存放在了string pool中,节省了在查找资源时,字符串处理的开销。
Android打包脚本gradle执行流程流程
先借用一张官方Android打包图镇楼
如上图所示:
1.打包资源文件,生成R.java文件
R.java是资源文件引用的一个映射,没一个资源都会在编译的过程中生成一个唯一id
打包资源文件的工具是aapt(The Android Asset Packing Tool),位于android-sdk/platform-tools目录下。 在这个过程中,项目中的AndroidManifest.xml文件和布局文件xml都会编译生成相应的R.java。
同时还有编译生成resources.arsc和uncompiled res文件(二进制文件 & 非二进制文件)
非二进制文件(eg:res/raw、res/pic)保持原样。
assets资源文件内容保持原样。
2.处理AIDL文件,生成相应的java文件
这个过程使用的工具是aidl(Android Interface Definition Language),位于android-sdk/platform-tools目录下。
aidl工具解析接口定义文件,然后生成相应的java接口,供程序调用。
如果项目中没有使用到aidl文件,那么这个过程可以跳过。
3.编译项目源代码,生成.class文件
项目中所有的java文件,包括R.java文件和**.aidl文件,都会被java编译器(Java Compiler)编译成.class文件。
生成的class文件位于工程中的bin/classes目录下。
4.转换所有的class文件,生成classes.dex文件
这个过程使用的工具是dx,该工具位于android-sdk/platform-tools。
该工具可以生成供Android系统虚拟机的执行文件 classes.dex。
dx工具主要工作就是将java字节码转换成Dalvik字节码、压缩常量池以及消除冗余信息等。
任何第三方的lib和.class文件都会被转换成.dex文件
5.打包生成Apk文件
所有没有编译过的资源(eg: images)、编译过的资源和.dex文件都会被 apkbuilder 工具打包到最终的.apk文件中去。
打包工具apkbuilder位于android-sdk/tools目录下。
apkbuilder实际上是一个脚本文件,调用的是android-sdk/tools/lib/sdklib.jar文件中的 com.android.sdklib.build.ApkbuilderMainl类。
6.对Apk文件签名
apk文件只有被签名才能被安装在设备上。
签名文件(keystore)有2种
一种是用于调试的 debug.keystore,开发工具中Run以后在设备上运行的Apk就是debug.keystore签名,在Android sdk中可以找到,是固定的
一种是用于发布正式版本的keystore,属于开发自行创建申请的证书,起到防止app被冒名顶替的作用
7.对签名后的文件进行对齐处理
在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,位于android-sdk/tools目录下。
对齐的主要过程是:
> 将Apk包中的所有资源文件距离文件起始位置偏移4字节整数倍。
> 对齐之后可以减少运行时内存的使用。
App安装流程
- 复制APK到/data/app目录下,解压并扫描安装包。
- 资源管理器解析APK里的资源文件。
- 解析AndroidManifest文件,并在/data/data/目录下创建对应的应用数据目录。
- 然后对dex文件进行优化,并保存在dalvik-cache目录下。
- 将AndroidManifest文件解析出的四大组件信息注册到PackageManagerService中。
- 安装完成后,发送广播。
RN打包原理
RN bundle文件结构
如上图,生成的bundle大致分为四层
var层
包含了当前进程,当前运行环境,bundle启动时间等
polyfill层
文件中!(function(r)开头的部分代表的是polyfill层,定义了对 define(__d)
、 require(__r)
、clear(__c)
的支持,以及 module(react-native 及第三方 dependences 依赖的 module) 的加载逻辑;
模块定义层:
__d 定义的代码块,包括 RN 框架源码 js 部分、自定义 js 代码部分、图片资源信息,供 require 引入使用
require层
r 定义的代码块,找到 d 定义的代码块 并执行
metro打包流程
metro 打包的整个流程大致分为:
1.命令参数解析
react-native bundle --dev false --platform android --entry-file index.js --config bundle.main.js --bundle-output ./CodePush/index.android.bundle --assets-dest ./CodePush --sourcemap-output ./CodePush/index.android.bundle.map "
platform:对应的平台,android/ios --entry-file: 入口文件 --config:额外配置,拆包有用到 --bundle-output:生成的bundle文件输出位置 --assets-dest:图片等资源文件输出位置 --sourcemap-output:sourcemap映射文件输出位置
2.metro 打包服务启动
- 合并 metro 默认配置和自定义配置,并设置 maxWorkers,resetCache,--config就属于用户的额外配置
- 根据解析得到参数,构建 requestOptions,传递给打包函数
- 实例化 metro Server
- 启动 metro 构建 bundle
- 处理资源文件,解析
- 关闭 Metro Server
3.解析和转化
Metro Server 使用IncrementalBundler
进行 js 代码的解析和转换**
在 Metro 使用IncrementalBundler
进行解析转换的主要作用是:
- 返回了以入口文件为入口的所有相关依赖文件的依赖图谱和 babel 转换后的代码;
- 返回了var 定义部分及 polyfill 部分所有相关依赖文件的依赖图谱和 babel 转换后的代码;
生成的依赖关系图谱如下
[
{
dependencies: Map(404) { // 入口文件下每个文件所依赖其他文件的关系图谱
'/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/index.js' => {
{
inverseDependencies: Set(1) {
'/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/index.js'
},
path: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/App.js',
dependencies: Map(8) {
'@babel/runtime/helpers/createClass' => {
absolutePath: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/node_modules/@babel/runtime/helpers/createClass.js',
data: {
name: '@babel/runtime/helpers/createClass',
data: { isAsync: false }
}
},
// ....
'react' => {
absolutePath: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/node_modules/react/index.js',
data: { name: 'react', data: { isAsync: false } }
},
'react-native' => {
absolutePath: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/node_modules/react-native/index.js',
data: { name: 'react-native', data: { isAsync: false } }
}
},
getSource: [Function: getSource],
output: [
{
data: {// 对应文件转换后的代码
code: `__d(function(g,r,i,a,m,e,d){var t=r(d[0]);Object.defineProperty(e,"__esModule",{value:!0}),e.default=void 0;var n=t(r(d[1])),u=t(r(d[2])),l=t(r(d[3])),c=t(r(d[4])),f=t(r(d[5])),o=t(r(d[6])),s=r(d[7]);function y(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(t){return!1}}var p=(function(t){(0,l.default)(R,t);var p,h,x=(p=R,h=y(),function(){var t,n=(0,f.default)(p);if(h){var u=(0,f.default)(this).constructor;t=Reflect.construct(n,arguments,u)}else t=n.apply(this,arguments);return(0,c.default)(this,t)});function R(){return(0,n.default)(this,R),x.apply(this,arguments)}return(0,u.default)(R,[{key:"render",value:function(){return o.default.createElement(o.default.Fragment,null,o.default.createElement(s.View,{style:v.body},o.default.createElement(s.Text,{style:v.text},"\u4f60\u597d\uff0c\u4e16\u754c")))}}]),R})(o.default.Component);e.default=p;var v=s.StyleSheet.create({body:{backgroundColor:'white',flex:1,justifyContent:'center',alignItems:'center'},text:{textAlign:'center',color:'red'}})});`,
lineCount: 1,
map: [
[ 1, 177, 9, 0, '_react' ],
[ 1, 179, 9, 0, '_interopRequireDefault' ],
[ 1, 181, 9, 0, 'r' ],
[ 1, 183, 9, 0, 'd' ],
[ 1, 185, 9, 0 ],
[ 1, 190, 10, 0, '_reactNative' ],
// .....
],
functionMap: {
names: [ '<global>', 'App', 'render' ],
mappings: 'AAA;eCW;ECC;GDQ;CDC'
}
},
type: 'js/module'
}
]
}
},
'/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/App.js' => {
inverseDependencies: [Set],
path: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/App.js',
dependencies: [Map],
getSource: [Function: getSource],
output: [Array]
},
'/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/app.json' => {
inverseDependencies: [Set],
path: '/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/app.json',
dependencies: Map(0) {},
getSource: [Function: getSource],
output: [Array]
}
},
entryPoints: [ //入口文件
'/Users/alexganggao/Desktop/react-native-ssr/ReactNativeSSR/index.js'
],
importBundleNames: Set(0) {}
}
]
4.生成
metro 代码生成部分使用 baseJSBundle
得到代码,并使用 baseToString
拼接最终 Bundle
代码
在 baseJSBundle
中:
baseJSBundle
整体调用了三次processModules
分别用于解析出:preCode
,postCode
和modules
其对应的分别是var 和 polyfills 部分的代码 , require 部分的代码 ,_d
部分的代码processModules
经过两次filter
过滤出所有类型为js/
类型的数据,第二次过滤使用用户自定义filter
函数;过滤完成之后使用wrapModule
转换成_d(factory,moduleId,dependencies)
的代码,类似于设计模式中的责任链模式
在baseToString
中:
- 先将 var 及 polyfill 部分的代码使用\n 进行字符串拼接;
- 然后将
_d
部分的代码使用moduleId
进行升序排列并使用字符串拼接的方式构造_d
部分的代码; - 最后合如
_r
部分的代码
5.停止打包服务
停止打包服务
总结如下几点:
- 整个 metro 进行依赖分析和 babel 转换主要通过了JestHasteMap (opens new window)去做依赖分析;
- 在做依赖分析的通过,metro 会监听当前目录的文件变化,然后以最小变化生成最终依赖关系图谱;
- 不管是入口文件解析还是 polyfill 文件的依赖解析都是使用了JestHasteMap (opens new window);
RN拆包原理
在打包生成过程中processModules
经过两次 filter
过滤出所有类型为 js/
类型的数据,第二次过滤使用用户自定义 filter
函数;过滤完成之后使用 wrapModule
转换成_d(factory,moduleId,dependencies)
的代码,类似于设计模式中的责任链模式
function createModuleIdFactory() {
const fileToIdMap = new Map();
let nextId = 0;
return path => {
let id = fileToIdMap.get(path);
if (typeof id !== "number") {
id = nextId++;
fileToIdMap.set(path, id);
}
return id;
};
}
module.exports = createModuleIdFactory;
逻辑比较简单,如果查到 map 里没有记录这个模块则 id 自增,然后将该模块记录到 map 中,所以从这里可以看出,官方代码生成 moduleId 的规则就是自增,所以这里要替换成我们自己的配置逻辑,我们要做拆包就需要保证这个 id 不能重复,但是这个 id 只是在打包时生成,如果我们单独打业务包,基础包,这个 id 的连续性就会丢失,所以对于 id 的处理,我们还是可以参考上述开源项目,每个包有十万位间隔空间的划分,基础包从 0 开始自增,业务 A 从 1000000 开始自增,又或者通过每个模块自己的路径或者 uuid 等去分配,来避免碰撞,但是字符串会增大包的体积,这里不推荐这种做法。
在基础包生成以后,打业务包的时候过滤所有基础包moduleId即可
function postProcessModulesFilter(module) {
const path = module["path"];
for (let i = 0, len = excludeFiles.length; i < len; i++) {
if (path.indexOf(excludeFiles[i]) >= 0) {
return false;
}
}
return true;
}
Android、RN打包比较
apk,bundle文件都是能够在对应平台安装运行的压缩文件,不管是gradle还是metro都只是担任打包角色,换一种工具一样可行
两者都提供了开发可以介入打包流程的入口,都是责任链+拦截器模式
RN的hermes原理是提前预编译减少启动时间,同样Android在5.0以上引入了art也是对apk中的dex文件进行预编译,来减少app启动时长
RN打包原文参考: blog.gaogangsever.cn/react/react…
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!