Android、RN打包原理分析

4,728 阅读11分钟

目录:

Android打包原理

Android apk文件夹目录结构

Android打包脚本gradle执行流程流程

App安装流程

RN打包原理

RN bundle文件结构

metro打包流程

RN拆包原理

Android、RN打包比较

Android、RN打包比较

在分析一件事物原理的时候我喜欢从结果开始逆向推理过程,这样更容易理解

Android打包原理

Android apk文件夹目录结构

image.png

image.png

无图无真相,通过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打包图镇楼

image.png

如上图所示:

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安装流程

image.png

  1. 复制APK到/data/app目录下,解压并扫描安装包。
  2. 资源管理器解析APK里的资源文件。
  3. 解析AndroidManifest文件,并在/data/data/目录下创建对应的应用数据目录。
  4. 然后对dex文件进行优化,并保存在dalvik-cache目录下。
  5. 将AndroidManifest文件解析出的四大组件信息注册到PackageManagerService中。
  6. 安装完成后,发送广播。

RN打包原理

RN bundle文件结构

image.png

image.png

如上图,生成的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 打包的整个流程大致分为:

image.png

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 转换后的代码

image.png

生成的依赖关系图谱如下

[
{
  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.停止打包服务

停止打包服务

总结如下几点:

  1. 整个 metro 进行依赖分析和 babel 转换主要通过了JestHasteMap (opens new window)去做依赖分析;
  2. 在做依赖分析的通过,metro 会监听当前目录的文件变化,然后以最小变化生成最终依赖关系图谱;
  3. 不管是入口文件解析还是 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…