Maison  >  Article  >  Java  >  Développement Android : une brève introduction à la correction à chaud du code source Tinker

Développement Android : une brève introduction à la correction à chaud du code source Tinker

黄舟
黄舟original
2017-03-10 09:30:171887parcourir

0. Avant-propos

La technologie de réparation à chaud est fondamentalement devenue plus importante pour le module de projets Android. Principalement parce qu'après la mise en ligne d'un projet, il est inévitable qu'il y ait divers problèmes, et il est trop coûteux de compter sur les versions pour résoudre les problèmes.

La technologie actuelle de réparation à chaud comprend essentiellement le plan AndFix d'Alibaba, QZone, le plan idéologique proposé par Meituan et Tinker de Tencent, etc.

Parmi eux, AndFix est probablement le plus simple (et la commande Tinker La méthode d'accès aux lignes est similaire), mais AndFix présente certains problèmes de compatibilité, et la solution QZone a un impact négatif sur les performances Il y aura un certain impact , et le problème de confusion de mémoire se produit dans le mode Art L'idée. proposé par Meituan est principalement basé sur le principe de Instant Run n'est pas encore open source et a une bonne compatibilité.

Il semble que si vous choisissez une solution open source, Tinker soit actuellement le meilleur choix. Jetons un coup d'œil <.>Une analyse approximative des principes de Tinker.

1. Aperçu du principe

Bricolervaold.apk et new.apk ont fait diff et ont obtenu patch.dex puis faites-le avec le apk de la machine locale classes.dex Fusionner, génère un nouveau classes.dex, fusionnera le fusionné par réflexion pendant l'exécution dexFile est placé devant le tableau dexElements chargé.

Le principe de substitution à l'exécution est en fait similaire à la solution de Qzone, qui consiste à refléter la modification de dexElements. La différence entre est : Qzone insère directement patch.dex à l'avant du tableau et Tinker est le montant total de fusionné dex inséré devant le tableau . Parce qu'il y a un problème avec la solution CLASS_ISPREVERIFIED mentionnée dans la solution Qzone.

Le système ClassLoader d'Android utilise généralement PathClassLoader et DexClassLoader , tout le monde a juste besoin de comprendre que Android utilise PathClassLoader comme chargeur de classe , DexClassLoader peut charger en interne à partir de fichiers de type .jar et .apk classes.dex fichier ira bien. Pour charger une classe, donnez simplement un nom de classe, puis allez sur findClass, PathClassLoader et DexClassLoader héritent tous deux de BaseDexClassLoader. Il y a le code source suivant dans BaseDexClassLoader :

#BaseDexClassLoader
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    Class clazz = pathList.findClass(name);

    if (clazz == null) {
        throw new ClassNotFoundException(name);
    }

    return clazz;
}

#DexPathList
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;
}

#DexFile
public Class loadClassBinaryName(String name, ClassLoader loader) {
    return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);


comme vous pouvez le voir BaseDexClassLoader contient un objet pathList, et pathList contient un DexFile La collection dexElements, et pour le chargement de la classe consiste à parcourir cette collection et à trouver via DexFile.

En termes simples, un ClassLoader peut contenir plusieurs fichiers dex, chacun A Le fichier dex est un élément, et plusieurs fichiers dex sont organisés en un seul. Tableau ordonné dexElements, lors de la recherche d'une classe, il parcourra les fichiers dex dans l'ordre, puis repartira de celui actuellement parcouru dex pour trouver la classe. Si la classe est trouvée, elle sera renvoyée. Si elle n'est pas trouvée, continuez la recherche à partir du dex fichier. Dans ce cas, nous pouvons placer notre patch.jar dans le premier élément de ce tableau, qui contient la classe réparée. Dans ce cas, lorsque Lors du parcours de findClass, la classe que nous avons corrigée sera trouvée, remplaçant ainsi la classe buggée .

Deux lignes d'analyse du code source Tinker dans cet article :

(

1) Lorsque l'application démarre, chargez le classes.dex

(

2) fusionné à partir de la valeur par défaut. répertoire Une fois le patch émis, synthétisez classes.dex dans le répertoire cible

2Analyse du code source

2.1  加载patch

加载的代码实际上在生成的Application中调用的,其父类为TinkerApplication,在其attachBaseContext中辗转会调用到loadTinker()方法,在该方法内部,反射调用了TinkerLoadertryLoad方法继而调用了tryLoadPatchFilesInternal方法

@Override
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;
}
private void tryLoadPatchFilesInternal(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag, Intent resultIntent) {
    // 省略大量安全性校验代码

    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;
        }
    }

    //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;
        }
    }
}

其中TinkerDexLoader.checkComplete主要是用于检查下发的meta文件中记录的dex信息meta文件,可以查看生成patch的产物,在assets/dex-meta.txt),检查meta文件中记录的dex文件信息对应的dex文件是否存在,并把值存在TinkerDexLoader静态变量dexList中。

