Nuwa学习笔记

https://github.com/jasonross/Nuwa

Nuwa is a goddess in ancient Chinese mythology best known for repairing the pillar of heaven. 女娲是中国古代神话女神以补天而文明。

With this Nuwa project,you can also have the repairing power, fix your android applicaiton without have to publish a new APK to the appstore. 简单描述:其实就是Android热修复(ps:这里不做热修复的比对,纯粹的学习笔记) 使用方法,github上描述的很清楚

详细请参考Nuwa  懒得看英文的童鞋请看这里基于Nuwa实现Android自动化HotFix

一.原理分析 首先我来先需要了解以下大Google的分包方案,multidex 不了解的童鞋请移步MultiDex安装过程源码分析 multidex安装过程总结

将/data/app/apkName.apk路径下解压得到的classes2.dex, …, classesN.dex,依次写入到/data/data/pkgName/code_cache/secondary-dexes/apkName.apk.classes2.zip等zip文件的classes.dex中,并返回这个zip列表。然后针对这个zip列表执行安装过程,具体过程是,将这个要安装的zip列表加入BaseDexClassLoader的pathList实例的dexElements数组中,其中会针对各dex文件进行dex2opt优化。一旦加入到了dexElements数组中,程序启动的时候,ClassLoader会加载dexElements数组中的元素,从而实现multi dex的安装。

其实简要的概括就是把多个dex文件塞入到app的classloader之中,但是android dex拆包方案中的类是没有重复的,如果classes.dex和classes1.dex中有重复的类,当用到这个重复的类的时候,系统会选择哪个类进行加载呢? 让我们来看看类加载的代码:

public Class findClass(String name, List<Throwable> suppressed) {  
    for (Element element : dexElements) {  //每个Element就是一个dex文件
        DexFile dex = element.dexFile; 
                 if (dex != null) { 
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); 
            if (clazz != null) { 
                return clazz;
                            }
               }
   }               
 if (dexElementsSuppressedExceptions != null) {      
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    } 
    return null;
}

一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。 理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图:

在此基础上,我们构想了热补丁的方案,把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex插入到Elements的最前面,如下图

1.动态加载补丁dex,并将补丁dex插入到dexElements最前面 2.要实现热更新,需要热更新的类要防止被打上ISPREVERIFIED标记,关于这个标记,请阅读QQ空间的解决方案

二源码探究 对于第一点,通过DexClassLoader对象,将补丁dex对象加载进来,再通过反射将补丁dex插入到dexElements最前面即可。(可以照搬multidex源码) 而对于第二点,关键就是如何防止类被打上ISPREVERIFIED这个标记。

问题产生: 试验一下,修改某个类,然后打包成dex,插入到classloader,当加载类的时候出现了(本例中是ActivityManager要被替换):

为什么会出现以上问题呢? 从log的意思上来讲,ModuleManager引用了ActivityManager,但是发现这这两个类所在的dex不在一起,其中: 1. ModuleManager在classes.dex中 2. ActivityManager在patch.dex中 结果发生了错误。 这里有个问题,拆分dex的很多类都不是在同一个dex内的,怎么没有问题? 让我们搜索一下抛出错误的代码所在,嘿咻嘿咻,找到了一下代码:

从代码上来看,如果两个相关联的类在不同的dex中就会报错,但是拆分dex没有报错这是为什么,原来这个校验的前提是:

如果引用者(也就是ModuleManager)这个类被打上了CLASS_ISPREVERIFIED标志 ,那么就会进行dex的校验。那么这个标志是什么时候被打上去的? 让我们在继续搜索一下代码,嘿咻嘿咻~~,在DexPrepare.cpp找到了一下代码:

这段代码是dex转化成odex(dexopt)的代码中的一段,我们知道当一个apk在安装的时候,apk中的classes.dex会被虚拟机(dexopt)优化成odex文件,然后才会拿去执行. 虚拟机在启动的时候,会有许多的启动参数,其中一项就是verify选项,当verify选项被打开的时候,上面doVerify变量为true,那么就会执行dvmVerifyClass进行类的校验,如果dvmVerifyClass校验类成功,那么这个类会被打上CLASS_ISPREVERIFIED的标志,那么具体的校验过程是什么样子的呢? 此代码在DexVerify.cpp中,如下:

  1. 验证clazz->directMethods方法,directMethods包含了以下方法:
  2. static方法
  3. private方法
  4. 构造函数
  5. clazz->virtualMethods
  6. 虚函数=override方法?

