Tinker Dex热修复源码分析

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

0x10 应用接入Tinker

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

0x20 Tinker工作原理

Tinker Dex热修复源码分析_第1张图片

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生成补丁

    Tinker Dex热修复源码分析_第2张图片

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

0x22 tinker在应用内的安装

SampleApplication开始分析。

Tinker Dex热修复源码分析_第3张图片

首先看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开始分析。

Tinker Dex热修复源码分析_第4张图片

我们从点击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 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 Dex热修复源码分析_第5张图片

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 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 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 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 suppressedExceptions = new ArrayList();
            ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
                new ArrayList(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 小结

Tinker Dex热修复源码分析_第6张图片
Tinker Dex热修复源码分析_第7张图片

你可能感兴趣的:(Tinker Dex热修复源码分析)