首頁 Java java教程 Android開發— 熱修復Tinker源碼的簡單介紹

Android開發— 熱修復Tinker源碼的簡單介紹

Mar 10, 2017 am 09:30 AM

0.  前言

#熱修復這項技術,基本上已經成為Android專案比較重要的模組了。主要因為專案在上線之後,都難免會有各種問題,而依賴髮版去修復問題,成本太高了。

現在熱修復的技術基本上有阿里的AndFixQZone的方案、美團提出的思想方案以及騰訊的Tinker等。

其中AndFix可能存取是最簡單的一個(和Tinker指令行接入方式差不多),不過AndFix相容性有一定的問題QZone方案對效能會有一定的影響,且在Art模式下出現記憶體錯亂的問題,美團提出的思想方案主要是基於Instant Run的原理,目前尚未開源,相容性較好。

這麼看來,如果選擇開源方案,Tinker目前是最佳的選擇,下面來看看 #Tinker的大致的原理分析。

1. #原則概述

#Tinker##Tinker #old.apknew.apk做了##diff,拿 patch.dex後將其與本機中apkclasses.dex做了合併,產生新的classes.dex運行時透過反射##將合併後的 dex檔案放置在載入的dexElements

陣列的前面。 運行時替代的原理,其實和Qzone的方案差不多,都是去反射修改dexElements兩者的差異是:Qzone是直接將patch.dex插到陣列的前面;而Tinker合併後的全量dex是插在陣列的前面。因為Qzone方案中提到的

###CLASS_ISPREVERIFIED######的解決方案有問題。 ######

Android的ClassLoader系統中載入類別一般使用的是PathClassLoader#和DexClassLoader## ,大家只需要明白,Android使用PathClassLoader作為其類別載入器DexClassLoader可以從.jar.apk類型的檔案內部載入 classes.dex就好了。 對於載入類,無非是給個classname,然後去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的時候,我們修復的類別就會被查找到,從而取代有bug的類別本片文章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#檔案#patch #中對應dex檔案,對二者的透過executeAndSaveTo#方法進行#合併至patchedDexFile,即patch的目標私有目錄。 至於合併

演算法

###,這裡其實才是Tinker###比較核心的地方,有興趣可以參考這篇文章! ############

以上是Android開發— 熱修復Tinker源碼的簡單介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1664
14
CakePHP 教程
1423
52
Laravel 教程
1321
25
PHP教程
1269
29
C# 教程
1249
24
PHP和Python:比較兩種流行的編程語言 PHP和Python:比較兩種流行的編程語言 Apr 14, 2025 am 12:13 AM

PHP和Python各有優勢,選擇依據項目需求。 1.PHP適合web開發,尤其快速開發和維護網站。 2.Python適用於數據科學、機器學習和人工智能,語法簡潔,適合初學者。

PHP行動:現實世界中的示例和應用程序 PHP行動:現實世界中的示例和應用程序 Apr 14, 2025 am 12:19 AM

PHP在電子商務、內容管理系統和API開發中廣泛應用。 1)電子商務:用於購物車功能和支付處理。 2)內容管理系統:用於動態內容生成和用戶管理。 3)API開發:用於RESTfulAPI開發和API安全性。通過性能優化和最佳實踐,PHP應用的效率和可維護性得以提升。

PHP:網絡開發的關鍵語言 PHP:網絡開發的關鍵語言 Apr 13, 2025 am 12:08 AM

PHP是一種廣泛應用於服務器端的腳本語言,特別適合web開發。 1.PHP可以嵌入HTML,處理HTTP請求和響應,支持多種數據庫。 2.PHP用於生成動態網頁內容,處理表單數據,訪問數據庫等,具有強大的社區支持和開源資源。 3.PHP是解釋型語言,執行過程包括詞法分析、語法分析、編譯和執行。 4.PHP可以與MySQL結合用於用戶註冊系統等高級應用。 5.調試PHP時,可使用error_reporting()和var_dump()等函數。 6.優化PHP代碼可通過緩存機制、優化數據庫查詢和使用內置函數。 7

PHP的持久相關性:它還活著嗎? PHP的持久相關性:它還活著嗎? Apr 14, 2025 am 12:12 AM

PHP仍然具有活力,其在現代編程領域中依然佔據重要地位。 1)PHP的簡單易學和強大社區支持使其在Web開發中廣泛應用;2)其靈活性和穩定性使其在處理Web表單、數據庫操作和文件處理等方面表現出色;3)PHP不斷進化和優化,適用於初學者和經驗豐富的開發者。

PHP與Python:了解差異 PHP與Python:了解差異 Apr 11, 2025 am 12:15 AM

PHP和Python各有優勢,選擇應基於項目需求。 1.PHP適合web開發,語法簡單,執行效率高。 2.Python適用於數據科學和機器學習,語法簡潔,庫豐富。

PHP和Python:代碼示例和比較 PHP和Python:代碼示例和比較 Apr 15, 2025 am 12:07 AM

PHP和Python各有優劣,選擇取決於項目需求和個人偏好。 1.PHP適合快速開發和維護大型Web應用。 2.Python在數據科學和機器學習領域佔據主導地位。

PHP與其他語言:比較 PHP與其他語言:比較 Apr 13, 2025 am 12:19 AM

PHP適合web開發,特別是在快速開發和處理動態內容方面表現出色,但不擅長數據科學和企業級應用。與Python相比,PHP在web開發中更具優勢,但在數據科學領域不如Python;與Java相比,PHP在企業級應用中表現較差,但在web開發中更靈活;與JavaScript相比,PHP在後端開發中更簡潔,但在前端開發中不如JavaScript。

PHP和Python:解釋了不同的範例 PHP和Python:解釋了不同的範例 Apr 18, 2025 am 12:26 AM

PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。

See all articles