概括一下就是如果以上方法中直接引用到的类(第一层级关系,不会进行递归搜索)和clazz都在同一个dex中的话,那么这个类就会被打上CLASS_ISPREVERIFIED标志

所以为了实现补丁方案,所以必须从这些方法中入手,防止类被打上CLASS_ISPREVERIFIED标志 。 最终空间的方案是往所有类的构造函数里面插入了一段代码,代码如下:

if (ClassVerifier.PREVENT_VERIFY) { System.out.println(AntilazyLoad.class); }

其中AntilazyLoad类会被打包成单独的hack.dex,这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了。只要没被打上这个标志的类都可以进行打补丁操作。 然后在应用启动的时候加载进来.AntilazyLoad类所在的dex包必须被先加载进来,不然AntilazyLoad类会被标记为不存在,即使后续加载了hack.dex包,那么他也是不存在的,这样屏幕就会出现茫茫多的类AntilazyLoad找不到的log。 所以Application作为应用的入口不能插入这段代码。(因为载入hack.dex的代码是在Application中onCreate中执行的,如果在Application的构造函数里面插入了这段代码,那么就是在hack.dex加载之前就使用该类,该类一次找不到,会被永远的打上找不到的标志)。 其中: class ClassVerifier { public static boolean PREVENT_VERIFY = false;//false防止代码被执行,提高性能 } 之所以选择构造函数是因为他不增加方法数,一个类即使没有显式的构造函数,也会有一个隐式的默认构造函数。 空间使用的是在字节码插入代码,而不是源代码插入,使用的是javaassist库来进行字节码插入的。 隐 患

虚拟机在安装期间为类打上CLASS_ISPREVERIFIED标志是为了提高性能的,我们强制防止类被打上标志是否会影响性能?这里我们会做一下更加详细的性能测试。 但是在大项目中拆分dex的问题已经比较严重,很多类都没有被打上这个标志。

如何打包补丁包:

1.空间在正式版本发布的时候,会生成一份缓存文件,里面记录了所有class文件的md5。还有一份mapping混淆文件。 2.在后续的版本中使用-applymapping选项,应用正式版本的mapping文件,然后计算编译完成后的class文件的md5和正式版本进行比较,把不相同的class文件打包成补丁包。 备注:该方案现在也应用到我们的编译过程当中,编译不需要重新打包dex,只需要把修改过的类的class文件打包成patch dex,然后放到sdcard下,那么就会让改变的代码生效。

原文发布于微信公众号 - Android历练记(gh_db8538619cdd)

原文发表时间:2016-10-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏博客园迁移

Java并发问题--乐观锁与悲观锁以及乐观锁的一种实现方式-CAS

  悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库...

1232
来自专栏JAVA高级架构

桥接模式(Bridge)

892
来自专栏SDNLAB

SDN开发笔记(七):L2switch源码分析(上)

前言 一般按照odl官方文档或者wiki安装L2switch组件会采用在karaf控制台上输入feature:install odl-l2switch-all命...

4118
来自专栏抠抠空间

configparser模块

模块简介 该模块适用于配置文件的格式与windows ini文件类似,可以包含一个或多个节(section),每个节可以有多个参数(键=值)。 创建文件 来看...

2876
来自专栏深度学习计算机视觉

ubuntu下curses中文输出乱码问题解决

在写Linux小游戏的时候,遇到了curses中文输出乱码,通过查阅资料和实践证明,找到了解决办法 第一步:检查头文件 将<curses.h>改成<ncurse...

2955
来自专栏salesforce零基础学习

salesforce 零基础开发入门学习(五)异步进程介绍与数据批处理Batchable

本篇知识参考:https://developer.salesforce.com/trailhead/force_com_dev_intermediate/asy...

3717
来自专栏python3

python3--基础总练习题

3、利用 python 打印前一天的本地时间,格式为‘2018-01-30’(面试题)

4923
来自专栏友弟技术工作室

beego路由配置路由设置

web框架中,路由是重要的一环,对于beego的路由配置如何? 让我们从入口文件先分析起来吧:

4811
来自专栏闵开慧

java中线程安全

关于线程安全,是指当多个线程访问同一个变量时,该变量不会因为多线程访问产生意想不到的问题,为了避免多线程访问的不可预知的问题,对于程序中多线程能访问到的变量要加...

3387
来自专栏一个爱瞎折腾的程序猿

MSMQ队列学习记录

使用MessageQueue类操作MSMQ,其在System.Messaging命名空间下,需要添加引用

1682

扫码关注云+社区

领取腾讯云代金券