热修复Tinker(三)dex文件合并源码分析

写在前面的话

<p>

前面的一篇文章有讲到补丁文件的加载,最后通过dexDiff合成并且校验然后push到/data/data/package_name/tinker/下,当我们再次启动App时候就会读取这些文件,然后完成热修复的功能,那么这一篇就是对于dex文件加载进行相关分析

dex文件加载流程

<p>

启动一个应用首先启动Application,所以一般很多初始化工作都是放在Application,我们来看一下tinker-sample-android的Application

public class SampleApplication extends TinkerApplication {

    public SampleApplication() {
        super(7, "tinker.sample.android.app.SampleApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);
    }

}

这里的SampleApplication继承于TinkerApplication,这里调用了super()方法,也就是初始化了TinkerApplication

TinkerApplication.class

    protected TinkerApplication(int tinkerFlags, String delegateClassName,
                                String loaderClassName, boolean tinkerLoadVerifyFlag) {
        this.tinkerFlags = tinkerFlags;
        this.delegateClassName = delegateClassName;
        this.loaderClassName = loaderClassName;
        this.tinkerLoadVerifyFlag = tinkerLoadVerifyFlag;

    }

这里四个参数含义分别为

  • tinker支持的类型,dex,so,library,还是全部都支持!
  • ApplicationLike的实现类,只能传递字符串,不能使用class.getName()
  • 这个类以及它使用的类都是不能被补丁修改的,并且我们需要将它们加到dex.loader[]中,一般默认就可以了
  • 是否进行md5校验插件,默认每次加载时我们并不会去校验tinker文件的Md5

接下来就看一下Application的attachBaseContext与onCreate事件,看一下Trinker在这里做了什么

attachBaseContext事件先于onCreate事件

TinkerApplication.class

 protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //为整个App捕获全局异常
        Thread.setDefaultUncaughtExceptionHandler(new TinkerUncaughtHandler(this));
        onBaseContextAttached(base);
    }

   private void onBaseContextAttached(Context base) {
        applicationStartElapsedTime = SystemClock.elapsedRealtime();
        applicationStartMillisTime = System.currentTimeMillis();
        loadTinker();
        ensureDelegate();
        try {
            Method method = ShareReflectUtil.findMethod(delegate, "onBaseContextAttached", Context.class);
            method.invoke(delegate, base);
        } catch (Throwable t) {
            throw new TinkerRuntimeException("onBaseContextAttached method not found", t);
        }
        //reset save mode
        if (useSafeMode) {
            String processName = ShareTinkerInternals.getProcessName(this);
            String preferName = ShareConstants.TINKER_OWN_PREFERENCE_CONFIG + processName;
            SharedPreferences sp = getSharedPreferences(preferName, Context.MODE_PRIVATE);
            sp.edit().putInt(ShareConstants.TINKER_SAFE_MODE_COUNT, 0).commit();
        }
    }

在onBaseContextAttached里面首先是loadTinker方法,这里我们先看后面ensureDelegate方法其实是根据初始化传入的delegateClassName也就是ApplicationLike的实现类生成可以用的实体对象,反射调用delegate同步Application的周期,所以在我们Application接收到onBaseContextAttached方法前,已经完成了前面的loadTinker方法

onCreate方法里面没有什么东西和onBaseContextAttached中有类似部分,不做介绍

接下来就可以看一下这里的loadTinker方法

TinkerApplication.class

  private void loadTinker() {
        //disable tinker, not need to install
        if (tinkerFlags == TINKER_DISABLE) {
            return;
        }
        tinkerResultIntent = new Intent();
        try {
            //reflect tinker loader, because loaderClass may be define by user!
            Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, getClassLoader());

            Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class, int.class, boolean.class);
            Constructor<?> constructor = tinkerLoadClass.getConstructor();
            tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this, tinkerFlags, tinkerLoadVerifyFlag);
        } catch (Throwable e) {
            //has exception, put exception error code
            ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);
            tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);
        }
    }

这里也是通过反射获取我们传递过来的loaderClassName对象,并调用其中的tryLoad方法,loaderClassName对象其实就是TinkerLoader对象,接下来看一下TinkerLoader中的tryLoad方法

TinkerLoader.class

    public Intent tryLoad(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag) {
        Intent resultIntent = new Intent();

        long begin = SystemClock.elapsedRealtime();
        tryLoadPatchFilesInternal(app, tinkerFlag, tinkerLoadVerifyFlag, resultIntent);
        long cost = SystemClock.elapsedRealtime() - begin;
        ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
        return resultIntent;
    }

这里调用了tryLoadPatchFilesInternal方法

TinkerLoader.class

    private void tryLoadPatchFilesInternal(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag, Intent resultIntent) {
        
        ...

        final boolean isEnabledForDex = ShareTinkerInternals.isTinkerEnabledForDex(tinkerFlag);

        if (isEnabledForDex) {
            //tinker/patch.info/patch-641e634c/dex
            boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
            if (!dexCheck) {
                //file not found, do not load patch
                Log.w(TAG, "tryLoadPatchFiles:dex check fail");
                return;
            }
        }

        final boolean isEnabledForNativeLib = ShareTinkerInternals.isTinkerEnabledForNativeLib(tinkerFlag);

        if (isEnabledForNativeLib) {
            //tinker/patch.info/patch-641e634c/lib
            boolean libCheck = TinkerSoLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
            if (!libCheck) {
                //file not found, do not load patch
                Log.w(TAG, "tryLoadPatchFiles:native lib check fail");
                return;
            }
        }

        //check resource
        final boolean isEnabledForResource = ShareTinkerInternals.isTinkerEnabledForResource(tinkerFlag);
        Log.w(TAG, "tryLoadPatchFiles:isEnabledForResource:" + isEnabledForResource);
        if (isEnabledForResource) {
            boolean resourceCheck = TinkerResourceLoader.checkComplete(app, patchVersionDirectory, securityCheck, resultIntent);
            if (!resourceCheck) {
                //file not found, do not load patch
                Log.w(TAG, "tryLoadPatchFiles:resource check fail");
                return;
            }
        }
        //only work for art platform oat
        boolean isSystemOTA = ShareTinkerInternals.isVmArt() && ShareTinkerInternals.isSystemOTA(patchInfo.fingerPrint);

        //we should first try rewrite patch info file, if there is a error, we can't load jar
        if (isSystemOTA
            || (mainProcess && versionChanged)) {
            patchInfo.oldVersion = version;
            //update old version to new
            if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile)) {
                ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_REWRITE_PATCH_INFO_FAIL);
                Log.w(TAG, "tryLoadPatchFiles:onReWritePatchInfoCorrupted");
                return;
            }
        }
        if (!checkSafeModeCount(app)) {
            resultIntent.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, new TinkerRuntimeException("checkSafeModeCount fail"));
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_UNCAUGHT_EXCEPTION);
            Log.w(TAG, "tryLoadPatchFiles:checkSafeModeCount fail");
            return;
        }
        //now we can load patch jar
        if (isEnabledForDex) {
            boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent, isSystemOTA);
            if (!loadTinkerJars) {
                Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
                return;
            }
        }

        //now we can load patch resource
        if (isEnabledForResource) {
            boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent);
            if (!loadTinkerResources) {
                Log.w(TAG, "tryLoadPatchFiles:onPatchLoadResourcesFail");
                return;
            }
        }
        //all is ok!
        ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_OK);
        Log.i(TAG, "tryLoadPatchFiles: load end, ok!");
        return;
    }

这里很多代码我省略了,并没有贴出来,因为大多数都是做的判断空操作等等

接下来根据开发者配置的Tinker可补丁类型判断是否可以加载dex,res,so。然后分别分发给TinkerDexLoader、TinkerSoLoader、TinkerResourceLoader分别进行校验是否符合加载条件进而进行加载。

我这里仅仅分析一个关于dex的加载了,其他的加载就自行分析咯

对于dex加载主要是下面的方法了

TinkerLoader.class

    if (isEnabledForDex) {
        //tinker/patch.info/patch-641e634c/dex
        boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
        if (!dexCheck) {
            //file not found, do not load patch
            Log.w(TAG, "tryLoadPatchFiles:dex check fail");
            return;
        }
    }
    if (isEnabledForDex) {
        boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent, isSystemOTA);
        if (!loadTinkerJars) {
            Log.w(TAG, "tryLoadPatchFiles:onPatchLoadDexesFail");
            return;
        }
    }

这里的checkComplete校验dex_meta.xml文件中记载的dex补丁文件和经过opt优化过的文件是否存在,
而真正的加载则在loadTinkerJars方法内

TinkerDexLoader.java

public static boolean loadTinkerJars(Application application, boolean tinkerLoadVerifyFlag, String directory, Intent intentResult, boolean isSystemOTA) {
    if (dexList.isEmpty()) {
        Log.w(TAG, "there is no dex to load");
        return true;
    }

    PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();
    if (classLoader != null) {
        Log.i(TAG, "classloader: " + classLoader.toString());
    } else {
        Log.e(TAG, "classloader is null");
        ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_CLASSLOADER_NULL);
        return false;
    }
    String dexPath = directory + "/" + DEX_PATH + "/";
    File optimizeDir = new File(directory + "/" + DEX_OPTIMIZE_PATH);
//        Log.i(TAG, "loadTinkerJars: dex path: " + dexPath);
//        Log.i(TAG, "loadTinkerJars: opt path: " + optimizeDir.getAbsolutePath());

    ArrayList<File> legalFiles = new ArrayList<>();

    final boolean isArtPlatForm = ShareTinkerInternals.isVmArt();
    for (ShareDexDiffPatchInfo info : dexList) {
        //for dalvik, ignore art support dex
        if (isJustArtSupportDex(info)) {
            continue;
        }
        String path = dexPath + info.realName;
        File file = new File(path);

        if (tinkerLoadVerifyFlag) {
           ...
        }
        legalFiles.add(file);
    }

    if (isSystemOTA) {
        ....
    }
    try {
        SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);
    } catch (Throwable e) {
        Log.e(TAG, "install dexes failed");
//            e.printStackTrace();
        intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
        ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
        return false;
    }

    return true;
}

这里会根据传过来的tinkerLoadVerifyFlag选项控制是否每次加载都要验证dex的md5值,默认也是false,后面还有一个关于是否是OTA的判断,这个我也不太清楚是干嘛的。。。

接下来就是调用SystemClassLoaderAdder的installDexes方法

SystemClassLoaderAdder.java

 public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
        throws Throwable {

        if (!files.isEmpty()) {
            ClassLoader classLoader = loader;
            if (Build.VERSION.SDK_INT >= 24) {
                classLoader = AndroidNClassLoader.inject(loader, application);
            }
            //because in dalvik, if inner class is not the same classloader with it wrapper class.
            //it won't fail at dex2opt
            if (Build.VERSION.SDK_INT >= 23) {
                V23.install(classLoader, files, dexOptDir);
            } else if (Build.VERSION.SDK_INT >= 19) {
                V19.install(classLoader, files, dexOptDir);
            } else if (Build.VERSION.SDK_INT >= 14) {
                V14.install(classLoader, files, dexOptDir);
            } else {
                V4.install(classLoader, files, dexOptDir);
            }
            //install done
            sPatchDexCount = files.size();
            Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);

            if (!checkDexInstall(classLoader)) {
                //reset patch dex
                SystemClassLoaderAdder.uninstallPatchDex(classLoader);
                throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
            }
        }
    }

这里区分了不同版本分别是SDK版本14(Android4.0)以下,14(Android4.0)到19(Android4.4),19(Android4.4)到23(Android6.0)与23(Android6.0)以上

我这里就选取其中一个进行分析了,以14(Android4.0)到19(Android4.4)为例

SystemClassLoaderAdder.java

private static final class V14 {

    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.
         */
        Field pathListField = ShareReflectUtil.findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
            new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
    }

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

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

首先我们看install方法,这里通过反射拿到ClassLoader中的pathList变量,接下来调用了expandFieldArray方法,这个方法有三个参数,第一个为pathList,第二个是一个类型为String为的"dexElements"参数,第三个参数则调用了 makeDexElements方法。

接下来我们看下这里的makeDexElements方法,则是反射出了pathList的makeDexElements方法,并且运行这个方法,传入的是插件补丁dexList路径与优化过的opt目录,通过这个方法生成一个新的DexElements,这个DexElements为插件的DexElements。

我们继续看上面的expandFieldArray方法

    public 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);

        // NOTE: changed to copy extraElements first, for patch load first

        System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
        System.arraycopy(original, 0, combined, extraElements.length, original.length);

        jlrField.set(instance, combined);
    }

这里首先找到pathList的原始oldDexElements,然后生成一个新的数组combined,长度是oldDexElements.length + newDexElements.length。然后将newDexElements拷贝到combined的前面,将oldDexElements拷贝的combined的剩余位置,我们称之为dex前置。

然后通过反射获取pathList的dexElements参数,并把我们合并的DexElements设置为pathList的dexElements。

到这里就完成了dex文件的合并了,至于其他几个版本的合并方式,我就不一一做说明了,有兴趣的同学可以自己去看一下是如何合并的。

这样下次运行App后,当patch.dex中包含修复的Class时就会优先加载,在后续的DEX中遇到Class的话就会直接返回而不去加载,这样就达到了修复的目的。

到这里整个Tinker的源码流程就结束了,

写在后面的话

<p>

对于热修复来说Tinker只是众多方案中的一种,方案其实也是与QQ空间超级补丁技术相同,但是微信做了更多优化,对开发者透明,也不需要对包进行额外处理,同时兼容性和稳定性比较高,并且经过很多网友的实际项目的实践还是值得去接入的,后面有时间我还会对其他方案的热修复进行分析,peace~~~

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

推荐阅读更多精彩内容