图片 16

加快Android编译速度的技巧总结

对于Android开发者而言,随着工程不断的壮大,Android项目的编译时间也逐渐变长,即便是有时候添加一行代码也需要等待好久才能看见期待的效果。之前加快Android编译的工具相对较少,其中最具有代表性的开源项目当属FaceBook的Buck和
mmin18的LayoutCast,除此之外还有JRebel 和 Jimulabs。不过前两天google宣布推出Instant
Run加快Android
编译速度,相信对其他的工具来说都是一次冲击,这也是写这篇文章的动机。

预备知识

  • 知识详解模块

    • dex/class深入理解
      • 什么是class文件:
        能够被JVM识别,加载并执行的文件格式。(java语言,Scala语言,Python语言,其他语言,都可以生成class文件)。
      • 如何生成一个class 文件:
        IDE自动生成或者手动通过javac去生成class
        文件。通过java命令去执行java文件。
      • class文件的作用:记录一个类中的所有信息。
      • class文件格式讲解:一种8位字节的二进制文件。各个数据按顺序紧密排列,无间隙。每个类或者接口都占用一个class
        文件。
        ![](https://upload-images.jianshu.io/upload_images/325120-24f3da0bcecf237d.png)



        ![](https://upload-images.jianshu.io/upload_images/325120-f7838ecabd932095.png)
  • constant_pool

    • CONTANT_Integer_info
    • CONTANT_Long_info
    • CONTANT_String_info
    • CONTANT_Class_info
    • CONTANT_Fieldref_info
    • CONTANT_Methodref_info
  • class文件的弊端

    • 内存占用大,不适合移动端。
    • 堆栈的加载模式,加载速度慢。
    • 文件IO操作多,类查找慢。
  • dex文件的优点

    • 什么是dex文件: 能够被DVM识别,加载并执行的文件格式。
    • 如何生成dex文件:通过IDE生成或者通过dx命令生成dex文件。
    • dex文件的作用:记录整个工程所有文件的信息。
    • dex
      文件格式详解:一种8位字节的二进制流文件,各个数据按顺序排列,无间隙,整个应用中所有的java源文件都放在dex中。

图片 1

图片 2

  • jvm/dvm/art深入理解

    • Java虚拟机结构解析
      • JVM整体结构讲解
        ![](https://upload-images.jianshu.io/upload_images/325120-a4ca03d23d842a02.png)

        ![](https://upload-images.jianshu.io/upload_images/325120-4ac150171cd039e2.png)

        ![](https://upload-images.jianshu.io/upload_images/325120-77e7ec9214d7f309.png)

        ![](https://upload-images.jianshu.io/upload_images/325120-44ed64a60d978e78.png)

-   Loading:类的信息从文件中获取并且载入到JVM的内存中。
-   Verifying:检测读入的结构是否符合JVM规范的描述。
-   Preparing:分配一个结构用来存储类信息。
-   Resolving:把这个类的常量池中的所有的符号引用改为直接引用。
-   Intializing:执行静态初始化程序,把静态变量初始化成指定的值。
  • Java栈区

    • 作用:它存放的是Java方法执行时的所有的数据。
    • 组成:由栈帧组成,一个栈帧代表一个方法的执行。
  • Java栈帧

    • 每个方法从调用到执行完成就对应一个栈帧在虚拟机栈中入栈到出栈。比如:A
      方法调用B方法,java虚拟机就会创建保存B方法的栈帧,将其压入栈中。当B方法完成时,回到A方法时,保存B方法的栈帧弹出栈区。
    • 局部变量表、栈操作数、动态链接、方法出口。
  • 本地方法栈

    • 作用:本地方法栈是专门为Native方法服务。
  • 方法区

    • 作用:存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后等数据。
  • 堆区

    • 作用:所有通过new 创建的对象的内存都在堆中分配。
    • 特点:是虚拟机中最大的一块内存,是GC要回收的部分。
    ![](https://upload-images.jianshu.io/upload_images/325120-6b72a555bf770827.png)
  • Java代码的编译和执行过程

    • 内存管理和垃圾回收

      • 垃圾回收算法: 1: 引用计数算法。2:可达性算法。

      • 对象之间的引用关系:强、弱,虚、软 引用。

      • 弱引用方式:WeakReference<Object> wf = new
        WeakReference<Object>(obj);

      • 垃圾回收算法:

        ![](https://upload-images.jianshu.io/upload_images/325120-770beee38854bd96.png)

    -   优点:不需要对象的移动,仅对不存活的对象处理
        。缺点:直接清理,会造成内存碎片。

    -   复制算法:



        ![](https://upload-images.jianshu.io/upload_images/325120-8d02f4960c5c6200.png)

    -   优点:存活对象少时,极为高效
        。缺点:需要一块内存作为交换空间,来移动数据。

    -   标记-整理算法



        ![](https://upload-images.jianshu.io/upload_images/325120-82ca4a678459495f.png)

    -   触发回收

        -   Java虚拟机无法再为新的对象分配内存空间。
        -   手动调用System.gc()方法。不会立刻启动垃圾回收。强烈不推荐。
        -   低优先级的GC线程,被运行时就会执行GC。

-   Dalvik与JVM不同

    -   执行的文件不同,一个class ,一个是dex。
    -   类加载的系统与JVM区别较大。
    -   可以同时存在多个DVM。
    -   Dalvik是基于寄存器的,而JVM是基于栈的。

-   ART比Dalvik有哪些优势

    -   DVM使用JIT来将字节码转化为机器码,效率低。
    -   ART采用AOT 预编译技术,执行速度更快。
    -   ART会占用更多的应用安装时间和存储空间。
  • Class loader深入理解

    • Android中的ClassLoader 作用详解

      • 概述:ClassLoader种类:
        • BootClassLoader:用来加载Android
          framework层的字节码文件。
        • PathClassLoader:已经安装的系统中的APK的字节码文件。
        • DexClassLoader:用来加载指定目录的字节码文件。
            ![](https://upload-images.jianshu.io/upload_images/325120-bda41bb2d5a39965.png)

-   Android中的ClassLoader 的特点

    -   双亲代理模式
        -   就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。提高了类的加载效率。
    -   类加载的共享功能。
    -   类加载的隔离功能。
  • ClassLoader源码解析
![](https://upload-images.jianshu.io/upload_images/325120-389187a7a641715b.png)

![](https://upload-images.jianshu.io/upload_images/325120-8ea6ef27055253e3.png)

![](https://upload-images.jianshu.io/upload_images/325120-89fed7ae12279ef7.png)
  • Android中的动态加载比一般的Java 程序复杂在哪里

    • 有许多组件类需要注册才能使用。
    • 资源的动态加载很复杂。
    • Android程序运行需要一个上下文环境。也是比较难解决的问题,需要学习他们是怎么解决问题的。

上篇文章讲到了ant方式进行dex分包《Android
Dex分包》,本篇文章再来看一下采用gradle方式进行dex分包的实现。

相对于Buck而言,LayoutCast显得更轻量一些,对项目的侵入性较弱。今年8月份的时候,花了一个星期左右的时间才完成公司的代码的适配,对于一些繁重的项目而言,Buck带来的好处是显而易见的,但是适配过程中的坑也是很多的。Instant
Run
对项目的侵入性其实也是比较大的,但是这些都不需要用户去操作、配置,所以看起来和LayoutCast一样属于轻量型的。

1:什么是热修复

  • 一般的bug修复,都是等下一个版本解决,然后发布新的apk。
  • 热修复:可以直接在客户已经安装的程序当中修复bug。
  • bug一般会出现在某个类的某个方法地方。
    • 如果我们能够动态地将客户手机里面的apk里面的某个类给替换成我们已经修复好的类。
  • Instant run
    • 在做今天的热修复的时候记得把Instant
      run功能关闭。不然会影响我们今天的热修复实现

dex分包的gradle方式实现

我们用同样的demo工程采用gradle进行multidex分包测试。由于本人的AS已经升到2.3.1版本,对应的gradle版本为2.3.1,gradle插件版本升到了3.3,而gradle插件3.3版本要求buildToolsVersion版本为25及以上,而buildTools
25又要求jdk版本大于等于52,即jdk1.8,所以需要将android
studio切换到jdk1.8,需要自行下载jdk1.8并配置好环境即可,build.gradle中不需要配置

android
studio配置jdk1.8,网上有些教程推荐直接在build.gradle中配置即可,如果是在build.gradle中指定了用jdk1.8来编译

 compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }

会编译失败,报如下错误

 * What went wrong:
A problem occurred configuring project ':app'.
> Jack is required to support java 8 language features. Either enable Jack or remove sourceCompatibility JavaVersion.VERSION_1_8.

要求使用Jack编译器来支持java8特性,或者移除sourceCompatibility直接编译。

如果要使用Jack编译器,则需要在build.gradle添加如下支持

jackOptions {
    enabled true
}

关于Jack编译器,可参考《Android 新一代编译 toolchain Jack & Jill
简介》一文

Jack 是 Java Android Compiler Kit 的缩写,它可以将 Java 代码直接编译为
Dalvik 字节码,并负责 Minification, Obfuscation, Repackaging,
Multidexing, Incremental compilation。它试图取代
javac/dx/proguard/jarjar/multidex 库等工具

使用Jack编译器来编译之后,可以正常打包构建,并且也进行了mulitdex处理,但是dexOptions中的参数都未生效,究其原因就是由于采用了Jack编译器来执行编译操作,不同与原来的
javac+dx编译过程,二者区别如下:

//javac+dx编译过程
javac (.java –> .class) –> dx (.class –> .dex)
//jack编译过程
Jack (.java –> .jack –> .dex)

Jack是将java源码编译城.jack文件再转化为.dex文件,不再执行dx操作,所以配置的dexOptions没有生效

本来google推出Jack
编译器是准备取代javac+dx的编译方式,但是由于Jack在支持基本编译功能之外的其他功能上存在一定的局限,所以在今年3月,Google宣布放弃Jack,重新采用javac+dx的方式在Android里支持Java
8。

所以我们这里没有采用这种编译方式,没有在gradler脚本中配置jdk1.8,而是直接在系统变量中更改编译环境为jdk1.8

图片 3

classesdex.png

demo中build.gradle脚本如下

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "25.0.0"

    defaultConfig {

        applicationId "com.example.multidextest"
        minSdkVersion 14
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        multiDexEnabled true

//这里不采用jack编译方式
//        jackOptions {
//            enabled true
//        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

//    compileOptions {
//        sourceCompatibility 1.8
//        targetCompatibility 1.8
//    }

    dexOptions {
        javaMaxHeapSize "1g"
        preDexLibraries = false
        additionalParameters = [    //配置multidex参数
                                '--multi-dex',//多dex分包
                                '--set-max-idx-number=30000',//每个包内方法数上限
                                '--main-dex-list='+projectDir+'/main-dex-rule', //打包到主classes.dex的文件列表
                                '--minimal-main-dex'
        ]
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:23.3.0'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    //multidex支持依赖
    compile 'com.android.support:multidex:1.0.0'
    testCompile 'junit:junit:4.12'
}

main-dex-rule文件内容如下:

com/example/multidextest/MainActivity$1.class
com/example/multidextest/HelperOne.class
com/example/multidextest/MainActivity.class
com/example/multidextest/ApplicationLoader.class

执行gradle命令后,得到构建出的apk文件,通过as可以看到已经包含了多个dex

主dex中包含指定的类文件

图片 4

classesdex.png

从dex中包含其他的未打到主dex中的类和其他依赖的jar包等

图片 5

classes2dex.png

关于main-dex-rule文件的自动生成方式,可以参考
可参考《Android傻瓜式分包插件》或者《android
multidex异步加载》

时间去哪了?

Android程序编译大致过程如图所示,详细的过程可以参考gradle 中的tasks。

图片 6

那么为什么我们每次编译都需要等待那么久?事实上我们我们可以gradle中添加TaskExecutionListener来监听gradle脚本中每个task的执行时间。

class TimingsListener implements TaskExecutionListener, BuildListener {
    private Clock clock
    private timings = []
    @Override
    void beforeExecute(Task task) {
        clock = new org.gradle.util.Clock()
    }
    @Override
    void afterExecute(Task task, TaskState taskState) {
        def ms = clock.timeInMs
        timings.add([ms, task.path])
        task.project.logger.warn "${task.path} took ${ms}ms"
    }
    @Override
    void buildFinished(BuildResult result) {
        println "Task timings:"
        for (timing in timings) {
            if (timing[0] >= 50) {
                printf "%7sms  %sn", timing
            }
        }
    }
    @Override
    void buildStarted(Gradle gradle) {}

    @Override
    void projectsEvaluated(Gradle gradle) {}

    @Override
    void projectsLoaded(Gradle gradle) {}

    @Override
    void settingsEvaluated(Settings settings) {}
}

gradle.addListener new TimingsListener()

执行脚本可以发现主要的费时在dex(包含preDex)以及install这两个步骤。BUCK和LayoutCast的主要工作也是集中于这些费时的步骤上面。

2:热更新的流程

  • 1: 线上检测到严重的 crash。

  • 2:拉出bugfix分支并在分支上解决问题。

  • 3:jenkins构建和补丁生成。

  • 4:app通过推送或主动拉取补丁文件。

  • 5:将bugfix代码合并到master上。

dex文件的加载

上篇文章已经提到,apk初次安装启动的时候只会对主dex进行优化加载操作,而从dex文件需要在app启动时手动加载,AS中可以通过引入multidex包来支持从dex的加载,有三种方式,如下:

1.manifest文件中指定Application为MultiDexApplication,对于一般不需要在application中执行初始化操作的app可以采用这种

<application
        android:name="android.support.multidex.MultiDexApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
        ……>

2.自定义Application并继承MultiDexApplication

public class MyApplication extends MultiDexApplication{
        ……
}

3.重写Application的attachBaseContext方法

public class MyApplication extends Application{

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }
}

方式一二相同,先来看方式二的实现只需要将ApplicationLoader类由原先继承自Application类修改为继承MultiDexApplication即可,无需在onCreate中添加其他加载dex的代码。所以可以猜想,MultiDexApplication中肯定是执行了加载从dex的相关操作。下面来看MultiDexApplication的源码

public class MultiDexApplication extends Application {
    public MultiDexApplication() {
    }

    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(this);
    }
}

可以看到MultiDexApplication 继承Application,
并在attachBaseContext()中调用了MultiDex.install(this),所以上述几种方式本质是相同的。

MultiDex.install()方法如下:

 /**
     * Patches the application context class loader by appending extra dex files
     * loaded from the application apk. This method should be called in the
     * attachBaseContext of your {@link Application}, see
     * {@link MultiDexApplication} for more explanation and an example.
     *
     * @param context application context.
     * @throws RuntimeException if an error occurred preventing the classloader
     *         extension.
     */
    public static void install(Context context) {

        //省略若干代码...

        try {
            ApplicationInfo applicationInfo = getApplicationInfo(context);
            if (applicationInfo == null) {
                // Looks like running on a test Context, so just return without patching.
                return;
            }

            synchronized (installedApk) {
                String apkPath = applicationInfo.sourceDir;

                //installedApk 为set集合,防止dex重复加载
                if (installedApk.contains(apkPath)) {
                    return;
                }
                installedApk.add(apkPath);

                //省略若干代码...

                ClassLoader loader;
                try {
                    //此处获取到的是PathClassLoader
                    loader = context.getClassLoader();
                } catch (RuntimeException e) {
                   //...
                    return;
                }

                //...

                try {
                  clearOldDexDir(context);
                } catch (Throwable t) {
                  Log.w(TAG, "Something went wrong when trying to clear old MultiDex extraction, "
                      + "continuing without cleaning.", t);
                }

                //data/data/<packagename>/code_cache/secondary-dexes"   即从dex优化后的缓存的路径
                File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
                //从apk中抽取dex文件并存到缓存目录下,保存为zip文件
                List<File> files = MultiDexExtractor.load(context, applicationInfo, dexDir, false);
                if (checkValidZipFiles(files)) {
                    //
                    installSecondaryDexes(loader, dexDir, files);
                } else {
                    Log.w(TAG, "Files were not valid zip files.  Forcing a reload.");
                    // Try again, but this time force a reload of the zip file.
                    files = MultiDexExtractor.load(context, applicationInfo, dexDir, true);

                    if (checkValidZipFiles(files)) {
                        installSecondaryDexes(loader, dexDir, files);
                    } else {
                        // Second time didn't work, give up
                        throw new RuntimeException("Zip files were not valid.");
                    }
                }
            }

        } catch (Exception e) {
            Log.e(TAG, "Multidex installation failure", e);
            throw new RuntimeException("Multi dex installation failed (" + e.getMessage() + ").");
        }
        Log.i(TAG, "install done");
    }

重点关注MultiDexExtractor.load(context, applicationInfo, dexDir, false)
从apk中抽取出从dex,

该方法有四个参数
context 上下文
applicationInfo 应用信息,用于获取apk文件
dexDir dex文件优化后的缓存路径
forceReload 是否强制重新从apk文件中抽取dex

 /**
     * Extracts application secondary dexes into files in the application data
     * directory.
     *
     * @return a list of files that were created. The list may be empty if there
     *         are no secondary dex files.
     * @throws IOException if encounters a problem while reading or writing
     *         secondary dex files
     */
    static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir,
            boolean forceReload) throws IOException {
        Log.i(TAG, "MultiDexExtractor.load(" + applicationInfo.sourceDir + ", " + forceReload + ")");
        final File sourceApk = new File(applicationInfo.sourceDir);

        //首先进行crc校验
        long currentCrc = getZipCrc(sourceApk);

        List<File> files;
        if (!forceReload && !isModified(context, sourceApk, currentCrc)) {
            try {
                  //已经从apk中抽取出dex文件并存到缓存目录中,则直接返回zip文件list
                files = loadExistingExtractions(context, sourceApk, dexDir);
            } catch (IOException ioe) {
                Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
                        + " falling back to fresh extraction", ioe);
                files = performExtractions(sourceApk, dexDir);
                putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);

            }
        } else {
            Log.i(TAG, "Detected that extraction must be performed.");
            //从apk中复制dex文件到缓存目录
            files = performExtractions(sourceApk, dexDir);
            //保存时间戳、crc、dex数量等信息到sp
            putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
        }

        Log.i(TAG, "load found " + files.size() + " secondary dex files");
        return files;
    }

forceReload为false并且已经从apk中抽取过dex文件则直接调用loadExistingExtractions
返回dex文件的zip列表

  private static List<File> loadExistingExtractions(Context context, File sourceApk, File dexDir)
            throws IOException {
        Log.i(TAG, "loading existing secondary dex files");

        //dex文件的前缀 ,即data/data/packageName/code_cache/secondary-dexes/data/data/apkName.apk.classes
        final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
        //获取dex数目
        int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
        final List<File> files = new ArrayList<File>(totalDexNumber);

        //遍历除主dex外的其他dex
        for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
             //文件名为 data/data/packageName/code_cache/secondary-dexes/data/data/apkName.apk.classes*.zip
            String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
            //以zip文件形式返回
            File extractedFile = new File(dexDir, fileName);
            if (extractedFile.isFile()) {
                //添加到list中并返回
                files.add(extractedFile);
                if (!verifyZipFile(extractedFile)) {
                    Log.i(TAG, "Invalid zip file: " + extractedFile);
                    throw new IOException("Invalid ZIP file.");
                }
            } else {
                throw new IOException("Missing extracted secondary dex file '" +
                        extractedFile.getPath() + "'");
            }
        }

        return files;
    }

否则调用performExtractions()方法从apk中抽取dex文件

private static List<File> performExtractions(File sourceApk, File dexDir)
            throws IOException {

        final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;

        // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
        // contains a secondary dex file in there is not consistent with the latest apk.  Otherwise,
        // multi-process race conditions can cause a crash loop where one process deletes the zip
        // while another had created it.
        prepareDexDir(dexDir, extractedFilePrefix);

        List<File> files = new ArrayList<File>();

        final ZipFile apk = new ZipFile(sourceApk);
        try {

            int secondaryNumber = 2;
            //获取classes2.dex
            ZipEntry dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
            while (dexFile != null) {
                 //data/data/packageName/code_cache/secondary-dexes/data/data/apkName.apk.classes*.zip
                String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
                File extractedFile = new File(dexDir, fileName);
                //添加到list列表中
                files.add(extractedFile);

                Log.i(TAG, "Extraction is needed for file " + extractedFile);
                int numAttempts = 0;
                boolean isExtractionSuccessful = false;
                //最多重试3次
                while (numAttempts < MAX_EXTRACT_ATTEMPTS && !isExtractionSuccessful) {
                    numAttempts++;

                    // Create a zip file (extractedFile) containing only the secondary dex file
                    // (dexFile) from the apk.
                    //从apk中抽取classes*dex文件并重命名为zip文件保存到指定目录
                    extract(apk, dexFile, extractedFile, extractedFilePrefix);

                    // Verify that the extracted file is indeed a zip file.
                    //判断是否抽取成功
                    isExtractionSuccessful = verifyZipFile(extractedFile);

                    // Log the sha1 of the extracted zip file
                    Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "success" : "failed") +
                            " - length " + extractedFile.getAbsolutePath() + ": " +
                            extractedFile.length());
                    if (!isExtractionSuccessful) {
                        // Delete the extracted file
                        extractedFile.delete();
                        if (extractedFile.exists()) {
                            Log.w(TAG, "Failed to delete corrupted secondary dex '" +
                                    extractedFile.getPath() + "'");
                        }
                    }
                }
                if (!isExtractionSuccessful) {
                    throw new IOException("Could not create zip file " +
                            extractedFile.getAbsolutePath() + " for secondary dex (" +
                            secondaryNumber + ")");
                }
                //自增以读取下一个classes*.dex文件
                secondaryNumber++;
                dexFile = apk.getEntry(DEX_PREFIX + secondaryNumber + DEX_SUFFIX);
            }
        } finally {
            try {
                apk.close();
            } catch (IOException e) {
                Log.w(TAG, "Failed to close resource", e);
            }
        }

        return files;
}

抽取方法extract()

private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo,
            String extractedFilePrefix) throws IOException, FileNotFoundException {

                //获取classes*.dex 对应输入流
        InputStream in = apk.getInputStream(dexFile);
        ZipOutputStream out = null;
        //创建临时文件
        File tmp = File.createTempFile(extractedFilePrefix, EXTRACTED_SUFFIX,
                extractTo.getParentFile());
        Log.i(TAG, "Extracting " + tmp.getPath());
        try {
            //输出为zip文件,zip文件中包含classes.dex
            out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
            try {
                ZipEntry classesDex = new ZipEntry("classes.dex");
                // keep zip entry time since it is the criteria used by Dalvik
                classesDex.setTime(dexFile.getTime());
                out.putNextEntry(classesDex);

                byte[] buffer = new byte[BUFFER_SIZE];
                int length = in.read(buffer);
                while (length != -1) {
                    out.write(buffer, 0, length);
                    length = in.read(buffer);
                }
                out.closeEntry();
            } finally {
                out.close();
            }
            Log.i(TAG, "Renaming to " + extractTo.getPath());
            if (!tmp.renameTo(extractTo)) {
                throw new IOException("Failed to rename "" + tmp.getAbsolutePath() +
                        "" to "" + extractTo.getAbsolutePath() + """);
            }
        } finally {
            closeQuietly(in);
            tmp.delete(); // return status ignored
        }
}

再回到MultiDex的install()方法中,通过MultiDexExtractor.load()得到dex文件的zip列表后,调用installSecondaryDexes()

 private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files)
            throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
            InvocationTargetException, NoSuchMethodException, IOException {
        if (!files.isEmpty()) {
            if (Build.VERSION.SDK_INT >= 19) {
                V19.install(loader, files, dexDir);
            } else if (Build.VERSION.SDK_INT >= 14) {
                V14.install(loader, files, dexDir);
            } else {
                V4.install(loader, files);
            }
        }
    }

根据sdk版本不同,调用对应的方法,V19、V14、V4都是MultiDex的内部类,处理的逻辑也差不多,这里主要看一下V19

     /**
     * Installer for platform versions 19.
     */
    private static final class V19 {

        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                File optimizedDirectory)
                        throws IllegalArgumentException, IllegalAccessException,
                        NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
            /* The patched class loader is expected to be a descendant of
             * dalvik.system.BaseDexClassLoader. We modify its
             * dalvik.system.DexPathList pathList field to append additional DEX
             * file entries.
             */
             //获取PathClassLoader的pathList成员变量,即DexPathList对象,其成员变量dexElements用于存储dex文件相关信息
            Field pathListField = findField(loader, "pathList");
            Object dexPathList = pathListField.get(loader);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();

            //调用makeDexElements方法,内部通过反射调用DexPathList的makeDexElements方法,返回dexElements
            //参数为/code_cache/secondary-dexes缓存目录中包含classes.dex的zip文件list以及优化后的dex文件存放目录
            //expandFieldArray方法先获取dexPathList对象的现有dexElements变量,然后建其和makeDexElements方法返回
            //的dexElements数组合并,然后再将合并之后的结果设置为dexPathList对象的dexElements变量
            expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
                    new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                    suppressedExceptions));
            if (suppressedExceptions.size() > 0) {
                for (IOException e : suppressedExceptions) {
                    Log.w(TAG, "Exception in makeDexElement", e);
                }
                Field suppressedExceptionsField =
                        findField(loader, "dexElementsSuppressedExceptions");
                IOException[] dexElementsSuppressedExceptions =
                        (IOException[]) suppressedExceptionsField.get(loader);

                if (dexElementsSuppressedExceptions == null) {
                    dexElementsSuppressedExceptions =
                            suppressedExceptions.toArray(
                                    new IOException[suppressedExceptions.size()]);
                } else {
                    IOException[] combined =
                            new IOException[suppressedExceptions.size() +
                                            dexElementsSuppressedExceptions.length];
                    suppressedExceptions.toArray(combined);
                    System.arraycopy(dexElementsSuppressedExceptions, 0, combined,
                            suppressedExceptions.size(), dexElementsSuppressedExceptions.length);
                    dexElementsSuppressedExceptions = combined;
                }

                suppressedExceptionsField.set(loader, dexElementsSuppressedExceptions);
            }
        }

        /**
         * A wrapper around
         * {@code private static final dalvik.system.DexPathList#makeDexElements}.
         */
        private static Object[] makeDexElements(
                Object dexPathList, ArrayList<File> files, File optimizedDirectory,
                ArrayList<IOException> suppressedExceptions)
                        throws IllegalAccessException, InvocationTargetException,
                        NoSuchMethodException {
            Method makeDexElements =
                    findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class,
                            ArrayList.class);

            return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory,
                    suppressedExceptions);
        }
    }

makeDexElements()其实就是通过反射方式调用dexPathList对象的makeDexElements方法,将从dex添加到其dexElements属性中,具体的过程在前面的文章中已经介绍过—《android
Dex文件的加载》,这里不再赘述。

     private static void expandFieldArray(Object instance, String fieldName,
            Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        Field jlrField = findField(instance, fieldName);
        Object[] original = (Object[]) jlrField.get(instance);
        Object[] combined = (Object[]) Array.newInstance(
                original.getClass().getComponentType(), original.length + extraElements.length);
        System.arraycopy(original, 0, combined, 0, original.length);
        System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
        jlrField.set(instance, combined);
    }

到这里MultiDex.install(this)方法的逻辑就分析完了,可以看到其中的处理步骤和上篇文章ant方式中我们手动加载从dex的方式基本上是一致的,所以这两种方式并没有本质上的区别。

如何加快?

开发过程中对项目的改动一般分为Java文件的修改以及资源文件的修改,这些修改都会涉及到上述的几个费时步骤,这也就是为什么即便我们修改一行代码也需要编译很久。

3:主流的热更新框架介绍

  • 1:阿里系: Dexposed,andfix 底层是从二进制入手的。

  • 2:腾讯系:thinker java的加载机制入手的

图片 7

图片 8

1、Java文件修改

通常,修改的.java文件会先经过javac操作生成.class文件。而后与其他的.class文件经过dx生成.dex文件。经过dx的操作很费时,针对这种情况,BUCK、LayoutCast和Instant
Run采用了两种方法来解决。

4:热更新机制

  • Android类加载机制

    • 我们把 App 的 dex 分成两部分:
      • patch 库的 dex 文件 -> classes.dex
      • 其他业务代码的 dex 文件 -> classes[N].dex

     public class PathClassLoader extends BaseDexClassLoader {
          用来加载应用程序的dex
    }
    
    public class DexClassLoader extends BaseDexClassLoader {
      可以加载指定的某个dex文件。(限制:必须要在应用程序的目录下面)
    }
    
    • 修复方案:搞多个dex。
      • 第一个版本:classes.dex
        -修复后的补丁包:classes2.dex(包涵了我们修复xxx.class)
  • 热修复机制

    • 替换工程内的类文件

      • 实现思路:类在使用之前必须要经过加载器的加载才能够使用,在加载类时会调用自身的findClass()方法进行查找。然而在Android中类的查找使用的是BaseDexClassLoader,BaseDexClassLoader对findClass()方法进行了重写:

图片 9

  • 如果可以解决这个问题:把两个dex合并—将修复的class替换原来出bug的class.

    • 通过BaseDexClassLoader调用findClass(className)
      Class<?> findClass(String name)

    • 将修复好的dex插入到dexElements的集合,位置:出现bug的xxx.class所在的dex的前面。

    • List of dex/resource (class path) elements.Element[]
      dexElements;存储的是dex的集合

    • 最本质的实现原理:类加载器去加载某个类的时候,是去dexElements里面从头往下查找的。

    • fixed.dex,classes1.dex,classes2.dex,classes3.dex

    • 1:dexElements

    • 2: ClassLoader会遍历这个数组。

图片 10

image

BUCK

BUCK建立了一套完善的依赖规则以及细化的缓存系统来缩减编译时间,并通过使用三方的dex
merege工具将.dex文件合并的时间复杂度从O(N^2)降到O(NlgN)。

图片 11

如图所示,当修改A.java文件时,只涉及到相应的dx操作以及dex
merge操作(红色部分),这样就大大的缩减了dx的操作时间。BUCK在依赖规则上狠下功夫推出了ABI,更是进一步的减少了不必要的操作。

5:热修复AndFix详解

图片 12

  • 方法替换:
    • AndFix通过Java的自定义注解来判断一个方法是否应该被替换,如果可以就会hook该方法并进行替换。AndFix在ART架构上的Native方法是art_replaceMethod
      、在X86架构上的Native方法是dalvik_replaceMethod。他们的实现方式是不同的。对于Dalvik,它将改变目标方法的类型为Native同时hook方法的实现至AndFix自己的Native方法,这个方法称为
      dalvik_dispatcher,这个方法将会唤醒已经注册的回调,这就是我们通常说的hooked(挂钩)。对于ART来说,我们仅仅改变目标方法的属性来替代它。

图片 13

process.png

  • 使用AndFix修复应用
    操作步骤:

    • 如果你用的是gradle依赖,添加如下代码:

dependencies {
    compile 'com.alipay.euler:andfix:0.3.1@aar'
}
  • 如何使用AndFix ?

    • 初始化 PatchManager

 patchManager = new PatchManager(context);
 patchManager.init(appversion);//current version
  • Load patch 加载补丁

patchManager.loadPatch();
  • 你应该尽可能早的加载补丁,通常都是在Application的onCreate()方法中进行初始化。

  • Add patch 添加补丁

  patchManager.addPatch(path);//path:补丁文件下载到本地的路径。
  • 当一个新的补丁文件被下载后,调用addPatch(path)就会立即生效。

  • 下边我们使用组件化方法封装AndFix第三方组件:

    图片 14

  • 准备阶段

    • build一个有bug的old APK并安装到手机。
    • 分析问题解决bug后,build 一个new apk。
  • patch生成阶段

    • AndFix 提供了一个补丁创建工具
      apkpatch.

    • 如何使用这个工具

      • 1.准备两个应用apk:
        一个是线上的apk,另一个是修复了bug的apk.
        -2.通过提供的两个apk生成补丁文件.
    • apkpatch命令详解

      • 执行 ./apkpatch.sh

       usage: apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <***> -a <alias> -e <***>
       -a,--alias <alias>     keystore entry alias.
       -e,--epassword <***>   keystore entry password.
       -f,--from <loc>        new Apk file path.
       -k,--keystore <loc>    keystore path.
       -n,--name <name>       patch name.
       -o,--out <dir>         output dir.
       -p,--kpassword <***>   keystore password.
       -t,--to <loc>          old Apk file path.
      
    • 获取补丁文件。接下来你就可以通过某种方式将该补丁文件分发给你的客户端。

    • patch安装阶段

      • 将apatch文件通过adb
        push到手机或者发布到服务器,客户端下载该apatch。

      adb push wangpu.apatch   /storage/emulated/0/Android/data/com.wangpu/cache/apatch/wangpu.apatch  //不同的路径
      
      • 使用户已经安装的应用load我们的apatch文件。
      • load成功后验证我们的bug 是否修复。

LayoutCast

LayoutCast的实现同很多插件的实现原理差不多,具体分析如下:

在ClassLoader查找类的时候会先去调用BaseDexClassLoader类中的findClass方法。

//----dalvik/system/BaseDexClassLoader.java  
 protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

随后在DexPathList类中根据dexElements来查找相应的class。

//----dalvik/system/DexPathList.java  
public Class findClass(String name) {
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;
            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        return null;
    }

其中dexElements代表着不同dex文件。

/** list of dex/resource (class path) elements */
    private final Element[] dexElements;

也就是说,在ClassLoader加载类的时候会去按照dexElements中dex文件的顺序依次查找,如下图所示,在1.dex中查找到了A类,那么就不会再从后面的dex文件中继续查找了。

图片 15

LayoutCast就是利用这样的原理,将修改的Java文件生成dex文件,并将此dex文件利用反射的方式插入到dexElements数组的前面。当然,从Java到dex的过程需要额外的查找各种依赖包之类的工作,这部分工作在cast.py中实现。

这种方式的实现在ART下是没有问题的,但是在Dalvik中就会出现IllegalAccessError的问题

java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
dalvik.system.DexFile.defineClass(Native Method)
dalvik.system.DexFile.loadClassBinaryName(DexFile.java:211)
dalvik.system.DexPathList.findClass(DexPathList.java:315)
dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.j

具体的原因以及解决方案可以参考Bugly的文章

6、将AndFix 组件化 思考

思考:组件化的步骤:

  • 1: 发现bug生成apatch文件。

  • 2:将apatch下发到手机存储系统。(下载模块)

  • 3:利用AndFix完成patch安装,解决bug。

![](https://upload-images.jianshu.io/upload_images/325120-1d32ffa49dae45c6.png)

Install Run

Install Run
同样也是生成新的增量dex,但是新增dex中的类和原来的类名有区别。比如说,在修改Hello.java类之后,会生成包含Hello$overide类的dex文件。

那么,这个新增的dex文件中Hello$Override类是如何被调用的?

我们先看看原来的Hello.java文件经过Instant Run 编译前后的区别:

编译前的hello.java文件

public String name(String str) {
    return str;
}

经过Instant Run之后的

---compiled  Hello.java
public String name(String str) {
       IncrementalChange var2 = $change;
       return var2 != null?(String)var2.access$dispatch("name.(Ljava/lang/String;)Ljava/lang/String;", new Object[]{this, str}):str;
   }

可以看出,如果$change存在的话,就会调用$change中相应的函数,那么我们只需要通过反射将Hello.java中$change字段改为修改后的Hello$override的类就Ok了。

这也就是为什么Instant
Run并不存在前面说到的IllegalAccessError的问题,并且支持不重启就能看见修改效果的原因。具体可以看看寒江不钓的博客

7、AndFix优劣

  • 原理简单,集成简单,使用简单,即使生效。
  • 只能修复方法及别的bug,极大的限制了使用场景。

2、Res修改

Resource文件的修改会涉及到AAPT、ApkBuilder以及最后的Install操作。其中APPT的操作要求比较高,LayoutCast、Instant
Run均没有在这部分进行优化,他们的主要工作在于后面的两个操作。其主要的思路在于将修改的后的资源利用aapt打包成新的.ap_文件,并通过反射的方式将原来的资源文件改为修改后的。

LayoutCast

LayoutCast主要做了两件事。

修改LayoutInflater服务

对于下面的用法我们并不陌生:

LayoutInflater layoutInflater = LayoutInflater.from(context);
View view = layoutInflater.inflate(resourceId, root);

其中LayoutInflater.from的实现是在Context的实现类ContextImp中获取LAYOUT_INFLATER_SERVICE系统服务

//----  android/view/LayoutInflater.java
public static LayoutInflater from(Context context) {
         LayoutInflater LayoutInflater =
                 (LayoutInflater)context.getSystemService(Context.
                 LAYOUT_INFLATER_SERVICE);
         if (LayoutInflater == null) {
             throw new AssertionError("LayoutInflater not found.");
         }
         return LayoutInflater;
     }

那么ContextImpl又是如何获取相应的服务的,查看ContextImpl类可以发现,

//---- android/app/ContextImpl.java
public Object getSystemService(String name) {
        ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
        return fetcher == null ? null : fetcher.getService(this);
    }

可以发现调用getSystemService的过程是在SYSTEM_SERVICE_MAP的表中查找ServiceFetcher,并返回ServiceFetcher中的mCachedInstance。那么只需要将mCachedInstance替换为自定义的BootInflater并在BootInflater中完成Resource的Overrirde就可以了,如下图所示。

图片 16

修改Resource

我们知道Activity中的通过调用getResources()方法来访问资源,这实际上是调用ContextWrapper类中的getResource()方法

public Resources getResources(){
         return mBase.getResources();
}

LayoutCast中就采用替换mBase为自定义的OverrideContext,并在其中将Resource返回为修改后的Resource。

Instant Run

Instant Run
对资源文件的处理和LayoutCast基本类似,但是在细节的处理上有所不同,比如Instant
Run
通过对ActivityThread类中的mPackagesmResourcePackages的修改来改变LoadedApkmResDir的值。

for (String fieldName : new String[] { "mPackages", "mResourcePackages" })
{
  Field field = activityThread.getDeclaredField(fieldName);
  field.setAccessible(true);
  Object value = field.get(currentActivityThread);
  for (Map.Entry<String, WeakReference<?>> entry : ((Map)value).entrySet())
  {
    Object loadedApk = ((WeakReference)entry.getValue()).get();
    if (loadedApk != null) {
      if (mApplication.get(loadedApk) == bootstrap)
      {
        if (externalResourceFile != null) {
          mResDir.set(loadedApk, externalResourceFile);
        }
        if ((realApplication != null) && (mLoadedApk != null)) {
          mLoadedApk.set(realApplication, loadedApk);
        }
      }
    }
  }
}

资源文件修改的处理相对于Java文件的处理较为复杂,这中间涉及到aapt、attribute唯一性
、ID值一致等问题都增加了资源文件处理的难度。

总结

总的来说,每种方法都有自己的特色,BUCK依赖于自己强大的缓存和依赖管理系统。而LayoutCast和Instant
Run相对而言采用了更灵巧的方法。相对而言,Instant Run
凭借着天然的优势(和升级后的gradle结合),可以胜LayoutCast一筹,但是LayoutCast这种想法的提出还是很赞的。目前增量的编译集中在Java文件的修改,对于Res的修改暂时好像还不支持,这在后续应该会有提升吧。

发表评论

电子邮件地址不会被公开。 必填项已用*标注