TinkerDexLoader.loadTinkerJars传入四个参数,分别为applicationtinkerLoadVerifyFlag(注解上声明的值,传入为false),patchVersionDirectory当前versionpatch文件夹,intent,当前patch是否仅适用于art

@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public static boolean loadTinkerJars(Application application, boolean tinkerLoadVerifyFlag, 
    String directory, Intent intentResult, boolean isSystemOTA) {
        PathClassLoader classLoader = (PathClassLoader) TinkerDexLoader.class.getClassLoader();

        String dexPath = directory + "/" + DEX_PATH + "/";
        File optimizeDir = new File(directory + "/" + DEX_OPTIMIZE_PATH);

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

            legalFiles.add(file);
        }
        // just for art
        if (isSystemOTA) {
            parallelOTAResult = true;
            parallelOTAThrowable = null;
            Log.w(TAG, "systemOTA, try parallel oat dexes!!!!!");

            TinkerParallelDexOptimizer.optimizeAll(
                legalFiles, optimizeDir,
                new TinkerParallelDexOptimizer.ResultCallback() {
                }
            );

        SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles);
        return true;

找出仅支持artdex,且当前patch是否仅适用于art时,并行去loadDex关键是最后的installDexes

@SuppressLint("NewApi")
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&#39;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);
        }
    }
}

这里实际上就是根据不同的系统版本,去反射处理dexElements我们看一下V19的实现

private static final class V19 {
    private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
                                File optimizedDirectory)
        throws IllegalArgumentException, IllegalAccessException,
        NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {

        Field pathListField = ShareReflectUtil.findField(loader, "pathList");
        Object dexPathList = pathListField.get(loader);
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
            new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
            suppressedExceptions));
        if (suppressedExceptions.size() > 0) {
            for (IOException e : suppressedExceptions) {
                Log.w(TAG, "Exception in makeDexElement", e);
                throw e;
            }
        }
    }
}

1)找到PathClassLoaderBaseDexClassLoader)对象中的pathList对象

2)根据pathList对象找到其中的makeDexElements方法,传入patch相关的对应的实参,返回Element[]对象

3)拿到pathList对象中原本的dexElements方法

4)步骤2与步骤3中的Element[]数组进行合并,将patch相关的dex放在数组的前面

5)最后将合并后的数组,设置给pathList


2.2 合成patch

入口为onReceiveUpgradePatch()方法:

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
                Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed.apk");


上述代码会调用DefaultPatchListener中的onPatchReceived方法:

# DefaultPatchListener
@Override
public int onPatchReceived(String path) {

    int returnCode = patchCheck(path);

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


首先对Tinker的相关配置(isEnable)以及patch的合法性进行检测,如果合法,则调用TinkerPatchService.runPatchService(context, path)

public static void runPatchService(Context context, String path) {
    try {
        Intent intent = new Intent(context, TinkerPatchService.class);
        intent.putExtra(PATCH_PATH_EXTRA, path);
        intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());
        context.startService(intent);
    } catch (Throwable throwable) {
        TinkerLog.e(TAG, "start patch service fail, exception:" + throwable);
    }
}


TinkerPatchServiceIntentService的子类,这里通过intent设置了两个参数,一个是patch的路径,一个是resultServiceClass,该值是调用Tinker.install的时候设置的,默认为DefaultTinkerResultService.class。由于是IntentService,直接看onHandleIntent即可

@Override
protected void onHandleIntent(Intent intent) {
    final Context context = getApplicationContext();
    Tinker tinker = Tinker.with(context);
    String path = getPatchPathExtra(intent);
    File patchFile = new File(path);
    boolean result;
    increasingPriority();
    PatchResult patchResult = new PatchResult();
    result = upgradePatchProcessor.tryPatch(context, path, patchResult);
    patchResult.isSuccess = result;
    patchResult.rawPatchFilePath = path;
    patchResult.costTime = cost;
    patchResult.e = e;
    AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));
}


比较清晰,主要关注upgradePatchProcessor.tryPatch方法,调用的是UpgradePatch.tryPatch。这里有个有意思的地方increasingPriority(),其内部实现为:

private void increasingPriority() {
    TinkerLog.i(TAG, "try to increase patch process priority");
    try {
        Notification notification = new Notification();
        if (Build.VERSION.SDK_INT < 18) {
            startForeground(notificationId, notification);
        } else {
            startForeground(notificationId, notification);
            // start InnerService
            startService(new Intent(this, InnerService.class));
        }
    } catch (Throwable e) {
        TinkerLog.i(TAG, "try to increase patch process priority error:" + e);
    }
}


