Tinker Dex热修复源码分析

Tencent Tinker Hotfix开源方案https://github.com/Tencent/tinker

0x10 应用接入Tinker

请参考Tinker官方指导或者Tinker Github

0x20 Tinker工作原理

0x21 生成patch dex

  • 运行assembleRelease生成base apk
  • 修改base apk的代码
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Tinker.MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.e(TAG, "i am on onCreate classloader:" + MainActivity.class.getClassLoader().toString());
        //test resource change
        // base apk代码
        //Log.e(TAG, "i am on onCreate string:" + getResources().getString(R.string.test_resource));
        // patch apk代码
        Log.e(TAG, "i am on patch onCreate");
        ...
    }
    ...
}
  • 修改Gradle脚本声明base apk
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/app-release-1123-20-23-05.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/app-release-1123-20-23-05-mapping.txt"
    //resource R.txt to build patch apk, must input if there is resource changed
    tinkerApplyResourcePath = "${bakPath}/app-release-1123-20-23-05-R.txt"
  • 运行tinkerPatchRelease生成补丁

  • 对于生成patch dex文件的原理,由于水平所限,暂不讨论

0x22 tinker在应用内的安装

SampleApplication开始分析。

首先看TinkerLoadertryLoad()方法,tryLoad()方法用于加载patch后的classes.dex文件。

    public Intent tryLoad(TinkerApplication app) {
        Intent resultIntent = new Intent();

        long begin = SystemClock.elapsedRealtime();
        // 实际调用tryLoadPatchFilesInternal
        tryLoadPatchFilesInternal(app, resultIntent);
        long cost = SystemClock.elapsedRealtime() - begin;
        ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
        return resultIntent;
    }

    private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
        final int tinkerFlag = app.getTinkerFlags();
        // 首先检查是否可以加载,不满足直接返回
        if (!ShareTinkerInternals.isTinkerEnabled(tinkerFlag)) {
            Log.w(TAG, "tryLoadPatchFiles: tinker is disable, just return");
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
            return;
        }
        if (ShareTinkerInternals.isInPatchProcess(app)) {
            Log.w(TAG, "tryLoadPatchFiles: we don't load patch with :patch process itself, just return");
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
            return;

        }
        //tinker
        File patchDirectoryFile = SharePatchFileUtil.getPatchDirectory(app);
        if (patchDirectoryFile == null) {
            Log.w(TAG, "tryLoadPatchFiles:getPatchDirectory == null");
            //treat as not exist
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
            return;
        }
        ......
    }

tinker的安装初始化阶段,我们假定patch dex还没有生成,这样加载失败直接返回。后面分析patch dex生成后,加载patch后的classes.dex的过程,也就是hotfix如何生效的过程。
接下来是tinker的安装过程,暂不讨论。

0x22 合并patch dex生成oat文件

以tinker-sample-android Demo为例

我们从点击load patch开始分析。

我们从点击loadPatchButton开始分析。

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.e(TAG, "i am on onCreate classloader:" + MainActivity.class.getClassLoader().toString());
        //test resource change
        Log.e(TAG, "i am on onCreate string:" + getResources().getString(R.string.test_resource));
//        Log.e(TAG, "i am on patch onCreate");

        Button loadPatchButton = (Button) findViewById(R.id.loadPatch);

        loadPatchButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 加载Patch Dex文件/sdcard/patch_signed_7zip.apk
                TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
            }
        });
        ......
    }

继续看onReceiveUpgradePatch()的实现。

    public static void onReceiveUpgradePatch(Context context, String patchLocation) {
       Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
   }

getPatchListener()返回SamplePatchListener对象,SamplePatchListener继承自DefaultPatchListener,下面看onPatchReceived()的实现。

   public int onPatchReceived(String path) {
       File patchFile = new File(path);
       // Patch检查
       int returnCode = patchCheck(path, SharePatchFileUtil.getMD5(patchFile));

       if (returnCode == ShareConstants.ERROR_PATCH_OK) {
           // 运行PatchService
           TinkerPatchService.runPatchService(context, path);
       } else {
           Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
       }
       return returnCode;
   }

