Android热修复Tinker原理分析

目录

1、tinker的class文件修复
2、tinker的资源文件修复
3、几种热修复方案对比

1、tinker的class文件修复

1.1、先说dex文件的加载和类的查找过程

1.1.1、dex文件的加载过程

Java层通过我们会通过创建一个DexClassLoader来加载我们的dex,下面就以此为切入点进行

dexClassLoader = new DexClassLoader(apkPath, getFilesDir().getAbsolutePath(), null, getClassLoader());

//查看DexClassLoader的构造方法。
public class DexClassLoader extends BaseDexClassLoader {
    // dexPath:是加载apk/dex/jar的路径
    // optimizedDirectory:是优化dex后得到的.odex文件的输出路径
    // libraryPath:是加载的时候需要用到的so库
    // parent:给DexClassLoader指定父加载器
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

//可以看到它调用的是父类的构造函数,所以直接来看BaseDexClassLoader的构造函数。
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String libraryPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}

//创建了一个DexPathList实例,下面来看看DexPathList的构造函数。
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions);
}

//它调用的是makeDexElements方法来创建一个Element数组来存放Element对象,每个Element对象包含一个DexFile对象。
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                         ArrayList<IOException> suppressedExceptions) {
    ArrayList<Element> elements = new ArrayList<Element>();
    /*
     * Open all files and load the (direct or contained) dex files
     * up front.
     */
    for (File file : files) {
        File zip = null;
        DexFile dex = null;
        String name = file.getName();

        // 如果是一个dex文件
        if (name.endsWith(DEX_SUFFIX)) {
            // Raw dex file (not inside a zip/jar).
            try {
                dex = loadDexFile(file, optimizedDirectory);
            } catch (IOException ex) {
                System.logE("Unable to load dex file: " + file, ex);
            }
        // 如果是一个apk或者jar或者zip文件
        } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
                || name.endsWith(ZIP_SUFFIX)) {
            zip = file;

            try {
                // 1、调用loadDexFile加载dex文件,得到一个DexFile对象
                    loadDexFile通过c++层native方法去加载dex文件
                dex = loadDexFile(file, optimizedDirectory);
            } catch (IOException suppressed) {
               
                suppressedExceptions.add(suppressed);
            }
        } else if (file.isDirectory()) {
            elements.add(new Element(file, true, null, null));
        } else {
            System.logW("Unknown file type for: " + file);
        }
        
        // 2、把DexFile对象封装到Element对象中,然后将Element对象加入Element数组
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    return elements.toArray(new Element[elements.size()]);
}

dex文件的加载流程:我们会使用DexClassLoader去加载dex文件,DexClassLoader会将这个任务委派给DexPathList中的makeDexElements方法,在makeDexElements中调用了native层的 c++方法去真正的加载dex文件,然后返回DexFile的对象,通过这个对象构建一个Element的对象,然后将这个Element添加到dexElements的数组中。

1.1.2、class文件的查找过程

//DexClassLoader间接调用父类findClass方法,findClass方法中调用DexPathList中的DexPathList方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList =
            new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);
        if (clazz == null) {
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

//看DexPathList中的findClass方法,可以看到它是遍历dexElements数组,到每个dex文件去寻找当前需要的类,找到之后直接返回不往下找了
    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;
    }

类的查找过程:DexClassLoader通过findClass去查找一个类,同样它也是委派给DexPathList的findClass去查找,在DexPathList的findClass中会去遍历我们上面创建的dexElements数组,然后在每个dex中去查找相应的类,找到之后就返回,不再向后查找。

1.2、Tinker中class修复过程

1.2.1、先看tinker-补丁包合成流程图
tinker-补丁包合成流程图.png

补丁包的合成流程:当tinker收到补丁包bug path后,它会开启一个service,和当前有问题的bug dex和成为一个新的fixed dex文件,然后置于tinker dex文件加载路径。

1.2.2、再看tinker-合成后的补丁包加载流程

tinker-合成后的补丁包加载流程.png

补丁包的加载流程:获取到fixed dex文件后,通过反射DexPathList中的dexElements数组,将fixed dex插入到dexElements数组的最前面。classloader在寻找bug class的时候,找到的就是最前面的dex文件中我们已修复的fixed class。

2、tinker的资源文件修复

2.1、Context.getResources()

我们访问资源的时候通过Context.getResources(),获取Resources对象,然后通过Resources对象就可以访问各种资源了。
Context.getResources()流程
Context.getResources()获取的ContextImpl中的mResources对象。

mResources对象的获取过程

mResources是在activitythread初始化的时候获取的,具体是通过
ResourcesManager.getInstance().getResources()去得到一个Resources。

这个方法的思想是这样的:在ResourcesManager中,所有的资源对象都被存储在ArrayMap中,首先根据当前的请求参数去查找资源,如果找到了就返回,否则就创建一个Resources对象返回,并且放到ArrayMap中。

为什么会有多个资源对象,因为不同分辨率、不同系统版本所对应的资源文件可以不同,比如折叠屏手机就有两个不同分辨率的屏幕对应,drawable-hdpi和drawable-xhdpi。

Resources对象的创建过程
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config)

 protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        AssetManager assets = new AssetManager();
        if (key.mResDir != null) {
            if (assets.addAssetPath(key.mResDir) == 0) {
                Log.e(TAG, "failed to add asset path " + key.mResDir);
                return null;
            }
        }

创建一个resource需要一个AssetManager, 然后通过AssetManager的addAssetPath去加载资源文件。
然后我们就可以通过resource访问各种资源文件了。

2.2、tinker的资源文件修复

 public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
   
final Field[] packagesFields;
        if (Build.VERSION.SDK_INT < 27) {
            packagesFields = new Field[]{packagesFiled, resourcePackagesFiled};
        } else {
            packagesFields = new Field[]{packagesFiled};
        }
//首先将activityThread中所有LoadApk中的resDir的值替换成新合成的资源文件路径
        for (Field field : packagesFields) {
            final Object value = field.get(currentActivityThread);

            for (Map.Entry<String, WeakReference<?>> entry
                    : ((Map<String, WeakReference<?>>) value).entrySet()) {
                final Object loadedApk = entry.getValue().get();
                if (loadedApk == null) {
                    continue;
                }
                final String resDirPath = (String) resDir.get(loadedApk);
                if (appInfo.sourceDir.equals(resDirPath)) {
                    resDir.set(loadedApk, externalResourceFile);
                }
            }
        }

// Create a new AssetManager instance 
 //创建一个新的AssetManager
        newAssetManager = (AssetManager) findConstructor(assets).newInstance();

// 并把资源补丁apk加载进新的 AssetManager 中
        if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
            throw new IllegalStateException("Could not create new AssetManager");
        }

//循环替换ResourcesManager中所有Resources对象的AssetManager。
        for (WeakReference<Resources> wr : references) {
            final Resources resources = wr.get();
            if (resources == null) {
                continue;
            }
            try {
        // 把原来 resources 的 mAssets 属性替换成新的 AssetManager 对象
                assetsFiled.set(resources, newAssetManager);
            } catch (Throwable ignore) {
            }

            clearPreloadTypedArrayIssue(resources);
    // 最后调用 updateConfiguration 方法来确保资源更新了
            resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
        }

1、首先将activityThread中所有LoadApk中的resDir的值替换成新合成的资源文件路径(获取Resources时,会以LoadApk中的resDir作为key去ResourcesManager中获取)
2、创建一个新的AssetManager,并把资源补丁apk加载进新的 AssetManager 中
3、将ResourcesManager中所有Resources对象中AssetManager替换成我们新建的AssetManager,那么所有的Resources对象获取到的都是新合成的资源文件。

3、几种热修复方案对比

Andfix、QQ控件热修复、Tinker。

3.1、Andfix

aandfix功能比较单一,只能修复方法,大致原理就是在运行时通过在native层去将bug方法替换成修复的方法。这样导致了两个问题:1、由于运行时,class已经加载,其field数值无法改变(要是能改变,通过这个class创建的对象就失效了),故其不能增加或者减少成员变量;2、因为是动态的,跳过了类的初始化,所以对于静态方法、静态成员变量、构造方法处理可能会有问题,另外增加类也是不可能的;3、由于其采用在native层去进行方法替换,不同厂商手机可能对native层代码做修改,故其兼容性可能较差。基于以上3个缺点,由于我们的app对兼容性要求较高,且可能对方法之外的地方做修改,所以Andfix是不满足我们的需求的。

