> Java > java지도 시간 > 본문

Android 개발 - Tinker 소스 코드 핫픽스에 대한 간략한 소개

黄舟
풀어 주다: 2017-03-10 09:30:17
원래의
1917명이 탐색했습니다.

0. 머리말

안드로이드 프로젝트 모듈에서는 기본적으로 Hot Repair 기술이 더욱 중요해졌습니다. 주로 프로젝트가 온라인 상태가 되면 다양한 문제가 발생할 수밖에 없고, 문제를 해결하기 위해 릴리스에 의존하기에는 비용이 너무 많이 들기 때문입니다.

현재 핫 리페어 기술은 기본적으로 알리의 AndFix, QZone 기획, Meituan이 제안한 이념적 계획, Tencent의 Tinker

그 중 AndFix가 아마도 가장 간단한 것일 것입니다(그리고 Tinker 명령). 행 접근 방법은 유사), 그러나 AndFix에는 특정 호환성 문제가 있고 QZone 솔루션에는 성능에 부정적인 영향이 있을 것이며, 예술 모드에서는 기억 혼란 문제가 발생합니다. Meituan이 제안하는 것은 주로 Instant Run의 원리를 기반으로 하며 아직 오픈 소스가 아니며 호환성이 좋습니다.

현재 오픈소스 솔루션을 선택한다면 Tinker가 최선의 선택인 것 같습니다Tinker의 대략적인 원리 분석.

1. 원칙 개요

Tinkerold.apknew.apkdiff를 만들고 patch.dex 그런 다음 로컬 컴퓨터 classes.dexapk를 사용하여 만듭니다. 은 새로운 classes.dex를 생성하며, 는 런타임 동안 반사를 통해 병합된 을 병합합니다. dexFile은 로드된 dexElements 배열 앞에 배치됩니다.

런타임 대체의 원리는 을 반영하고 수정하는 Qzone의 솔루션과 사실상 유사하다. dexElement. 의 차이점은 다음과 같습니다. Qzonepatch.dex를 배열 앞에 직접 삽입합니다. 🎜>Tinker 배열 앞에 삽입된 병합된 dex의 전체 양입니다. Qzone 솔루션에서 언급한 CLASS_ISPREVERIFIED 솔루션에 문제가 있기 때문입니다.

Android의 ClassLoader는 일반적으로 PathClassLoaderDexClassLoader를 사용하여 클래스를 로드합니다. AndroidPathClassLoader를 클래스 로더 , DexClassLoader.jar.apkclasses.dex 유형의 파일에서 을 내부적으로 로드할 수 있습니다. file이면 괜찮습니다. 클래스를 로드하려면 클래스 이름을 지정한 다음 findClass, PathClassLoaderDexClassLoader는 모두 BaseDexClassLoader에서 상속됩니다. 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);
로그인 후 복사


BaseDexClassLoader에는 pathList 객체가 있고 pathList에는 <🎜 컬렉션이 포함되어 있습니다. >DexFile dexElements, 클래스 로딩의 경우 이 컬렉션을 탐색하고 DexFile을 통해 을 찾는 것입니다. 일반인의 관점에서

ClassLoader는 여러 개의 dex 파일을 포함할 수 있습니다. dex 파일은 Element이며, 여러 개의 dex 파일이 하나로 정리되어 있습니다. 정렬된 배열 dexElements, 클래스를 찾을 때 dex 파일을 순서대로 순회한 다음 현재 순회하는 파일부터 시작합니다. dex 파일을 찾아 클래스를 찾으면 반환됩니다. 찾지 못한 경우 다음 dex<🎜부터 계속 검색하세요. > 파일입니다. 이 경우 patch.jar 을 복구된 클래스가 포함된 이 배열의 첫 번째 요소에 배치할 수 있습니다. >findClass를 순회하면 우리가 수정한 클래스가 발견되어 버그가 있는 클래스를 대체합니다. 이 기사의 Tinker 소스 코드 분석 두 줄:

(1

) 앱이 시작되면 병합된 classes.dex(2

)를 기본값에서 로드합니다. 디렉토리 patch가 발행된 후 classes.dex를 대상 디렉토리 에 합성합니다. 2

.

소스코드 분석

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

}
로그인 후 복사


ZipFile을 통해 내부 파일의 InputStream을 가져옵니다. 이는 실제로 로컬 apk 해당 dex 파일 패치 dex 파일 에 해당하고, executeAndSaveTo 메소드를 에 전달합니다. patchedDexFilepatch의 대상 개인 디렉터리입니다. 병합알고리즘은 사실 Tinker의 핵심입니다.


위 내용은 Android 개발 - Tinker 소스 코드 핫픽스에 대한 간략한 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