TinkerPatchService负责合并patch dex与原始apk的classes.dex以及dex优化,运行在独立的进程中。TinkerPatchService继承自IntentService,这样最终在onHandleIntent()中处理服务请求。

   protected void onHandleIntent(Intent intent) {
       final Context context = getApplicationContext();
       Tinker tinker = Tinker.with(context);
       tinker.getPatchReporter().onPatchServiceStart(intent);
       ......
       String path = getPatchPathExtra(intent);
       if (path == null) {
           TinkerLog.e(TAG, "TinkerPatchService can't get the path extra, ignoring.");
           return;
       }
       // patch文件
       File patchFile = new File(path);
       ......
       // 前台服务
       increasingPriority();
       PatchResult patchResult = new PatchResult();
       try {
           if (upgradePatchProcessor == null) {
               throw new TinkerRuntimeException("upgradePatchProcessor is null.");
           }
           // 尝试安装patch
           result = upgradePatchProcessor.tryPatch(context, path, patchResult);
       } catch (Throwable throwable) {
           e = throwable;
           result = false;
           tinker.getPatchReporter().onPatchException(patchFile, e);
       }
       ......
   }        

upgradePatchProcessorUpgradePatch类的对象,下面看trypatch()的实现。

   public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
       Tinker manager = Tinker.with(context);

       final File patchFile = new File(tempPatchPath);
       ......
       if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
           TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
           return false;
       }
       ......
   }         

tryRecoverDexFiles()实现了dex文件的合并与oat文件的生成,本文只讨论oat文件的生成过程。下面直接看dexOptimizeDexFiles()方法。

   private static boolean dexOptimizeDexFiles(Context context, List<File> dexFiles, String optimizeDexDirectory, final File patchFile) {
       // 参数dexFiles表示需要优化的dex文件(patch合成后)列表
       // optimizeDexDirectory为oat文件的存放路径
       final Tinker manager = Tinker.with(context);

       optFiles.clear();

       if (dexFiles != null) {
           File optimizeDexDirectoryFile = new File(optimizeDexDirectory);
           ......
           // add opt files
           for (File file : dexFiles) {
               String outputPathName = SharePatchFileUtil.optimizedPathFor(file, optimizeDexDirectoryFile);
               optFiles.add(new File(outputPathName));
           }
           ......
           // try parallel dex optimizer
           // 串行优化多个dex文件
           TinkerDexOptimizer.optimizeAll(
                   dexFiles, optimizeDexDirectoryFile,
               new TinkerDexOptimizer.ResultCallback() {
                   long startTime;

                   @Override
                   public void onStart(File dexFile, File optimizedDir) {
                       startTime = System.currentTimeMillis();
                       TinkerLog.i(TAG, "start to parallel optimize dex %s, size: %d", dexFile.getPath(), dexFile.length());
                   }

                   @Override
                   public void onSuccess(File dexFile, File optimizedDir, File optimizedFile) {
                       // Do nothing.
                       TinkerLog.i(TAG, "success to parallel optimize dex %s, opt file:%s, opt file size: %d, use time %d",
                           dexFile.getPath(), optimizedFile.getPath(), optimizedFile.length(), (System.currentTimeMillis() - startTime));
                   }

                   @Override
                   public void onFailed(File dexFile, File optimizedDir, Throwable thr) {
                       TinkerLog.i(TAG, "fail to parallel optimize dex %s use time %d",
                           dexFile.getPath(), (System.currentTimeMillis() - startTime));
                       failOptDexFile.add(dexFile);
                       throwable[0] = thr;
                   }
               }
           );
           .......
       }
   }                        

optimizeAll()方法对多个dex文件排序后,调用OptimizeWorkerrun()方法优化dex。下面看run()方法的实现。

        public boolean run() {
            try {
                if (!SharePatchFileUtil.isLegalFile(dexFile)) {
                    if (callback != null) {
                        callback.onFailed(dexFile, optimizedDir,
                            new IOException("dex file " + dexFile.getAbsolutePath() + " is not exist!"));
                        return false;
                    }
                }
                if (callback != null) {
                    callback.onStart(dexFile, optimizedDir);
                }
                String optimizedPath = SharePatchFileUtil.optimizedPathFor(this.dexFile, this.optimizedDir);
                if (useInterpretMode) {
                    interpretDex2Oat(dexFile.getAbsolutePath(), optimizedPath);
                } else {
                    DexFile.loadDex(dexFile.getAbsolutePath(), optimizedPath, 0);
                }
                if (callback != null) {
                    callback.onSuccess(dexFile, optimizedDir, new File(optimizedPath));
                }
            } catch (final Throwable e) {
                Log.e(TAG, "Failed to optimize dex: " + dexFile.getAbsolutePath(), e);
                if (callback != null) {
                    callback.onFailed(dexFile, optimizedDir, e);
                    return false;
                }
            }
            return true;
        }

run()方法中,如果采用interpret-only编译dex,直接使用Android提供的dex2oat命令生成oat文件;否则使用DexFile.loadDex()方法生成oat文件。interpertDex2Oat()以及DexFile.loadDex()均会等待dex2oat进程结束才会返回。

Tinker之前采用并行优化dex文件,多个dex2oat进程同时编译,系统负载太高,可能会引发ANR等问题,现在代码中已是串行。另外,对于不紧急的patch,是否可以使用JobScheduler当系统空闲时来优化dex文件。DexFile.loadDex()最终也是通过dex2oat来生成oat文件,但默认的compiler-filter是speed。

0x23 Hotfix如何生效

tinker安装时,TinkerLoadertryLoad()会调用tryLoadPatchFilesInternal()尝试加载oat文件,我们从这里开始分析。

    private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
        final int tinkerFlag = app.getTinkerFlags();
        // 一系列检查
        if (!ShareTinkerInternals.isTinkerEnabled(tinkerFlag)) {
            Log.w(TAG, "tryLoadPatchFiles: tinker is disable, just return");
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
            return;
        }
        if (ShareTinkerInternals.isInPatchProcess(app)) {
            Log.w(TAG, "tryLoadPatchFiles: we don't load patch with :patch process itself, just return");
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_DISABLE);
            return;

        }
        //tinker
        File patchDirectoryFile = SharePatchFileUtil.getPatchDirectory(app);
        if (patchDirectoryFile == null) {
            Log.w(TAG, "tryLoadPatchFiles:getPatchDirectory == null");
            //treat as not exist
            ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_DIRECTORY_NOT_EXIST);
            return;
        }
        ......
        //now we can load patch jar
        // 满足条件加载oat文件
        if (isEnabledForDex) {
            boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA);
        ......
        }
        ......
    }

下面分析loadTinkerJars().

    public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA) {
        if (loadDexList.isEmpty() && classNDexInfo.isEmpty()) {
            Log.w(TAG, "there is no dex to load");
            return true;
        }
        ......
        ArrayList<File> legalFiles = new ArrayList<>();
        // 非classesN.dex的dex文件
        for (ShareDexDiffPatchInfo info : loadDexList) {
            //for dalvik, ignore art support dex
            if (isJustArtSupportDex(info)) {
                continue;
            }

            String path = dexPath + info.realName;
            File file = new File(path);

            if (application.isTinkerLoadVerifyFlag()) {
                long start = System.currentTimeMillis();
                String checkMd5 = getInfoMd5(info);
                if (!SharePatchFileUtil.verifyDexFileMd5(file, checkMd5)) {
                    //it is good to delete the mismatch file
                    ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH);
                    intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH,
                        file.getAbsolutePath());
                    return false;
                }
                Log.i(TAG, "verify dex file:" + file.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
            }
            legalFiles.add(file);
        }
        // verify merge classN.apk
        if (isVmArt && !classNDexInfo.isEmpty()) {
            File classNFile = new File(dexPath + ShareConstants.CLASS_N_APK_NAME);
            long start = System.currentTimeMillis();

            if (application.isTinkerLoadVerifyFlag()) {
                // classesN.dex文件,请参考multidex
                for (ShareDexDiffPatchInfo info : classNDexInfo) {
                    if (!SharePatchFileUtil.verifyDexFileMd5(classNFile, info.rawName, info.destMd5InArt)) {
                        ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_MD5_MISMATCH);
                        intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_MISMATCH_DEX_PATH,
                            classNFile.getAbsolutePath());
                        return false;
                    }
                }
            }
            Log.i(TAG, "verify dex file:" + classNFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));

            legalFiles.add(classNFile);
        }
        ......
        try {
            // 安装dex文件,合法的dex文件保存在参数legalFiles,optimizeDir为dex文件对应的oat文件路径
            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;
    }        

下面看installDexes(),它是patch dex生效的关键所在,这里只分析SDK >= 24的情况。

    public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)
        throws Throwable {
        Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());

        if (!files.isEmpty()) {
            files = createSortedAdditionalPathEntries(files);
            ClassLoader classLoader = loader;
            if (Build.VERSION.SDK_INT >= 24 && !checkIsProtectedApp(files)) {
                // 创建新的应用类加载器
                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) {
                // 修改类加载器的DexPathList
                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);
            }
        }
    }

下面首先看AndroidNClassLoaderinject()方法。

    public static AndroidNClassLoader inject(PathClassLoader originClassLoader, Application application) throws Exception {
        // originClassLoader为应用默认的类加载器
        // application为应用的Application对象
        AndroidNClassLoader classLoader = createAndroidNClassLoader(originClassLoader, application);
        // 把新的应用类加载器设置到LoadedApk以及Thread中
        reflectPackageInfoClassloader(application, classLoader);
        return classLoader;
    }

下面看createAndroidNClassLoader()的实现。

    private static AndroidNClassLoader createAndroidNClassLoader(PathClassLoader originalClassLoader, Application application) throws Exception {
        //let all element ""
        // 创建继承自PathClassLoader的应用类加载器
        final AndroidNClassLoader androidNClassLoader = new AndroidNClassLoader("",  originalClassLoader, application);
        final Field pathListField = ShareReflectUtil.findField(originalClassLoader, "pathList");
        final Object originPathList = pathListField.get(originalClassLoader);

        // To avoid 'dex file register with multiple classloader' exception on Android O, we must keep old
        // dexPathList in original classloader so that after the newly loaded base dex was bound to
        // AndroidNClassLoader we can still load class in base dex from original classloader.

        // 用原类加载器的pathList创建新的类加载器的pathList
        Object newPathList = recreateDexPathList(originPathList, androidNClassLoader);

        // Update new classloader's pathList.
        pathListField.set(androidNClassLoader, newPathList);

        return androidNClassLoader;
    }

下面看V23.install()的实现。

        private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                    File optimizedDirectory)
            throws IllegalArgumentException, IllegalAccessException,
            NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
            /* 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);
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
                new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
                suppressedExceptions));
            if (suppressedExceptions.size() > 0) {
                for (IOException e : suppressedExceptions) {
                    Log.w(TAG, "Exception in makePathElement", e);
                    throw e;
                }

            }
        }

install()主要负责加载新增的dex文件,以及将新增的dex文件与原来的dex文件列表合并起来。此后,应用的类加载工作由新类加载器接管。

如果dex文件没有优化安装,makePathElements会触发dex2oat.

0x24 小结

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

推荐阅读更多精彩内容