andfix修复方法的前提是对bug方法修复之后使用注解进行标记(bug类名和bug方法名),然后将这个补丁文件记录在一个patch.mf的文件中。


Andfix bug method 修复流程.png

方法在虚拟机中叫ArtMethod结构体,它是Native层的。方法最终是转换为ArtMethod结构体被执行。

loadPatch()中对本地的patch进行了遍历,获取每个patch的信息,逐一进行fix(),其中参数classes为patch中配置文件Patch.MF的Patch-Classes字段对应的所有类,即为要修复的类。
fixClass()方法中进行的过程就是从需要修复的类中定位到需要修复的方法。
replaceMethod() 定位到需要修复的方法以后,进入AndFix进行方法的替换。

3.2、QQ空间的热修复方案

QQ空间的热修复方案和Tinker方案较为类似,都是通过操作dexElements数组替换有问题的class来实现的,不同的是Tinker时将差分包和有问题的dex文件合成一个新的 fix dex插入到dexElements数组前面的,而QQ空间的热修复方案是直接将差分包,插入到dexElements的前面。会导致引用类和直接被引用类不在同一个dex的错误:

dex文件转化成odex文件期间会经历两个阶段:pre_verified和optimize,pre_verified会判断一个类的直接引用类是否和它在同一个dex文件,如果是,这个类就会被打上CLASS_ISPREVERIFIED,在optimize会根据这个标记去对这个类做指令优化。但是在类加载的时候,也会去判断这个类和其直接引用类是否在同一个dex,如果不在则报错。
如果一个直接被引用类是我们修复的方法,按照qq空间的修复方案,它和其引用类就会处于不同的dex文件,那么类加载的时候就会报错。

dex文件在dalvik执行之前会转化成odex文件,在这个过程中会对dex中每个类做一个检验:如果引用类(A)的直接引用的类(B)和这个类(A)在同一个dex,这个引用类(A)会被打上CLASS_ISPREVERIFIED标记,在类加载的时候,就会去判断类(A)和被引用类是否在一个dex,不在,则报错。
如果这个被引用类(B)是我们修复的方法,它就会和其引用类(A)处于不同的dex文件,那么类加载的时候就会报错。

QQ空间的热修复方案的解决方法是,使用aop在每个类的构造函数,引用了一个特殊的类(C),在编译的时候将这个类打进一个单独dex文件,这样所有的类在dex转化成odex的过程中都不会被打上标记,这样就避免了上述错误的发生。

但是这样有一个问题,如果在dex转化成odex期间,不做pre_verified和optimize两步,那么这这两步将推迟到类加载的阶段,会拖慢类加载的速度。
另外采用javassist进行字节码操作速度慢,会拖慢编译速度。
故淘汰了QQ空间的热修复方案。

3.3、Tinker3.4、tinker的其他优点

1、Tinker的覆盖比较全面,对class、资源文件、so文件都支持,
2、另外它比较容易扩展,在修复的各个阶段都提供了listener,我们可以重写这个listener,去监听修复的各个阶段。

3.4、Tinker的缺点

它的问题在于
1、由于Tinker采用的将fix dex 插入到dexElements最前面的方式去修复bug,所有它需要重启,不能即时生效。
2、Tinker在下发path的时候需要启动一个service去将path和有问题的dex文件合成一个新的dex文件,这个过程是对性能影响较大的,但是合成只需要一次,后续就不需要了,这样的性能损耗还是可以接受的;

4、参考文章

4.1、Andfix原理:https://blog.csdn.net/qxs965266509/article/details/49816007
4.2、Andfix原理:http://w4lle.com/2016/03/03/Android%E7%83%AD%E8%A1%A5%E4%B8%81%E4%B9%8BAndFix%E5%8E%9F%E7%90%86%E8%A7%A3%E6%9E%90/index.html
4.3、QQ空间的热修复方案:https://cloud.tencent.com/developer/article/1004417
4.4、Tinker:http://w4lle.com/2016/12/16/tinker/
4.5、Tinker:https://www.jianshu.com/p/8edd8cd83423

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,406评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,976评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,302评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,366评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,372评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,457评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,872评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,521评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,717评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,523评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,590评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,299评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,859评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,883评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,127评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,760评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,290评论 2 342

推荐阅读更多精彩内容