如果你对保活这个话题比较关注,那么对这段代码一定不陌生,主要是利用系统的一个漏洞来启动一个前台Service下面继续回到tryPatch方法:

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

    final File patchFile = new File(tempPatchPath);

    //it is a new patch, so we should not find a exist
    SharePatchInfo oldInfo = manager.getTinkerLoadResultIfPresent().patchInfo;
    String patchMd5 = SharePatchFileUtil.getMD5(patchFile);

    //use md5 as version
    patchResult.patchVersion = patchMd5;
    SharePatchInfo newInfo;

    //already have patch
    if (oldInfo != null) {
        newInfo = new SharePatchInfo(oldInfo.oldVersion, patchMd5, Build.FINGERPRINT);
    } else {
        newInfo = new SharePatchInfo("", patchMd5, Build.FINGERPRINT);
    }

    //check ok, we can real recover a new patch
    final String patchDirectory = manager.getPatchDirectory().getAbsolutePath();
    final String patchName = SharePatchFileUtil.getPatchVersionDirectory(patchMd5);
    final String patchVersionDirectory = patchDirectory + "/" + patchName;

    //copy file
    File destPatchFile = new File(patchVersionDirectory + "/" + SharePatchFileUtil.getPatchVersionFile(patchMd5));
    // check md5 first
    if (!patchMd5.equals(SharePatchFileUtil.getMD5(destPatchFile))) {
        SharePatchFileUtil.copyFileUsingStream(patchFile, destPatchFile);
    }

 //we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
    if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, 
                destPatchFile)) {
        TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
        return false;
    }
    return true;
}


拷贝patch文件拷贝至私有目录,然后调用DexDiffPatchInternal.tryRecoverDexFiles

protected static boolean tryRecoverDexFiles(Tinker manager, ShareSecurityCheck checker, Context context,
                                                
    String patchVersionDirectory, File patchFile) {
    String dexMeta = checker.getMetaContentMap().get(DEX_META_FILE);
    boolean result = patchDexExtractViaDexDiff(context, patchVersionDirectory, dexMeta, patchFile);
    return result;
}


直接看patchDexExtractViaDexDiff

private static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile) {
    String dir = patchVersionDirectory + "/" + DEX_PATH + "/";

    if (!extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)) {
        TinkerLog.w(TAG, "patch recover, extractDiffInternals fail");
        return false;
    }

    final Tinker manager = Tinker.with(context);

    File dexFiles = new File(dir);
    File[] files = dexFiles.listFiles();

    ...files遍历执行:DexFile.loadDex
     return true;
}


核心代码主要在extractDexDiffInternals中:

private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {
    //parse meta
    ArrayList<ShareDexDiffPatchInfo> patchList = new ArrayList<>();
    ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);

    File directory = new File(dir);
    //I think it is better to extract the raw files from apk
    Tinker manager = Tinker.with(context);
    ZipFile apk = null;
    ZipFile patch = null;

    ApplicationInfo applicationInfo = context.getApplicationInfo();

    String apkPath = applicationInfo.sourceDir; //base.apk
    apk = new ZipFile(apkPath);
    patch = new ZipFile(patchFile);

    for (ShareDexDiffPatchInfo info : patchList) {

        final String infoPath = info.path;
        String patchRealPath;
        if (infoPath.equals("")) {
            patchRealPath = info.rawName;
        } else {
            patchRealPath = info.path + "/" + info.rawName;
        }

        File extractedFile = new File(dir + info.realName);

        ZipEntry patchFileEntry = patch.getEntry(patchRealPath);
        ZipEntry rawApkFileEntry = apk.getEntry(patchRealPath);

        patchDexFile(apk, patch, rawApkFileEntry, patchFileEntry, info, extractedFile);
    }

    return true;
}


这里的代码比较关键了,可以看出首先解析了meta里面的信息,meta中包含了patch中每个dex的相关数据。然后通过Application拿到sourceDir,其实就是本机apk的路径以及patch文件;根据mate中的信息开始遍历,其实就是取出对应的dex文件,最后通过patchDexFile对两个dex文件做合并

private static void patchDexFile(
            ZipFile baseApk, ZipFile patchPkg, ZipEntry oldDexEntry, ZipEntry patchFileEntry,
            ShareDexDiffPatchInfo patchInfo,  File patchedDexFile) throws IOException {
    InputStream oldDexStream = null;
    InputStream patchFileStream = null;

    oldDexStream = new BufferedInputStream(baseApk.getInputStream(oldDexEntry));
    patchFileStream = (patchFileEntry != null ? new BufferedInputStream(patchPkg.getInputStream(patchFileEntry)) : null);

    new DexPatchApplier(oldDexStream, patchFileStream).executeAndSaveTo(patchedDexFile);

}


obtient le InputStream de son fichier interne via ZipFile, qui lit en fait à partir de Local apk correspondant dex fichier , et patch Correspond au fichier dex , et passe la méthode executeAndSaveTo pour Merge into patchedDexFile, qui est le répertoire privé cible de patch. Quant à l'algorithme de fusion , c'est en fait le cœur de Tinker Si vous êtes intéressé, vous pouvez vous référer à cet article !


Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn