澳门新葡萄京官网首页 12

澳门新葡萄京官网首页Android 热修复 Tinker 接入及源码浅析

一、概述

前段时间写了两篇剖析了tinker的loader部分源码以至dex
diff/patch算法相关深入分析,那么为了确认保障完整性,最终一篇主要写tinker-patch-gradle-plugin相关了。

(间隔看的时候已经快多个月了,再不写就忘了,赶紧记录下来)

注意:

正文基于1.7.7

前两篇随笔分别为:

  • Android 热修复
    Tinker接入及源码浅析
  • Android 热修复 Tinker 源码分析之DexDiff /
    DexPatch

有意思味的能够查阅~

在介绍细节从前,大家得以先酌量下:通过三个指令生成一个patch文件,这一个文件能够用来下发做热修复(可修补常规代码、能源等),那么首先反响是什么样啊?

常规思维,供给设置oldApk,然后笔者那边build生成newApk,两者须要做diff,搜索不一样的代码、能源,通过特定的算法将diff出来的数码打成patch文件。

ok,实乃如此的,然则上述那个历程有哪些必要注意的么?

  1. 咱们在新增加财富的时候,只怕会因为大家新扩张的叁个财富,引致超多的财富id发生变化,假设这么一直实行diff,大概会变成能源错乱等(id指向了错误的图样)难题。所以应该保管,当财富转移还是新添、删除能源时,早就存在的能源的id不会发生变化。
  2. 笔者们在上线app的时候,会做代码混淆,若无做特殊的安装,每回混淆后的代码难以管教法则平等;所以,build进度中议论上急需设置混淆的mapping文件。
  3. 当项目相当大的时候,我们恐怕会遇上方法数超过65535的难题,大家许多时候会因而分包消除,那样就有主dex和其余dex的概念。集成了tinker之后,在选择的Application运维时会拾壹分早的就去做tinker的load操作,所以就调控了load相关的类必得在主dex中。
  4. 在对接一些库的时候,往往还需求配备混淆,比方第三方库中怎么样东西不能够被模糊等(当然强逼有些类在主dex中,也是有可能须求配置相对应的模糊法规)。

假设大家尝试过接入tinker并采用gradle的章程生成patch相关,会发未来急需在品种的build.gradle中,增加一些配置,那么些安顿中,会必要我们配置oldApk路径,能源的本田UR-V.txt路线,混淆mapping文件路线、还会有局部比较tinker相关的可比紧凑的布署音信等。

唯独并从未要求大家来得去管理上述几个难点(并从未令你去keep混淆法规,主dex分包准则,以至apply
mapping文件),所以上述的几个实际皆以tinker的gradle plugin
帮咱们做了。

就此,本文将会以那个难点为线索来带大家走一圈plugin的代码(当然实际上tinker
gradle plugin所做的事情远不止上述)。

说不上,tinker gradle plugin也是老大好的gradle的上学材质~

tinker修复的长河包罗五个经过,一方面服务端发生补丁包的进度;另一面客商端取得补丁包之后的修补工程,轻易的流程能够用如下的图描述:

一、概述

热修复那项本事,基本三月经济体改为项目很主要的模块了。重要归因于品种在上线之后,都难免会有种种难点,而依靠于发版去修补问题,花费太高了。

今昔热修复的技艺基本上有Ali的AndFix、QZone的方案、美团提议的思虑方案以至Tencent的Tinker等。

里面AndFix恐怕接入是最轻巧易行的叁个(和Tinker命令行接入措施超级多),可是包容性还是是有必然的主题素材的;QZone方案对质量会有自然的震慑,且在Art方式下冒出内部存款和储蓄器错乱的标题(其实这些难点小编前边并不精晓,首借使tinker在MDCC上建议的State of Qatar;美团提议的合计方案首假若根据Instant
Run的规律,如今未有开源,但是这一个方案作者或然蛮向往的,首如果包容性好。

如此那般看来,假若采纳开源方案,tinker近日是精品的挑精拣肥,tinker的介绍有那样一句:

Tinker已运转在微信的数亿Android设备上,那么为何您不接受Tinker呢?

好了,说了那般多,上面来拜谒tinker怎样接入,以致tinker的大要的准则分析。希望因而本文能够兑现救助大家越来越好的连结tinker,以至去探听tinker的三个大要的规律。

二、搜索查看代码入口

下载tinker的代码,导入后,plugin的代码都在tinker-patch-gradle-plugin中,可是当然不能够抱着代码一行一行去啃了,应该有个断定的进口,有系统的去读书那个代码。

那么这一个进口是何等呢?

实质上异常的粗略,咱们在打patch的时候,供给试行tinkerPatchDebug(注:本篇博客基于debug格局教学)。

当施行完后,将会看出实行进程包含以下流程:

:app:processDebugManifest
:app:tinkerProcessDebugManifest(tinker)
:app:tinkerProcessDebugResourceId (tinker)
:app:processDebugResources
:app:tinkerProguardConfigTask(tinker)
:app:transformClassesAndResourcesWithProguard
:app:tinkerProcessDebugMultidexKeep (tinker)
:app:transformClassesWidthMultidexlistForDebug
:app:assembleDebug
:app:tinkerPatchDebug(tinker)

注:包罗(tinker卡塔尔的都是tinker plugin 所增添的task

能够见见局地task参加到了build的流程中,那么那几个task是哪些进入到build进程中的呢?

在我们接入tinker之后,build.gradle中犹如下代码:

if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'
    tinkerPatch {} // 各种参数
}

如若翻开了tinker,会apply四个plugincom.tencent.tinker.patch

澳门新葡萄京官网首页 1

名称实际上正是properties文件的名字,该文件会对应切切实实的插件类。

对此gradle
plugin不打听的,能够参考,前边写会抽空单独写一篇详细讲gradle的稿子。

上面看TinkerPatchPlugin,在apply方法中,里面大致有像样的代码:

// ... 省略了一堆代码
TinkerPatchSchemaTask tinkerPatchBuildTask 
        = project.tasks.create("tinkerPatch${variantName}", TinkerPatchSchemaTask)
tinkerPatchBuildTask.dependsOn variant.assemble

TinkerManifestTask manifestTask 
        = project.tasks.create("tinkerProcess${variantName}Manifest", TinkerManifestTask)
manifestTask.mustRunAfter variantOutput.processManifest
variantOutput.processResources.dependsOn manifestTask

TinkerResourceIdTask applyResourceTask 
        = project.tasks.create("tinkerProcess${variantName}ResourceId", TinkerResourceIdTask)
applyResourceTask.mustRunAfter manifestTask
variantOutput.processResources.dependsOn applyResourceTask

if (proguardEnable) {
    TinkerProguardConfigTask proguardConfigTask 
            = project.tasks.create("tinkerProcess${variantName}Proguard", TinkerProguardConfigTask)
    proguardConfigTask.mustRunAfter manifestTask

    def proguardTask = getProguardTask(project, variantName)
    if (proguardTask != null) {
        proguardTask.dependsOn proguardConfigTask
    }

}
if (multiDexEnabled) {
    TinkerMultidexConfigTask multidexConfigTask 
            = project.tasks.create("tinkerProcess${variantName}MultidexKeep", TinkerMultidexConfigTask)
    multidexConfigTask.mustRunAfter manifestTask

    def multidexTask = getMultiDexTask(project, variantName)
    if (multidexTask != null) {
        multidexTask.dependsOn multidexConfigTask
    }
}

澳门新葡萄京官网首页,能够见到它通过gradle Project
API创制了5个task,通过dependsOn,mustRunAfter插入到了原来的流水生产线中。

例如:

TinkerManifestTask manifestTask = ...
manifestTask.mustRunAfter variantOutput.processManifest
variantOutput.processResources.dependsOn manifestTask

TinkerManifestTask必得在processManifest之后实践,processResources在manifestTask后实行。

于是流程变为:

processManifest-> manifestTask-> processResources

别的同理。

ok,大概了解了这一个task是怎么注入的事后,接下去就看看每种task的具体效果呢。

注:假如大家有须要在build进程中搞事,能够参见上述task编写以至凭仗形式的设置。

澳门新葡萄京官网首页 2

二、接入Tinker

连着tinker近来给了二种艺术,一种是依赖命令行的主意,相近于AndFix的对接格局;一种正是gradle的章程。

虚构初期接纳Andfix的app应该挺多的,以至无数人对gradle的相关配置也许感到比较冗杂的,下面前蒙受二种艺术都介绍下。

三、各个Task的切切实举办为

我们依照上述的流程来看,依次为:

TinkerManifestTask
TinkerResourceIdTask
TinkerProguardConfigTask
TinkerMultidexConfigTask
TinkerPatchSchemaTask

丢个图,对应下:

澳门新葡萄京官网首页 3

Tinker热修复进度暗暗提示图

(1)命令行接入

连着从前大家先考虑下,接入的话,平常须要的前提(开启混淆的意况)。

  • 对于API平日的话,大家对接热修库,会在Application#onCreate中开展一下伊始化操作。然后在有些地方去调用相似loadPatch那样的API去加载patch文件。
  • 对此patch的生成简单的主意正是经过三个apk做相比然后变化;须要专一的是:八个apk做相比,要求的前提条件,第二回打包混淆所使用的mapping文件应当和线上apk是平等的。

末段便是看看这么些项目有未有亟待配置混淆;

有了大致的概念,大家就基本领悟命令行接入tinker,大致须要如何步骤了。

四、TinkerManifestTask

#TinkerManifestTask
@TaskAction
def updateManifest() {
    // Parse the AndroidManifest.xml
    String tinkerValue = project.extensions.tinkerPatch.buildConfig.tinkerId

    tinkerValue = TINKER_ID_PREFIX + tinkerValue;//"tinker_id_"

    // /build/intermediates/manifests/full/debug/AndroidManifest.xml
    writeManifestMeta(manifestPath, TINKER_ID, tinkerValue)

    addApplicationToLoaderPattern()
    File manifestFile = new File(manifestPath)
    if (manifestFile.exists()) {
        FileOperation.copyFileUsingStream(manifestFile, project.file(MANIFEST_XML))
    }
}

那边根本做了两件事:

writeManifestMeta首要就是剖析AndroidManifest.xml,在<application>里头增加贰个meta标签,value为tinkerValue。譬如:

<meta-data
        android:name="TINKER_ID"
        android:value="tinker_id_com.zhy.abc" />

此地不详细展开了,话说groovy分析XML真低价。

addApplicationToLoaderPattern首假若记录自个儿的application类名和tinker相关的片段load
class com.tencent.tinker.loader.*,记录在project.extensions.tinkerPatch.dex.loader中。

末尾copy改过后的AndroidManifest.xmlbuild/intermediates/tinker_intermediates/AndroidManifest.xml

此处大家须求想转手,在文初的分析中,并不曾想到必要tinkerId这几个东西,那么它毕竟是干嘛的吧?

看一下Wechat提供的参数表明,就驾驭了:

在运维进程中,大家要求证实基准apk包的tinkerId是或不是等于补丁包的tinkerId。那么些是调整补丁包能运营在如何规范包上边,平时的话大家能够动用Git版本号、versionName等等。

想转手,在非强逼晋级的图景下,线上相同布满着相继版本的app。可是。你打patch肯定是对应某些版本,所以你要确定保障这么些patch下发下去只影响对应的版本,不会对其余版本变成影响,所以您必要tinkerId与具象的版本相呼应。

ok,下一个TinkerResourceIdTask。

服务端补丁产生进程首倘使tinker
定义的gradle插件来成功。大家在小说的背后会详细介绍gradle插件的细节。
咱俩首先来深入分析客户端的逻辑。多少个总体的apk解压之后基本上富含dex文件,财富文件和so文件。后边的作品中曾经详尽介绍了dex文件的修复进程,本章首要介绍能源文件和so文件的修补。

正视引进

dependencies {
    // ...
    //可选,用于生成application类
    provided('com.tencent.tinker:tinker-android-anno:1.7.7')
    //tinker的核心库
    compile('com.tencent.tinker:tinker-android-lib:1.7.7')
}

顺便加一下签定的构造:

android{
  //...
    signingConfigs {
        release {
            try {
                storeFile file("release.keystore")
                storePassword "testres"
                keyAlias "testres"
                keyPassword "testres"
            } catch (ex) {
                throw new InvalidUserDataException(ex.toString())
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled true
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            debuggable true
            minifyEnabled true
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

文末会有demo的下载地址,能够一向参谋build.gradle文件,不用忧虑这个签字文件去哪找。

五、TinkerResourceIdTask

文初提到,打patch的经超过实际际上要调整原来就有的能源id不能够产生变化,那一个task所做的事便是为此。

一旦保证本来就有财富的id保持不改变吗?

实质上要求public.xmlids.xml的出席,即预先在public.xml中的如下概念,在第一回打包之后可保持该能源对应的id值不改变。

注:对xml文件的称号应当未有强须求。

<public type="id" name="search_button" id="0x7f0c0046" />

好些个时候大家在追寻一定能源,平日都能观看通过public.xml去牢固能源id,可是此地有个ids.xml是干嘛的吗?

上面那篇小说有个很好的讲授~

首先必要生成public.xml,public.xml的更换通过aapt编写翻译时加多-P参数生成。相关代码通过gradle插件去hook
Task无缝参加该参数,有几许内需小心,通过appt生成的public.xml并非足以平昔用的,该文件中存在id类型的能源,生成patch时采纳步入编译的时候会报resource
is not
defined,杀绝格局是将id类型型的能源单独记录到ids.xml文件中,相当于二个扬言进程,编写翻译的时候和public.xml同样,将ids.xml也参加编写翻译就可以。

ok,知道了public.xml和ids.xml的作用之后,要求再思索一下什么样有限支撑id不改变?

第一我们在配置old
apk的时候,会陈设tinkerApplyResourcePath参数,该参数对应二个Wrangler.txt,里面的原委富含了全数old
apk中资源对应的int值。

那么大家能够如此做,依据那几个安德拉.txt,把此中的多寡写成public.xml不就能够保险原来的财富对应的int值不改变了么。

实乃那样的,不过tinker做了更加的多,不止将old
apk的中的能源消息写到public.xml,并且还过问了新的能源,对新的能源遵照能源id的变越来越准绳,也分配的相应的int值,写到了public.xml,能够说该task包办了能源id的转移。

能源文件的修复

财富文件的修复进度发生在客商端。富含五个重点的进程,首先是顾客端获取到patch.apk之后,和本地的base.apk的联合进程,这些进度依据BSD算法将patch的财富文件和base的能源文件合併,最终打包到fix.apk中;另二个是能源文件的加载进度,重要的效果与利益是带领base.apk使用fix.apk中的财富(这里有个关键点:固然大家在顾客端有了fix.apk,可是大家的fix.apk并未设置,运营应用的还是base.apk,只是辅导base.apk使用fix.apk里面包车型大巴dex文件,能源文件,so文件)。大家注重介绍能源文件的加载。能源文件的联合主若是算法,大家那边不做牵线,感兴趣的读者能够在tinker源码中自行学习BSPatch.java。

 public static boolean loadTinkerResources(Context context, boolean tinkerLoadVerifyFlag, String directory, Intent intentResult) {
        if (resPatchInfo == null || resPatchInfo.resArscMd5 == null) {
            return true;
        }
        String resourceString = directory + "/" + RESOURCE_PATH +  "/" + RESOURCE_FILE;
        File resourceFile = new File(resourceString);
        long start = System.currentTimeMillis();

        if (tinkerLoadVerifyFlag) {
            if (!SharePatchFileUtil.checkResourceArscMd5(resourceFile, resPatchInfo.resArscMd5)) {
                Log.e(TAG, "Failed to load resource file, path: " + resourceFile.getPath() + ", expect md5: " + resPatchInfo.resArscMd5);
                ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_MD5_MISMATCH);
                return false;
            }
            Log.i(TAG, "verify resource file:" + resourceFile.getPath() + " md5, use time: " + (System.currentTimeMillis() - start));
        }
        try {   TinkerResourcePatcher.monkeyPatchExistingResources(context, resourceString);
            Log.i(TAG, "monkeyPatchExistingResources resource file:" + resourceString + ", use time: " + (System.currentTimeMillis() - start));
        } catch (Throwable e) {
            Log.e(TAG, "install resources failed");
            //remove patch dex if resource is installed failed
            try {
                SystemClassLoaderAdder.uninstallPatchDex(context.getClassLoader());
            } catch (Throwable throwable) {
                Log.e(TAG, "uninstallPatchDex failed", e);
            }
            intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
            ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION);
            return false;
        }

        return true;
    }

resourceString
局地变量代表的是fix.apk的财富文件。首先检查文件的的md5是不是准确。然后再TinkerResourcePatcher中做到能源加载。

public static void monkeyPatchExistingResources(Context context, String externalResourceFile) throws Throwable {
        if (externalResourceFile == null) {
            return;
        }

        for (Field field : new Field[]{packagesFiled, resourcePackagesFiled}) {
            Object value = field.get(currentActivityThread);

            for (Map.Entry<String, WeakReference<?>> entry
                : ((Map<String, WeakReference<?>>) value).entrySet()) {
                Object loadedApk = entry.getValue().get();
                if (loadedApk == null) {
                    continue;
                }
                if (externalResourceFile != null) {
                    resDir.set(loadedApk, externalResourceFile);
                }
            }
        }
        // Create a new AssetManager instance and point it to the resources installed under
        if (((Integer) addAssetPathMethod.invoke(newAssetManager, externalResourceFile)) == 0) {
            throw new IllegalStateException("Could not create new AssetManager");
        }

        // Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
        // in L, so we do it unconditionally.
        ensureStringBlocksMethod.invoke(newAssetManager);

        for (WeakReference<Resources> wr : references) {
            Resources resources = wr.get();
            //pre-N
            if (resources != null) {
                // Set the AssetManager of the Resources instance to our brand new one
                try {
                    assetsFiled.set(resources, newAssetManager);
                } catch (Throwable ignore) {
                    // N
                    Object resourceImpl = resourcesImplFiled.get(resources);
                    // for Huawei HwResourcesImpl
                    Field implAssets = ShareReflectUtil.findField(resourceImpl, "mAssets");
                    implAssets.setAccessible(true);
                    implAssets.set(resourceImpl, newAssetManager);
                }

                clearPreloadTypedArrayIssue(resources);

                resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
            }
        }

        // Handle issues caused by WebView on Android N.
        // Issue: On Android N, if an activity contains a webview, when screen rotates
        // our resource patch may lost effects.
//        publicSourceDirField.set(context.getApplicationInfo(), externalResourceFile);

        if (!checkResUpdate(context)) {
            throw new TinkerRuntimeException(ShareConstants.CHECK_RES_INSTALL_FAIL);
        }
    }

入眼的效率在五个for循环中。外层的for循环是多少个菲尔德对象packagesFiled和resourcePackagesFiled,他们各自表示ActivityThread中的成员mPackages和mResourcePackages。里层的for循环分别纠正那八个Field对象中的成员resDir,也便是LoadedApk的分子mResDir。这里根本透过反射改过系统文件的品质,完毕能源的加载。前边的流程首要对android系统版本宽容管理。

为啥纠正了ActivityThread的习性mPackages,mResourcePackages和LoadedApk类的mResDir属性就可以完毕Base.apk加载Fix.apk的能源?我们从Android源码的角度剖析一下。

平时大家赢得某一能源的代码如下:context.getResource(卡塔尔.getxxxx,这里的context大家清楚是ContextImpl类。ContextImpl的点子getResource(卡塔尔国得到的是它的习性mResources,mResources代表了一个能源包,也正是说如果这里的mResources代表的是Fix.apk的财富包,大家就完事了能源的加载。mResources是何许开端化的啊?

 final void init(LoadedApk packageInfo,
                IBinder activityToken, ActivityThread mainThread,
                Resources container, String basePackageName) {
        mPackageInfo = packageInfo;
        mBasePackageName = basePackageName != null ? basePackageName : packageInfo.mPackageName;
        mResources = mPackageInfo.getResources(mainThread);

        if (mResources != null && container != null
                && container.getCompatibilityInfo().applicationScale !=
                        mResources.getCompatibilityInfo().applicationScale) {
            if (DEBUG) {
                Log.d(TAG, "loaded context has different scaling. Using container's" +
                        " compatiblity info:" + container.getDisplayMetrics());
            }
            mResources = mainThread.getTopLevelResources(
                    mPackageInfo.getResDir(), container.getCompatibilityInfo());
        }
        mMainThread = mainThread;
        mContentResolver = new ApplicationContentResolver(this, mainThread);

        setActivityToken(activityToken);
    }

在ContextImpl发轫化的时候对mResources
赋值。ActivityThread的艺术getTopLevelResources和LoadedApk的不二秘籍getResources都足以对mResources
,最后都以调用的

Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {
        ResourcesKey key = new ResourcesKey(resDir, compInfo.applicationScale);
        Resources r;
        synchronized (mPackages) {
            // Resources is app scale dependent.
            if (false) {
                Slog.w(TAG, "getTopLevelResources: " + resDir + " / "
                        + compInfo.applicationScale);
            }
            WeakReference<Resources> wr = mActiveResources.get(key);
            r = wr != null ? wr.get() : null;
            //if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
            if (r != null && r.getAssets().isUpToDate()) {
                if (false) {
                    Slog.w(TAG, "Returning cached resources " + r + " " + resDir
                            + ": appScale=" + r.getCompatibilityInfo().applicationScale);
                }
                return r;
            }
        }

        //if (r != null) {
        //    Slog.w(TAG, "Throwing away out-of-date resources!!!! "
        //            + r + " " + resDir);
        //}

        AssetManager assets = new AssetManager();
        if (assets.addAssetPath(resDir) == 0) {
            return null;
        }

        //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);
        DisplayMetrics metrics = getDisplayMetricsLocked(null, false);
        r = new Resources(assets, metrics, getConfiguration(), compInfo);
        if (false) {
            Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
                    + r.getConfiguration() + " appScale="
                    + r.getCompatibilityInfo().applicationScale);
        }

        synchronized (mPackages) {
            WeakReference<Resources> wr = mActiveResources.get(key);
            Resources existing = wr != null ? wr.get() : null;
            if (existing != null && existing.getAssets().isUpToDate()) {
                // Someone else already created the resources while we were
                // unlocked; go ahead and use theirs.
                r.getAssets().close();
                return existing;
            }

            // XXX need to remove entries when weak references go away
            mActiveResources.put(key, new WeakReference<Resources>(r));
            return r;
        }
    }

那边逻辑很清楚了。利用resDir设置AssetManager的属性,并创办Resource对象,resource对象和resDir一一对应。这里resDir参数是LoadedApk的习性mResourecDir。因而全部逻辑完结正是一旦我们纠正LoadedApk的质量mResourceDir是Fix.apk的能源,ContextImpl的性质mResource就象征了Fix.apk的财富。如下的图表明了她们之间的涉嫌:

澳门新葡萄京官网首页 4

Android系统财富文件相关类

API引入

API首要正是起先化和loadPacth。

健康状态下,我们会假造在Application的onCreate中去初叶化,可是tinker推荐上面包车型地铁写法:

@DefaultLifeCycle(application = ".SimpleTinkerInApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)
public class SimpleTinkerInApplicationLike extends ApplicationLike {
    public SimpleTinkerInApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        TinkerInstaller.install(this);
    }
}

ApplicationLike通过名字你大概会猜,并不是是Application的子类,而是一个像样Application的类。

tinker建议编写二个ApplicationLike的子类,你能够当成Application去行使,注意顶端的证明:@DefaultLifeCycle,其application属性,会在编写翻译期生成二个SimpleTinkerInApplication类。

就此,尽管大家这么写了,不过实际上Application会在编译期生成,所以AndroidManifest.xml中是那般的:

 <application
        android:name=".SimpleTinkerInApplication"
        .../>

编写制定如果报红,能够build下。

诸有此类实在也能猜出来,这几个评释背后有个Annotation
Processor在做拍卖,假使您没掌握过,能够看下:

Android
如何编写基于编写翻译时表明的等级次序

因此该文子禽对二个编写翻译时注解的运营流程和基本API有肯定的掌握,文中也会对tinker该部分的源码做解析。

上述,就成功了tinker的初叶化,那么调用loadPatch的空子,咱们一贯在Activity中增加叁个Button设置:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void loadPatch(View view) {
        TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
                Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed.apk");
    }
}

咱俩会将patch文件直接push到sdcard根目录;

由此自然要小心:增加SDCard权限,假若你是6.x之上的类别,本人增添上授权代码,可能手动在安装页面张开SDCard读写权限。

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

除以以外,有个独辟蹊径的地点就是tinker必要在AndroidManifest.xml中指定TINKER_ID。

<application>
  <meta-data
            android:name="TINKER_ID"
            android:value="tinker_id_6235657" />
    //...
</application>

到此API相关的就甘休了,剩下的就是考虑patch怎样转移。

浅析前的下结论

好了,由于代码非常短,作者主宰在这里个地方先用计算性的言语总计下,若无耐性看代码的可以从来跳过源码解析阶段:

先是将设置的old 大切诺基.txt读取到内部存款和储蓄器中,转为:

  • 一个Map,key-value都代表几个切实可行财富消息;直接复用,不会调换新的财富音信。
  • 二个Map,key为能源类型,value为此类财富当前的最大int值;参预新的能源id的生成。

接下去遍历当前app中的财富,财富分为:

values文件夹下文件

对持有values相关文件夹下的文书已经管理完结,差相当少的拍卖为:遍历文件中的节点,差非常的少有item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction那个节点,将装有的节点按类型分类存款和储蓄到rTypeResourceMap(key为财富类型,value为对应品种能源集结Set)中。

里头declare-styleable那个标签,首要读取其内部的attr标签,对attr标签对应的财富按上述管理。

res下非values文件夹

开荒本身的连串有看一眼,除了values相关还应该有layout,anim,color等文件夹,重要分为两类:

一类是对 文件
即为财富,比如R.layout.xxx,R.drawable.xxx等;另一类为xml文档中以@+(去除@+Android:id),其实就是找到大家自定义id节点,然后截取该节点的id值部分作为质量的名号(举例:@+id/tv,tv即为属性的称号)。

比方和装置的old
apk汉语件中平等name和type的节点无需独特处理,直接复用就能够;假使不设有则要求生成新的typeId、resourceId等音信。

会将装有变化的能源都存到rTypeResourceMap中,最终写文件。

这么就基本搜罗到了具备的内需生成能源新闻的享有的能源,最终写到public.xml即可。

总括性的言语难免有一对脱漏,实际以源码解析为行业内部。

So文件的加载

so的修复过程也是在客商端完结的。首先是遵照BSD算法修复so,然后在动用so文件的地点加载。

patch生成

tinker提供了patch生成的工具,源码见:tinker-patch-cli,打成叁个jar就足以利用,而且提供了命令行相关的参数以至文件。

命令行如下:

java -jar tinker-patch-cli-1.7.7.jar -old old.apk -new new.apk -config tinker_config.xml -out output

亟需静心的便是tinker_config.xml,里面包涵tinker的计划,比如签字文件等。

此地大家直接使用tinker提供的具名文件,所以无需做修正,可是里面有个Application的item修正为与本例一致:

<loader value="com.zhy.tinkersimplein.SimpleTinkerInApplication"/>

大概的公文布局如下:

澳门新葡萄京官网首页 5

可以在tinker-patch-cli中领到,也许直接下载文末的事例。

上述介绍了patch生成的指令,最后索要介意的便是,在首先次打出apk的时候,保留下转移的mapping文件,在/build/outputs/mapping/release/mapping.txt

可以copy到与proguard-rules.pro同目录,同一时候在其次次打修复包的时候,在proguard-rules.pro中加多上:

-applymapping mapping.txt

确定保证持续的卷入与线上包使用的是同三个mapping文件。

tinker自身的歪曲相关安插,能够参见:tinker_proguard.pro

假使,你对该片段叙述不打听,能够直接查看源码就可以。

伊始源码解析

@TaskAction
def applyResourceId() {
     // 资源mapping文件
    String resourceMappingFile = project.extensions.tinkerPatch.buildConfig.applyResourceMapping

    // resDir /build/intermediates/res/merged/debug
    String idsXml = resDir + "/values/ids.xml";
    String publicXml = resDir + "/values/public.xml";
    FileOperation.deleteFile(idsXml);
    FileOperation.deleteFile(publicXml);

    List<String> resourceDirectoryList = new ArrayList<String>();
    // /build/intermediates/res/merged/debug
    resourceDirectoryList.add(resDir);

    project.logger.error("we build ${project.getName()} apk with apply resource mapping file ${resourceMappingFile}");

    project.extensions.tinkerPatch.buildConfig.usingResourceMapping = true;

    // 收集所有的资源,以type->type,name,id,int/int[]存储
    Map<RDotTxtEntry.RType, Set<RDotTxtEntry>> rTypeResourceMap = PatchUtil.readRTxt(resourceMappingFile);

    AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap);

    PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml);
    File publicFile = new File(publicXml);
    if (publicFile.exists()) {
        FileOperation.copyFileUsingStream(publicFile, project.file(RESOURCE_PUBLIC_XML));
        project.logger.error("tinker gen resource public.xml in ${RESOURCE_PUBLIC_XML}");
    }
    File idxFile = new File(idsXml);
    if (idxFile.exists()) {
        FileOperation.copyFileUsingStream(idxFile, project.file(RESOURCE_IDX_XML));
        project.logger.error("tinker gen resource idx.xml in ${RESOURCE_IDX_XML}");
    }
}

差十分少浏览下代码,能够看出首先检查实验是还是不是设置了resource
mapping文件,若无设置会平昔跳过。并且最后的产品是public.xmlids.xml

因为生成patch时,要求确认保障四回打包已经存在的财富的id一致,必要public.xmlids.xml的参与。

先是清理已经存在的public.xmlids.xml,然后通过PatchUtil.readRTxt读取resourceMappingFile(参数中安装的),该公文记录的格式如下:

int anim abc_slide_in_bottom 0x7f050006
int id useLogo 0x7f0b0012
int[] styleable AppCompatImageView { 0x01010119, 0x7f010027 }
int styleable AppCompatImageView_android_src 0
int styleable AppCompatImageView_srcCompat 1

差十分少有两类,一类是int型种种能源;一类是int[]数组,代表styleable,其后边紧跟着它的item(领悟自定义View的必然不生分)。

PatchUtil.readRTxt的代码就不贴了,轻易描述下:

先是正则按行相称,每行分为四某个,即idType,rType,name,idValue(四个属性为LX570DotTxtEntry的成员变量)。

  • idType有两种INTINT_ARRAY
  • rType包涵各样能源:

ANIM, ANIMATOR, ARRAY, ATTR, BOOL, COLOR, DIMEN, DRAWABLE, FRACTION, ID, INTEGER, INTERPOLATOR, LAYOUT, MENU, MIPMAP, PLURALS, RAW, STRING, STYLE, STYLEABLE, TRANSITION, XML

name和value便是惯常的键值对了。

此处并从未对styleable做特殊管理。

最后按rType分类,存在一个Map中,即key为rType,value为一个SportageDotTxtEntry类型的Set集结。

回想下剩下的代码:

//...省略前半部分
     AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap);
    PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml);
    File publicFile = new File(publicXml);
    if (publicFile.exists()) {
        FileOperation.copyFileUsingStream(publicFile, project.file(RESOURCE_PUBLIC_XML));
        project.logger.error("tinker gen resource public.xml in ${RESOURCE_PUBLIC_XML}");
    }
    File idxFile = new File(idsXml);
    if (idxFile.exists()) {
        FileOperation.copyFileUsingStream(idxFile, project.file(RESOURCE_IDX_XML));
        project.logger.error("tinker gen resource idx.xml in ${RESOURCE_IDX_XML}");
    }

那就是提起了AaptUtil.collectResource方法,传入了resDir目录和我们刚刚搜集了能源音信的Map,再次回到了二个AaptResourceCollector对象,看名称是对aapt相关的能源的访谈:

看代码:

public static AaptResourceCollector collectResource(List<String> resourceDirectoryList,
                                                    Map<RType, Set<RDotTxtEntry>> rTypeResourceMap) {
    AaptResourceCollector resourceCollector = new AaptResourceCollector(rTypeResourceMap);
    List<com.tencent.tinker.build.aapt.RDotTxtEntry> references = new ArrayList<com.tencent.tinker.build.aapt.RDotTxtEntry>();
    for (String resourceDirectory : resourceDirectoryList) {
        try {
            collectResources(resourceDirectory, resourceCollector);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    for (String resourceDirectory : resourceDirectoryList) {
        try {
            processXmlFilesForIds(resourceDirectory, references, resourceCollector);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    return resourceCollector;
}

首先起始化了三个AaptResourceCollector对象,看其结构方法:

public AaptResourceCollector(Map<RType, Set<RDotTxtEntry>> rTypeResourceMap) {
    this();
    if (rTypeResourceMap != null) {
        Iterator<Entry<RType, Set<RDotTxtEntry>>> iterator = rTypeResourceMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<RType, Set<RDotTxtEntry>> entry = iterator.next();
            RType rType = entry.getKey();
            Set<RDotTxtEntry> set = entry.getValue();

            for (RDotTxtEntry rDotTxtEntry : set) {
                originalResourceMap.put(rDotTxtEntry, rDotTxtEntry);

                ResourceIdEnumerator resourceIdEnumerator = null;
                    // ARRAY主要是styleable
                if (!rDotTxtEntry.idType.equals(IdType.INT_ARRAY)) {
                        // 获得resourceId
                    int resourceId = Integer.decode(rDotTxtEntry.idValue.trim()).intValue();
                    // 获得typeId
                    int typeId = ((resourceId & 0x00FF0000) / 0x00010000);

                    if (typeId >= currentTypeId) {
                        currentTypeId = typeId + 1;
                    }

                        // type -> id的映射
                    if (this.rTypeEnumeratorMap.containsKey(rType)) {
                        resourceIdEnumerator = this.rTypeEnumeratorMap.get(rType);
                        if (resourceIdEnumerator.currentId < resourceId) {
                            resourceIdEnumerator.currentId = resourceId;
                        }
                    } else {
                        resourceIdEnumerator = new ResourceIdEnumerator();
                        resourceIdEnumerator.currentId = resourceId;
                        this.rTypeEnumeratorMap.put(rType, resourceIdEnumerator);
                    }
                }
            }
        }
    }
}

对rTypeResourceMap依照rType进行遍历,读取种种rType对应的Set集结;然后遍历每一种rDotTxtEntry:

  1. 加入到originalResourceMap,key和value都是rDotTxtEntry对象
  2. 比方是int型财富,首先读取其typeId,并不停更新currentTypeId(保障其为遍历达成后的最大值+1卡塔尔
  3. 发轫化rTypeEnumeratorMap,key为rType,value为ResourceIdEnumerator,且ResourceIdEnumerator中的currentId保存着日前同类财富的最大的resouceId,也正是说rTypeEnumeratorMap中积累了一一rType对应的最大的能源Id。

甘休实现构造方法,实践了

  1. 遍历了resourceDirectoryList,近年来中间独有贰个resDir,然后实行了collectResources方法;
  2. 遍历了resourceDirectoryList,执行了processXmlFilesForIds

个别读代码了:

collectResources

private static void collectResources(String resourceDirectory, AaptResourceCollector resourceCollector) throws Exception {
    File resourceDirectoryFile = new File(resourceDirectory);
    File[] fileArray = resourceDirectoryFile.listFiles();
    if (fileArray != null) {
        for (File file : fileArray) {
            if (file.isDirectory()) {
                String directoryName = file.getName();
                if (directoryName.startsWith("values")) {
                    if (!isAValuesDirectory(directoryName)) {
                        throw new AaptUtilException("'" + directoryName + "' is not a valid values directory.");
                    }
                    processValues(file.getAbsolutePath(), resourceCollector);
                } else {
                    processFileNamesInDirectory(file.getAbsolutePath(), resourceCollector);
                }
            }
        }
    }
}

遍历大家的resDir中的全部文件夹

  • 如若是values相关文书夹,实施processValues
  • 非values相关文书夹则实施processFileNamesInDirectory

processValues管理values相关文书,会遍历每一个合法的values相关文书夹下的文书,实践processValuesFile(file.getAbsolutePath(), resourceCollector);

public static void processValuesFile(String valuesFullFilename,
                                     AaptResourceCollector resourceCollector) throws Exception {
    Document document = JavaXmlUtil.parse(valuesFullFilename);
    String directoryName = new File(valuesFullFilename).getParentFile().getName();
    Element root = document.getDocumentElement();

    for (Node node = root.getFirstChild(); node != null; node = node.getNextSibling()) {
        if (node.getNodeType() != Node.ELEMENT_NODE) {
            continue;
        }

        String resourceType = node.getNodeName();
        if (resourceType.equals(ITEM_TAG)) {
            resourceType = node.getAttributes().getNamedItem("type").getNodeValue();
            if (resourceType.equals("id")) {
                resourceCollector.addIgnoreId(node.getAttributes().getNamedItem("name").getNodeValue());
            }
        }

        if (IGNORED_TAGS.contains(resourceType)) {
            continue;
        }

        if (!RESOURCE_TYPES.containsKey(resourceType)) {
            throw new AaptUtilException("Invalid resource type '<" + resourceType + ">' in '" + valuesFullFilename + "'.");
        }

        RType rType = RESOURCE_TYPES.get(resourceType);
        String resourceValue = null;
        switch (rType) {
            case STRING:
            case COLOR:
            case DIMEN:
            case DRAWABLE:
            case BOOL:
            case INTEGER:
                resourceValue = node.getTextContent().trim();
                break;
            case ARRAY://has sub item
            case PLURALS://has sub item
            case STYLE://has sub item
            case STYLEABLE://has sub item
                resourceValue = subNodeToString(node);
                break;
            case FRACTION://no sub item
                resourceValue = nodeToString(node, true);
                break;
            case ATTR://no sub item
                resourceValue = nodeToString(node, true);
                break;
        }
        try {
            addToResourceCollector(resourceCollector,
                    new ResourceDirectory(directoryName, valuesFullFilename),
                    node, rType, resourceValue);
        } catch (Exception e) {
            throw new AaptUtilException(e.getMessage() + ",Process file error:" + valuesFullFilename, e);
        }
    }
}

values下相关的公文大旨都以xml咯,所以遍历xml文件,遍历其里面的节点,(values的xml文件其内部平日为item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction等),每类别型的节点对应三个rType,依据差别类型的节点也会去获得节点的值,明确三个都会推行:

addToResourceCollector(resourceCollector,
    new ResourceDirectory(directoryName, valuesFullFilename),
    node, rType, resourceValue);

注:除此以外,这里在ignoreIdSet记录了注明的id财富,这个id是一度宣称过的,所以最后在编辑ids.xml时,能够过滤掉这么些id。

上边继续看:addToResourceCollector

源码如下:

private static void addToResourceCollector(AaptResourceCollector resourceCollector,
                                           ResourceDirectory resourceDirectory,
                                           Node node, RType rType, String resourceValue) {
    String resourceName = sanitizeName(rType, resourceCollector, extractNameAttribute(node));

    if (rType.equals(RType.STYLEABLE)) {

        int count = 0;
        for (Node attrNode = node.getFirstChild(); attrNode != null; attrNode = attrNode.getNextSibling()) {
            if (attrNode.getNodeType() != Node.ELEMENT_NODE || !attrNode.getNodeName().equals("attr")) {
                continue;
            }
            String rawAttrName = extractNameAttribute(attrNode);
            String attrName = sanitizeName(rType, resourceCollector, rawAttrName);

            if (!rawAttrName.startsWith("android:")) {
                resourceCollector.addIntResourceIfNotPresent(RType.ATTR, attrName);
            }
        }
    } else {
        resourceCollector.addIntResourceIfNotPresent(rType, resourceName);
    }
}

比如不是styleable的能源,则直接拿走resourceName,然后调用resourceCollector.addIntResourceIfNotPresent(rType,
resourceName卡塔尔。

假固然styleable类型的财富,则会遍历找到其内部的attr节点,搜索非android:开班的(因为android:起先的attr的id不必要大家去明确),设置rType为ATTR,value为attr属性的name,调用addIntResourceIfNotPresent。

public void addIntResourceIfNotPresent(RType rType, String name) { //, ResourceDirectory resourceDirectory) {
    if (!rTypeEnumeratorMap.containsKey(rType)) {
        if (rType.equals(RType.ATTR)) {
            rTypeEnumeratorMap.put(rType, new ResourceIdEnumerator(1));
        } else {
            rTypeEnumeratorMap.put(rType, new ResourceIdEnumerator(currentTypeId++));
        }
    }

    RDotTxtEntry entry = new FakeRDotTxtEntry(IdType.INT, rType, name);
    Set<RDotTxtEntry> resourceSet = null;
    if (this.rTypeResourceMap.containsKey(rType)) {
        resourceSet = this.rTypeResourceMap.get(rType);
    } else {
        resourceSet = new HashSet<RDotTxtEntry>();
        this.rTypeResourceMap.put(rType, resourceSet);
    }
    if (!resourceSet.contains(entry)) {
        String idValue = String.format("0x%08x", rTypeEnumeratorMap.get(rType).next());
        addResource(rType, IdType.INT, name, idValue); //, resourceDirectory);
    }
}

先是营造三个entry,然后判定当前的rTypeResourceMap中是还是不是留存该财富实体,借使存在,则什么都不要做。

一旦不设有,则须要创设三个entry,那么重大是id的创设。

关于id的构建:

还记得rTypeEnumeratorMap么,其里面含有了笔者们设置的”res
mapping”文件,存款和储蓄了种种财富(rType)的财富的最大resourceId值。

那正是说首先推断正是是或不是业本来就有这种类型了,纵然有的话,获抽出该类型当前最大的resourceId,然后+1,最为传入财富的resourceId.

一旦不设有当前那种类型,那么只要类型为ATT卡宴则固定type为1;不然的话,新添一个typeId,为近些日子最大的type+1(currentTypeId中也是记录了现阶段最大的type值),有了连串就足以因此ResourceIdEnumerator.next(卡塔尔来收获id。

透过上述就足以组织出三个idValue了。

最后调用:

addResource(rType, IdType.INT, name, idValue);

翻开代码:

public void addResource(RType rType, IdType idType, String name, String idValue) {
    Set<RDotTxtEntry> resourceSet = null;
    if (this.rTypeResourceMap.containsKey(rType)) {
        resourceSet = this.rTypeResourceMap.get(rType);
    } else {
        resourceSet = new HashSet<RDotTxtEntry>();
        this.rTypeResourceMap.put(rType, resourceSet);
    }
    RDotTxtEntry rDotTxtEntry = new RDotTxtEntry(idType, rType, name, idValue);

    if (!resourceSet.contains(rDotTxtEntry)) {
        if (this.originalResourceMap.containsKey(rDotTxtEntry)) {
            this.rTypeEnumeratorMap.get(rType).previous();
            rDotTxtEntry = this.originalResourceMap.get(rDotTxtEntry);
        } 
        resourceSet.add(rDotTxtEntry);
    }

}

大致敬思正是只要该能源不真实就加多到rTypeResourceMap。

第一构建出该财富实体,决断该类型对应的财富集结是或不是含有该能源实体(这里contains只比对name和type),即使不分包,推断是不是在originalResourceMap中,要是存在(这里做了三个previous操作,其实与地点的代码的next操作对应,首假诺针对财富存在大家的res
map中这种景况)则抽出该财富实体,最后将该财富实体参与到rTypeResourceMap中。

ok,到此地须求小节弹指间,大家刚刚对富有values相关文书夹下的文书已经管理达成,大约的管理为:遍历文件中的节点,大概有item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction这个节点,将具有的节点按类型分类存款和储蓄到rTypeResourceMap中(假如和设置的”res
map”文件中相符name和type的节点不要求独特管理,直接复用就能够;要是海市蜃楼则要求生成新的typeId、resourceId等音信)。

其中declare-styleable其一标签,重要读取其里面包车型大巴attr标签,对attr标签对应的能源按上述管理。

管理完了values相关文件夹之后,还供给管理局地res下的此外文件,譬如layout、layout、anim等公事夹,该类财富也须要在ENCORE中生成对应的id值,那类值也急需稳固。

processFileNamesInDirectory

public static void processFileNamesInDirectory(String resourceDirectory,
                                               AaptResourceCollector resourceCollector) throws IOException {
    File resourceDirectoryFile = new File(resourceDirectory);
    String directoryName = resourceDirectoryFile.getName();
    int dashIndex = directoryName.indexOf('-');
    if (dashIndex != -1) {
        directoryName = directoryName.substring(0, dashIndex);
    }

    if (!RESOURCE_TYPES.containsKey(directoryName)) {
        throw new AaptUtilException(resourceDirectoryFile.getAbsolutePath() + " is not a valid resource sub-directory.");
    }
    File[] fileArray = resourceDirectoryFile.listFiles();
    if (fileArray != null) {
        for (File file : fileArray) {
            if (file.isHidden()) {
                continue;
            }
            String filename = file.getName();
            int dotIndex = filename.indexOf('.');
            String resourceName = dotIndex != -1 ? filename.substring(0, dotIndex) : filename;

            RType rType = RESOURCE_TYPES.get(directoryName);
            resourceCollector.addIntResourceIfNotPresent(rType, resourceName);

            System.out.println("rType = " + rType + " , resName = " + resourceName);

            ResourceDirectory resourceDirectoryBean = new ResourceDirectory(file.getParentFile().getName(), file.getAbsolutePath());
            resourceCollector.addRTypeResourceName(rType, resourceName, null, resourceDirectoryBean);
        }
    }
}

遍历res下有所文件夹,依据文件夹名称分明其对应的财富类型(举例:drawable-xhpi,则感到个中间的文件类型为drawable类型),然后遍历该文件夹下全数的文书,最终以文件名称为能源的name,文件夹明确能源的type,最后调用:

resourceCollector
.addIntResourceIfNotPresent(rType, resourceName);

processXmlFilesForIds

public static void processXmlFilesForIds(String resourceDirectory,
                                         List<RDotTxtEntry> references, AaptResourceCollector resourceCollector) throws Exception {
    List<String> xmlFullFilenameList = FileUtil
            .findMatchFile(resourceDirectory, Constant.Symbol.DOT + Constant.File.XML);
    if (xmlFullFilenameList != null) {
        for (String xmlFullFilename : xmlFullFilenameList) {
            File xmlFile = new File(xmlFullFilename);

            String parentFullFilename = xmlFile.getParent();
            File parentFile = new File(parentFullFilename);
            if (isAValuesDirectory(parentFile.getName()) || parentFile.getName().startsWith("raw")) {
                // Ignore files under values* directories and raw*.
                continue;
            }
            processXmlFile(xmlFullFilename, references, resourceCollector);
        }
    }
}

遍历除了raw*以及values*相关文件夹下的xml文件,施行processXmlFile。

public static void processXmlFile(String xmlFullFilename, List<RDotTxtEntry> references, AaptResourceCollector resourceCollector)
        throws IOException, XPathExpressionException {
    Document document = JavaXmlUtil.parse(xmlFullFilename);
    NodeList nodesWithIds = (NodeList) ANDROID_ID_DEFINITION.evaluate(document, XPathConstants.NODESET);
    for (int i = 0; i < nodesWithIds.getLength(); i++) {
        String resourceName = nodesWithIds.item(i).getNodeValue();

        if (!resourceName.startsWith(ID_DEFINITION_PREFIX)) {
            throw new AaptUtilException("Invalid definition of a resource: '" + resourceName + "'");
        }

        resourceCollector.addIntResourceIfNotPresent(RType.ID, resourceName.substring(ID_DEFINITION_PREFIX.length()));
    }

    // 省略了无关代码
}

重中之重找xml文书档案中以@+(去除@+android:id),其实便是找到我们自定义id节点,然后截取该节点的id值部分作为质量的名称(举例:@+id/tv,tv即为属性的称谓),最后调用:

resourceCollector
    .addIntResourceIfNotPresent(RType.ID, 
        resourceName.substring(ID_DEFINITION_PREFIX.length()));

上述就完毕了具备的能源的搜聚,那么余下的便是写文件了:

public static void generatePublicResourceXml(AaptResourceCollector aaptResourceCollector,
                                             String outputIdsXmlFullFilename,
                                             String outputPublicXmlFullFilename) {
    if (aaptResourceCollector == null) {
        return;
    }
    FileUtil.createFile(outputIdsXmlFullFilename);
    FileUtil.createFile(outputPublicXmlFullFilename);

    PrintWriter idsWriter = null;
    PrintWriter publicWriter = null;
    try {
        FileUtil.createFile(outputIdsXmlFullFilename);
        FileUtil.createFile(outputPublicXmlFullFilename);
        idsWriter = new PrintWriter(new File(outputIdsXmlFullFilename), "UTF-8");

        publicWriter = new PrintWriter(new File(outputPublicXmlFullFilename), "UTF-8");
        idsWriter.println("<?xml version="1.0" encoding="utf-8"?>");
        publicWriter.println("<?xml version="1.0" encoding="utf-8"?>");
        idsWriter.println("<resources>");
        publicWriter.println("<resources>");
        Map<RType, Set<RDotTxtEntry>> map = aaptResourceCollector.getRTypeResourceMap();
        Iterator<Entry<RType, Set<RDotTxtEntry>>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<RType, Set<RDotTxtEntry>> entry = iterator.next();
            RType rType = entry.getKey();
            if (!rType.equals(RType.STYLEABLE)) {
                Set<RDotTxtEntry> set = entry.getValue();
                for (RDotTxtEntry rDotTxtEntry : set) {
                    String rawName = aaptResourceCollector.getRawName(rType, rDotTxtEntry.name);
                    if (StringUtil.isBlank(rawName)) {
                        rawName = rDotTxtEntry.name;
                    }
                    publicWriter.println("<public type="" + rType + "" name="" + rawName + "" id="" + rDotTxtEntry.idValue.trim() + "" />");          
                }
                Set<String> ignoreIdSet = aaptResourceCollector.getIgnoreIdSet();
                for (RDotTxtEntry rDotTxtEntry : set) {
                    if (rType.equals(RType.ID) && !ignoreIdSet.contains(rDotTxtEntry.name)) {
                        idsWriter.println("<item type="" + rType + "" name="" + rDotTxtEntry.name + ""/>");
                    } 
                }
            }
            idsWriter.flush();
            publicWriter.flush();
        }
        idsWriter.println("</resources>");
        publicWriter.println("</resources>");
    } catch (Exception e) {
        throw new PatchUtilException(e);
    } finally {
        if (idsWriter != null) {
            idsWriter.flush();
            idsWriter.close();
        }
        if (publicWriter != null) {
            publicWriter.flush();
            publicWriter.close();
        }
    }
}

珍视就是遍历rTypeResourceMap,然后各样财富实体对应一条public标签记录写到public.xml中。

除此以外,纵然开掘该因孟秋点的type为Id,而且不在ignoreSet中,会写到ids.xml那么些文件中。(这里有个ignoreSet,这里ignoreSet中记录了values下全数的<item type=id的财富,是一贯在类型中一度宣称过的,所以去除)。

so文件加运载飞机制

SO文件加载的机遇和Dex、财富的加载有个别不均等,Dex和能源的加载都以系统在一定的时机自动去加载,而SO加载的机缘则是让开采者自身调整.开采者能够通过System类对外暴揭露来的五个静态方法load和loadLibarary加载SO.那四个法子都拿ClassLoader再经过Runtime达成的。

  • Sytem.loadLibrary
    方法是加载app安装过未来自动从apk包中放出到/data/data/packagename/lib下相应的SO文件。假设要使用那样的秘籍加载fix.apk中的so文件,须要选拔反射改进DexPathList的属性nativeLibraryDirectorie,具体的艺术能够参照他事他说加以侦查后面作品中Dex文件的加载进度。
  • System.load
    方法能够依赖开辟者钦点的门道加载SO文件,例如/data/data/packagename/tinker/patch-xxx/lib/libtest.so,这是Tinker使用的方法,开垦者不用矫正任何代码,只要钦定路径就能够加载Fix.apk的so文件。

测试

先是随意生成三个apk(API、混淆相关已经根据上述引进),安装到手提式无线电话机照旧模拟器上。

然后,copy出mapping.txt文件,设置applymapping,改善代码,再一次卷入,生成new.apk。

五遍的apk,能够透过命令行指令去生成patch文件。

如若您下载本例,命令须要在[该目录]下执行。

终极会在output文件夹中变化付加物:

澳门新葡萄京官网首页 6

大家直接将patch_signed.apk
push到sdcard,点击loadpatch,必供给观望命令行是或不是成功。

澳门新葡萄京官网首页 7

本例校勘了title。

点击load帕特ch,观察log,如若成功,应用默认为重启,然后再一次运行就能够直达修复功用。

到这里命令行的艺术就介绍完了,和Andfix的衔接的不二等秘书籍多数是均等的。

值得注意的是:该例仅彰显了宗旨的交接,对于tinker的种种配置音讯,依然须要去读tinker的文档(假设你规定要选用)tinker-wiki。

六、TinkerProguardConfigTask

还记得文初说:

  1. 大家在上线app的时候,会做代码混淆,若无做特别的安装,每一趟混淆后的代码差异应该特别了不起;所以,build进度中反对上要求安装混淆的mapping文件。
  2. 在联网一些库的时候,往往还亟需安排混淆,比方第三方库中如李铁西不可能被模糊等(当然免强某个类在主dex中,也大概要求配备相对应的歪曲法规)。

那一个task的意义很肯定了。一时候为了有限辅助一些类在main
dex中,轻巧的做法也会对其在混淆配置中进行keep(制止由于混淆变成类名修改,而使main
dex的keep失效)。

只要翻开了proguard会实践该task。

以此便是非常重要去设置混淆的mapping文件,和keep一些要求的类了。

@TaskAction
def updateTinkerProguardConfig() {
    def file = project.file(PROGUARD_CONFIG_PATH)
    project.logger.error("try update tinker proguard file with ${file}")

    // Create the directory if it doesnt exist already
    file.getParentFile().mkdirs()

    // Write our recommended proguard settings to this file
    FileWriter fr = new FileWriter(file.path)

    String applyMappingFile = project.extensions.tinkerPatch.buildConfig.applyMapping

    //write applymapping
    if (shouldApplyMapping && FileOperation.isLegalFile(applyMappingFile)) {
        project.logger.error("try add applymapping ${applyMappingFile} to build the package")
        fr.write("-applymapping " + applyMappingFile)
        fr.write("n")
    } else {
        project.logger.error("applymapping file ${applyMappingFile} is illegal, just ignore")
    }

    fr.write(PROGUARD_CONFIG_SETTINGS)

    fr.write("#your dex.loader patterns heren")
    //they will removed when apply
    Iterable<String> loader = project.extensions.tinkerPatch.dex.loader
    for (String pattern : loader) {
        if (pattern.endsWith("*") && !pattern.endsWith("**")) {
            pattern += "*"
        }
        fr.write("-keep class " + pattern)
        fr.write("n")
    }
    fr.close()
    // Add this proguard settings file to the list
    applicationVariant.getBuildType().buildType.proguardFiles(file)
    def files = applicationVariant.getBuildType().buildType.getProguardFiles()

    project.logger.error("now proguard files is ${files}")
}

读取大家设置的mappingFile,设置

-applymapping applyMappingFile

下一场设置有些暗中认可须要keep的规行矩步:

PROGUARD_CONFIG_SETTINGS =
"-keepattributes *Annotation* n" +
"-dontwarn com.tencent.tinker.anno.AnnotationProcessor n" +
"-keep @com.tencent.tinker.anno.DefaultLifeCycle public class *n" +
"-keep public class * extends android.app.Application {n" +
"    *;n" +
"}n" +
"n" +
"-keep public class com.tencent.tinker.loader.app.ApplicationLifeCycle {n" +
"    *;n" +
"}n" +
"-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {n" +
"    *;n" +
"}n" +
"n" +
"-keep public class com.tencent.tinker.loader.TinkerLoader {n" +
"    *;n" +
"}n" +
"-keep public class * extends com.tencent.tinker.loader.TinkerLoader {n" +
"    *;n" +
"}n" +
"-keep public class com.tencent.tinker.loader.TinkerTestDexLoad {n" +
"    *;n" +
"}n" +
"n"

提起底是keep住我们的application、com.tencent.tinker.loader.**以至大家设置的相关类。

TinkerManifestTask中:addApplicationToLoaderPattern首借使记录本身的application类名和tinker相关的局地load
class com.tencent.tinker.loader.*,记录在project.extensions.tinkerPatch.dex.loader

Gradle插件

(2)gradle接入

gradle接入的法门应该算是主流的措施,所以tinker也一向付出了例子,单独将该tinker-sample-android以project格局引进就可以。

引入之后,能够查看其联网API的艺术,以致有关布置。

在您每一遍build时,会在build/bakApk下生花费地包裹的apk,Sportage文件,以致mapping文件。

要是你必要生成patch文件,能够通过:

./gradlew tinkerPatchRelease  // 或者 ./gradlew tinkerPatchDebug

生成。

浮动目录为:build/outputs/tinkerPatch

澳门新葡萄京官网首页 8

亟待在意的是,供给在app/build.gradle中设置绝相比的apk(即old.apk,此次为new.apk),

ext {
    tinkerEnabled = true
    //old apk file to build patch apk
    tinkerOldApkPath = "${bakPath}/old.apk"
    //proguard mapping file to build patch apk
    tinkerApplyMappingPath = "${bakPath}/old-mapping.txt"
}

提供的例子,基本上海展览中心示了tinker的自定义扩充的不二诀窍,具体还足以参见:Tinker-自定义扩大

故而,假设你接纳命令行格局连接,也绝不要忘了就学下其帮忙什么扩展。

七、TinkerMultidexConfigTask

对应文初:

当项目异常的大的时候,我们或然会遇见方法数超过65535的主题材料,大家比超级多时候会经过分包消逝,这样就有主dex和任何dex的定义。集成了tinker之后,在接纳的Application运营时会格外早的就去做tinker的load操作,所以就调整了load相关的类必需在主dex中。

如果multiDexEnabled开启。

珍视是让相关类必需在main dex。

"-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {n" +
    "    *;n" +
    "}n" +
    "n" +
    "-keep public class * extends com.tencent.tinker.loader.TinkerLoader {n" +
    "    *;n" +
    "}n" +
    "n" +
    "-keep public class * extends android.app.Application {n" +
    "    *;n" +
    "}n"

Iterable<String> loader = project.extensions.tinkerPatch.dex.loader
    for (String pattern : loader) {
        if (pattern.endsWith("*")) {
            if (!pattern.endsWith("**")) {
                pattern += "*"
            }
        }
        lines.append("-keep class " + pattern + " {n" +
                "    *;n" +
                "}n")
                .append("n")
    }

相关类都在loader这一个会集中,在TinkerManifestTask中装置的。

何以要用gradle插件

  • 基于地方的深入分析,顾客端要先拿走patch.apk,才成功前边的修补和加载的历程,patch.apk是客商端修复的第一。tinker中的Gradle插件正是达成patch.apk的。
  • 其它,dex文件的差分和合併相比较的是字节码,BSD算法的差分和合併比较的是二进制文件,那么这里带有了一条法则,服务端的fix.apk的Dex文件的歪曲,以至财富文件的揽胜文件的性质供给和base.apk的一模二样。以至对于代码超过65536的主意,必要分包的采取,分包方法也急需和base.apk同样。那几个也是gradle插件需求管理的标题。

三、Application是怎么着编译时生成的

从注释和命名上看:

//可选,用于生成application类
provided('com.tencent.tinker:tinker-android-anno:1.7.7')

威名赫赫是该库,其组织如下:

澳门新葡萄京官网首页 9

卓越的编写翻译时注脚的体系,源码见tinker-android-anno。

入口为com.tencent.tinker.anno.AnnotationProcessor,能够在该services/javax.annotation.processing.Processor文本中找随处理类全路径。

重新提出,若是您不精通,轻松阅读下Android
怎么样编写基于编写翻译时表明的门类该文。

直接看AnnotationProcessor的process方法:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    processDefaultLifeCycle(roundEnv.getElementsAnnotatedWith(DefaultLifeCycle.class));
    return true;
}

间接调用了processDefaultLifeCycle:

private void processDefaultLifeCycle(Set<? extends Element> elements) {
        // 被注解DefaultLifeCycle标识的对象
        for (Element e : elements) {
          // 拿到DefaultLifeCycle注解对象
            DefaultLifeCycle ca = e.getAnnotation(DefaultLifeCycle.class);

            String lifeCycleClassName = ((TypeElement) e).getQualifiedName().toString();
            String lifeCyclePackageName = lifeCycleClassName.substring(0, lifeCycleClassName.lastIndexOf('.'));
            lifeCycleClassName = lifeCycleClassName.substring(lifeCycleClassName.lastIndexOf('.') + 1);

            String applicationClassName = ca.application();
            if (applicationClassName.startsWith(".")) {
                applicationClassName = lifeCyclePackageName + applicationClassName;
            }
            String applicationPackageName = applicationClassName.substring(0, applicationClassName.lastIndexOf('.'));
            applicationClassName = applicationClassName.substring(applicationClassName.lastIndexOf('.') + 1);

            String loaderClassName = ca.loaderClass();
            if (loaderClassName.startsWith(".")) {
                loaderClassName = lifeCyclePackageName + loaderClassName;
            }

             // /TinkerAnnoApplication.tmpl
            final InputStream is = AnnotationProcessor.class.getResourceAsStream(APPLICATION_TEMPLATE_PATH);
            final Scanner scanner = new Scanner(is);
            final String template = scanner.useDelimiter("\A").next();
            final String fileContent = template
                .replaceAll("%PACKAGE%", applicationPackageName)
                .replaceAll("%APPLICATION%", applicationClassName)
                .replaceAll("%APPLICATION_LIFE_CYCLE%", lifeCyclePackageName + "." + lifeCycleClassName)
                .replaceAll("%TINKER_FLAGS%", "" + ca.flags())
                .replaceAll("%TINKER_LOADER_CLASS%", "" + loaderClassName)
                .replaceAll("%TINKER_LOAD_VERIFY_FLAG%", "" + ca.loadVerifyFlag());
                JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(applicationPackageName + "." + applicationClassName);
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Creating " + fileObject.toUri());
          Writer writer = fileObject.openWriter();
            PrintWriter pw = new PrintWriter(writer);
            pw.print(fileContent);
            pw.flush();
            writer.close();

        }
    }

代码比较轻巧,能够分三局地明白:

  • 手续1:首先找到被DefaultLifeCycle标志的Element(为类对象TypeElementState of Qatar,获得该对象的包名,类名等新闻,然后经过该对象,获得@DefaultLifeCycle目标,获取该申明中宣示属性的值。
  • 步骤2:读取一个模板文件,读取为字符串,将逐条占位符通过步骤第11中学的值代替。
  • 手续3:通过JavaFileObject将替换达成的字符串写文件,其实正是本例中的Application对象。

咱俩看一眼模板文件:

package %PACKAGE%;

import com.tencent.tinker.loader.app.TinkerApplication;

/**
 *
 * Generated application for tinker life cycle
 *
 */
public class %APPLICATION% extends TinkerApplication {

    public %APPLICATION%() {
        super(%TINKER_FLAGS%, "%APPLICATION_LIFE_CYCLE%", "%TINKER_LOADER_CLASS%", %TINKER_LOAD_VERIFY_FLAG%);
    }

}

对应大家的SimpleTinkerInApplicationLike

@DefaultLifeCycle(application = ".SimpleTinkerInApplication",
        flags = ShareConstants.TINKER_ENABLE_ALL,
        loadVerifyFlag = false)
public class SimpleTinkerInApplicationLike extends ApplicationLike {}

重在就多少个占位符:

  • 包名,假设application属性值以点起来,则同包;不然而截取
  • 类名,application属性值中的类名
  • %TINKER_FLAGS%对应flags
  • %APPLICATION_LIFE_CYCLE%,编写的ApplicationLike的全路线
  • “%TINKER_LOADER_CLASS%”,这一个值大家没有设置,实际上对应@DefaultLifeCycle的loaderClass属性,暗中同意值为com.tencent.tinker.loader.TinkerLoader
  • %TINKER_LOAD_VERIFY_FLAG%对应loadVerifyFlag

于是最终生成的代码为:

/**
 *
 * Generated application for tinker life cycle
 *
 */
public class SimpleTinkerInApplication extends TinkerApplication {

    public SimpleTinkerInApplication() {
        super(7, "com.zhy.tinkersimplein.SimpleTinkerInApplicationLike", "com.tencent.tinker.loader.TinkerLoader", false);
    }

}

tinker这么做的目标,文书档案上是那般说的:

为了收缩不当的现身,推荐应用Annotation生成Application类。

如此那般轮廓驾驭了Application是哪些变迁的。

接下去我们大致看一下tinker的原理。

八、TinkerPatchSchemaTask

至关重要施行Runner.tinkerPatch

protected void tinkerPatch() {
    try {
        //gen patch
        ApkDecoder decoder = new ApkDecoder(config);
        decoder.onAllPatchesStart();
        decoder.patch(config.mOldApkFile, config.mNewApkFile);
        decoder.onAllPatchesEnd();

        //gen meta file and version file
        PatchInfo info = new PatchInfo(config);
        info.gen();

        //build patch
        PatchBuilder builder = new PatchBuilder(config);
        builder.buildPatch();

    } catch (Throwable e) {
        e.printStackTrace();
        goToError();
    }
}

主要分为以下环节:

  • 生成patch
  • 生成meta-file和version-file,这里根本就是在assets目录下写一些键值对。(包罗tinkerId以至安顿中configField相关消息)
  • build patch

Tinker中gradle插件的流水生产线

澳门新葡萄京官网首页 10

Tinker中gradle插件专业流程图

简易的流水生产线如下:在fix.apk编译在此之前依据须求修补的base.apk的能源id映射生成map文件;依据base.apk的歪曲法则映射fix.apk的歪曲法则等,以至依照base.apk的multidex的包涵情势编写翻译出fix.apk,最后爆发差分的补丁patch.apk。

四、原理

澳门新葡萄京官网首页 11

来源于:

tinker贴了一张大约的原理图。

能够见见:

tinker将old.apk和new.apk做了diff,获得patch.dex,然后将patch.dex与本机中apk的classes.dex做了合并,生成新的classes.dex,运转时经过反射将联合后的dex文件放置在加载的dexElements数组的先头。

运行时替代的规律,其实和Qzone的方案差不离,都是去反射改正dexElements。

两个的出入是:Qzone是直接将patch.dex插到数组的前头;而tinker是将patch.dex与app中的classes.dex合并后的全量dex插在数组的前方。

tinker这么做的指标依然因为Qzone方案中涉嫌的CLASS_ISPREVERIFIED的缓慢解决方案存在难点;而tinker也正是换个思路解决了该难题。

接下去大家就从代码中去验证该原理。

本片小说源码深入分析的两条线:

  • 利用运维时,从暗中同意目录加载合併后的classes.dex
  • patch下发后,合成classes.dex至指标目录

(1)生成pacth

断章取义就是八个apk相比较去变通各个patch文件,那么从三个apk的组成来看,大概能够分成:

  • dex文件比对的patch文件
  • res文件比对的patch res文件
  • so文件比对生成的so patch文件

看下代码:

public boolean patch(File oldFile, File newFile) throws Exception {
    //check manifest change first
    manifestDecoder.patch(oldFile, newFile);

    unzipApkFiles(oldFile, newFile);

    Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(),
            mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));

    soPatchDecoder.onAllPatchesEnd();
    dexPatchDecoder.onAllPatchesEnd();
    manifestDecoder.onAllPatchesEnd();
    resPatchDecoder.onAllPatchesEnd();

    //clean resources
    dexPatchDecoder.clean();
    soPatchDecoder.clean();
    resPatchDecoder.clean();
    return true;
}

代码内部含有多个Decoder:

  • manifestDecoder
  • dexPatchDecoder
  • soPatchDecoder
  • resPatchDecoder

刚刚提到要求对dex、so、res文件做diff,不过为啥会有个manifestDecoder。近期tinker并不匡助四大组件,也正是说manifest文件中是不容许出现新增添组件的。

所以,manifestDecoder的作用实际上是用以检查的:

  1. minSdkVersion<14时仅同意dexMode使用jar格局(TODO:raw格局的分别是何等?)
  2. 会解析manifest文件,读抽取组大组件实行对照,不容许现身剧增的任何组件。

代码就不贴了那些好驾驭,关于manifest的拆解分析是依照该库封装的:

下一场正是解压四个apk文件了,old apk(大家设置的),old apk 生成的。

解压的目录为:

  • old apk: build/intermediates/outputs/old apk名称/
  • new apk: build/intermediates/outputs/app-debug/

解压完成后,正是单个文件相比较了:

相对来讲的思绪是,以newApk解压目录下全体的文书为尺度,去oldApk中找同名的文本,那么会有以下多少个状态:

  1. 在oldApkDir中没有找到,那么注脚该公文是新添的
  2. 在oldApkDir中找到了,那么比对md5,如若差异,则感觉改造了(则需求基于气象做diff)

有了大意上的垂询后,能够看代码:

Files.walkFileTree(
    mNewApkDir.toPath(), 
    new ApkFilesVisitor(
        config, 
        mNewApkDir.toPath(),
        mOldApkDir.toPath(), 
        dexPatchDecoder, 
        soPatchDecoder, 
        resPatchDecoder));

Files.walkFileTree会以mNewApkDir.toPath()为法规,遍历其内部有着的公文,ApkFilesVisitor中得以对各个遍历的文书举办操作。

重点看ApkFilesVisitor是什么样操作每一种文件的:

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {

    Path relativePath = newApkPath.relativize(file);
    // 在oldApkDir中找到该文件
    Path oldPath = oldApkPath.resolve(relativePath);

    File oldFile = null;
    //is a new file?!
    if (oldPath.toFile().exists()) {
        oldFile = oldPath.toFile();
    }

    String patternKey = relativePath.toString().replace("\", "/");

    if (Utils.checkFileInPattern(config.mDexFilePattern, patternKey)) {
        dexDecoder.patch(oldFile, file.toFile());
    }
    if (Utils.checkFileInPattern(config.mSoFilePattern, patternKey)) {
        soDecoder.patch(oldFile, file.toFile());
    }
    if (Utils.checkFileInPattern(config.mResFilePattern, patternKey)) {
         resDecoder.patch(oldFile, file.toFile());
    }
    return FileVisitResult.CONTINUE;
}

先是去除newApkDir中的叁个文本,在oldApkDir中找找同名的apk;然后根据名称推断该公文属于:

  1. dexFile -> dexDecoder.patch 达成dex文件间的比对
  2. soFile -> soDecoder.patch 达成so文件的比对
  3. resFile -> resDecoder.patch 达成res文件的比对

各个文件的准则是可配置的。

Tinker 中gradle插件的安排性

tinker定义了5个task来达成patch.apk的发生进度。如下图是tinker插件的类图,

澳门新葡萄京官网首页 12

图形引自别处

TinkerManifestTask 主借使在AndroidManifest.xml中插入 meta-data
TINKEPRADO_ID
,它是用来在顾客端归并补丁的时候验证old.apk中的TINKECRUISER_ID是或不是一律。

@(工作-待办)TaskAction
    def updateManifest() {
            String tinkerValue = project.extensions.tinkerPatch.buildConfig.tinkerId
        if (tinkerValue == null || tinkerValue.isEmpty()) {
            throw new GradleException('tinkerId is not set!!!')
        }//校验gradle配置的tinkerId的值

        tinkerValue = TINKER_ID_PREFIX + tinkerValue

        writeManifestMeta(manifestPath, TINKER_ID, tinkerValue)//像AndriodManifest.xml插入Tinkerid
        addApplicationToLoaderPattern()
        File manifestFile = new File(manifestPath)
        if (manifestFile.exists()) {//修改后的AndriodManifest.xml拷贝到appbuildintermediatestinker_intermediates下
            FileOperation.copyFileUsingStream(manifestFile, project.file(MANIFEST_XML))
            project.logger.error("tinker gen AndroidManifest.xml in ${MANIFEST_XML}")
        }

    }

TinkerManifestTask首先依照读取gradle配置的tinkerid,校验是不是合法。然后往AndroidManifest.xm插入tinkerid,并拷贝到appbuildintermediatestinker_intermediates。

TinkerResourceIdTask依据gradle配置的applyResourceMapping文件,产生public.xml和idx.xml,使得new.apk在编译能源文件的时候,依据public.xml钦命的id分配给财富

 def applyResourceId() {
        String resourceMappingFile = project.extensions.tinkerPatch.buildConfig.applyResourceMapping
        if (!FileOperation.isLegalFile(resourceMappingFile)) {
            project.logger.error("apply resource mapping file ${resourceMappingFile} is illegal, just ignore")
            return
        }

       project.extensions.tinkerPatch.buildConfig.usingResourceMapping = true

        PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml)

            FileOperation.copyFileUsingStream(publicFile, project.file(RESOURCE_PUBLIC_XML))

            FileOperation.copyFileUsingStream(idxFile, project.file(RESOURCE_IDX_XML))
       }
    }

TinkerMultidexConfigTask 如果张开了multiDex
会在编写翻译中遵照gradle的配置和暗中同意配置生成出要keep在main
dex中的proguard新闻文件,然后copy出那么些文件,方便开辟者使用multiDexKeepProguard进行配置.首先张开文件并写入暗中同意配置.文件路线也在tinker_intermediates下

 @TaskAction
    def updateTinkerProguardConfig() {
        File file = project.file(MULTIDEX_CONFIG_PATH)
        print(file.getAbsolutePath())
        project.logger.error("try update tinker multidex keep proguard file with ${file}")

        // Create the directory if it doesn't exist already
        file.getParentFile().mkdirs()

        StringBuffer lines = new StringBuffer()
        lines.append("n")
             .append("#tinker multidex keep patterns:n")
             .append(MULTIDEX_CONFIG_SETTINGS)
             .append("n")
             .append("#your dex.loader patterns heren")

        Iterable<String> loader = project.extensions.tinkerPatch.dex.loader
        for (String pattern : loader) {
            if (pattern.endsWith("*")) {
                if (!pattern.endsWith("**")) {
                    pattern += "*"
                }
            }
            lines.append("-keep class " + pattern + " {n" +
                    "    *;n" +
                    "}n")
                    .append("n")
        }

        // Write our recommended proguard settings to this file
        FileWriter fr = new FileWriter(file.path)
        try {
            for (String line : lines) {
                fr.write(line)
            }
        } finally {
            fr.close()
        }

        File multiDexKeepProguard = null
        try {
            multiDexKeepProguard = applicationVariant.getVariantData().getScope().getManifestKeepListProguardFile()
        } catch (Throwable ignore) {
            try {
                multiDexKeepProguard = applicationVariant.getVariantData().getScope().getManifestKeepListFile()
            } catch (Throwable e) {
                project.logger.error("can't find getManifestKeepListFile method, exception:${e}")
            }
        }
        if (multiDexKeepProguard == null) {
            project.logger.error("auto add multidex keep pattern fail, you can only copy ${file} to your own multiDex keep proguard file yourself.")
            return
        }
        FileWriter manifestWriter = new FileWriter(multiDexKeepProguard, true)
        try {
            for (String line : lines) {
                manifestWriter.write(line)
            }
        } finally {
            manifestWriter.close()
        }
    }

TinkerProguardConfigTask虽然张开了模糊,就能够在gradle插件中营造出该职责,首要的意义是将tinker中私下认可的模糊音信和基准包的mapping消息参预混淆列表,那样就足以因此gradle配置活动帮开辟者做一些类的混淆设置,并且能够经过applymapping的基准包的mapping文件达到在混淆上补丁包和标准包一致的指标.首先展开在编译路线下的模糊文件,为后边写入默许的keep准绳做寻思.文件的路径近似在tinker_intermediates下

  @TaskAction
    def updateTinkerProguardConfig() {
        def file = project.file(PROGUARD_CONFIG_PATH)
        project.logger.error("try update tinker proguard file with ${file}")

        // Create the directory if it doesnt exist already
        file.getParentFile().mkdirs()

        // Write our recommended proguard settings to this file
        FileWriter fr = new FileWriter(file.path)

        String applyMappingFile = project.extensions.tinkerPatch.buildConfig.applyMapping

        //write applymapping
        if (shouldApplyMapping && FileOperation.isLegalFile(applyMappingFile)) {
            project.logger.error("try add applymapping ${applyMappingFile} to build the package")
            fr.write("-applymapping " + applyMappingFile)
            fr.write("n")
        } else {
            project.logger.error("applymapping file ${applyMappingFile} is illegal, just ignore")
        }

        fr.write(PROGUARD_CONFIG_SETTINGS)

        fr.write("#your dex.loader patterns heren")
        //they will removed when apply
        Iterable<String> loader = project.extensions.tinkerPatch.dex.loader
        for (String pattern : loader) {
            if (pattern.endsWith("*") && !pattern.endsWith("**")) {
                pattern += "*"
            }
            fr.write("-keep class " + pattern)
            fr.write("n")
        }
        fr.close()
        // Add this proguard settings file to the list
        applicationVariant.getBuildType().buildType.proguardFiles(file)
        def files = applicationVariant.getBuildType().buildType.getProguardFiles()

        project.logger.error("now proguard files is ${files}")
    }

TinkerPatchSchemaTask
肩负校验Extensions的参数和条件是还是不是合法和补丁生成

 @TaskAction
    def tinkerPatch() {
        configuration.checkParameter()
        configuration.buildConfig.checkParameter()
        configuration.res.checkParameter()
        configuration.dex.checkDexMode()
        configuration.sevenZip.resolveZipFinalPath()

        InputParam.Builder builder = new InputParam.Builder()
        if (configuration.useSign) {
            if (signConfig == null) {
                throw new GradleException("can't the get signConfig for this build")
            }
            builder.setSignFile(signConfig.storeFile)
                    .setKeypass(signConfig.keyPassword)
                    .setStorealias(signConfig.keyAlias)
                    .setStorepass(signConfig.storePassword)

        }

        builder.setOldApk(configuration.oldApk)
               .setNewApk(buildApkPath)
               .setOutBuilder(outputFolder)
               .setIgnoreWarning(configuration.ignoreWarning)
               .setDexFilePattern(new ArrayList<String>(configuration.dex.pattern))
               .setDexLoaderPattern(new ArrayList<String>(configuration.dex.loader))
               .setDexMode(configuration.dex.dexMode)
               .setSoFilePattern(new ArrayList<String>(configuration.lib.pattern))
               .setResourceFilePattern(new ArrayList<String>(configuration.res.pattern))
               .setResourceIgnoreChangePattern(new ArrayList<String>(configuration.res.ignoreChange))
               .setResourceLargeModSize(configuration.res.largeModSize)
               .setUseApplyResource(configuration.buildConfig.usingResourceMapping)
               .setConfigFields(new HashMap<String, String>(configuration.packageConfig.getFields()))
               .setSevenZipPath(configuration.sevenZip.path)
               .setUseSign(configuration.useSign)

        InputParam inputParam = builder.create()
        Runner.gradleRun(inputParam);
    }

五、源码剖析

(1)dexDecoder.patch

public boolean patch(final File oldFile, final File newFile)  {
    final String dexName = getRelativeDexName(oldFile, newFile);

    // 检查loader class,省略了抛异常的一些代码
    excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);

    File dexDiffOut = getOutputPath(newFile).toFile();

    final String newMd5 = getRawOrWrappedDexMD5(newFile);

    //new add file
    if (oldFile == null || !oldFile.exists() || oldFile.length() == 0) {
        hasDexChanged = true;
        copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);
        return true;
    }

    final String oldMd5 = getRawOrWrappedDexMD5(oldFile);

    if ((oldMd5 != null && !oldMd5.equals(newMd5)) || (oldMd5 == null && newMd5 != null)) {
        hasDexChanged = true;
        if (oldMd5 != null) {
            collectAddedOrDeletedClasses(oldFile, newFile);
        }
    }

    RelatedInfo relatedInfo = new RelatedInfo();
    relatedInfo.oldMd5 = oldMd5;
    relatedInfo.newMd5 = newMd5;

    // collect current old dex file and corresponding new dex file for further processing.
    oldAndNewDexFilePairList.add(new AbstractMap.SimpleEntry<>(oldFile, newFile));

    dexNameToRelatedInfoMap.put(dexName, relatedInfo);

    return true;
}

先是实践:

checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);

该格局主要用途是检查 tinker loader相关classes**必须存在primary
dex中**,且不许新扩张、改正和删除。

负有首先将八个dex读取到内部存款和储蓄器中,遵照config.mDexLoaderPattern进展过滤,寻找deletedClassInfosaddedClassInfoschangedClassInfosMap,必需确定保证deletedClassInfos.isEmpty() && addedClassInfos.isEmpty() && changedClassInfosMap.isEmpty()即不准新扩充、删除、改革loader
相关类。

继续,取得输出目录:

build/intermediates/outputs/tinker_result/

接下来一旦oldFile子虚乌有,则newFile以为是新添文件,直接copy到输出目录,并记录log

copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);

若果存在,则计算五个文本的md5,假如md5不一致,则感觉dexChanged(hasDexChanged = true),执行:

collectAddedOrDeletedClasses(oldFile, newFile);

该方法搜聚了addClasses和deleteClasses的相干音信,记录在:

  • addedClassDescToDexNameMap key为addClassDesc 和 该dex file的path
  • deletedClassDescToDexNameMap key为deletedClassDesc 和 该dex
    file的path

三番陆次会利用那七个数据构造,mark一下。

接轨往下走,发轫化了叁个relatedInfo记录了五个公文的md5,以致在oldAndNewDexFilePairList中著录了多少个dex
file,在dexNameToRelatedInfoMap中著录了dexName和relatedInfo的映射。

继续会动用该变量,mark一下。

到此,dexDecoder的patch方法就得了了,仅将猛增的文件copy到了目的目录。

这便是说发生变动的文本,理论上理应要做md5看来在背后才会进行。

设若文件是so文件,则会走soDecoder.patch。

总结

  • Tinker
    和任何的类加载格局的修补方案比较,利用差分算法,最大限度的压缩的补丁包的大大小小,这点对于活动选拔来讲非常重大。
  • Tinker的修补包中不可能引进新的第四次全国代表大会组件,无法修正androidManifest文件。

(1)加载patch

加载的代码实际上在转移的Application中调用的,其父类为TinkerApplication,在其attachBaseContext中辗转会调用到loadTinker(卡塔尔方法,在该办法内部,反射调用了TinkerLoader的tryLoad方法。

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

tryLoadPatchFilesInternal中会调用到loadTinkerJars方法:

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传入多少个参数,分别为application,tinkerLoadVerifyFlag(表明上宣示的值,传入为false),patchVersionDirectory当前version的patch文件夹,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;
    }

寻找仅协理art的dex,且当前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'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的得以达成(首要本身看了下本机唯有个22的源码~):

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. 找到PathClassLoader(BaseDexClassLoader)对象中的pathList对象
  2. 基于pathList对象找到个中的makeDexElements方法,传入patch相关的对应的实参,再次回到Element[]对象
  3. 获得pathList对象中原本的dexElements方法
  4. 步骤2与步骤3中的Element[]数组实行合併,将patch相关的dex放在数组的日前
  5. 谈到底将联合后的数组,设置给pathList

此地实在和Qzone的建议的方案基本是一律的。假使您早先未了解过Qzone的方案,能够参照此文:Android
热补丁动态修复框架小结

(2)soDecoder.patch

soDecoder实际上是BsDiffDecoder

@Override
public boolean patch(File oldFile, File newFile)  {
    //new add file
    String newMd5 = MD5.getMD5(newFile);
    File bsDiffFile = getOutputPath(newFile).toFile();

    if (oldFile == null || !oldFile.exists()) {
        FileOperation.copyFileUsingStream(newFile, bsDiffFile);
        writeLogFiles(newFile, null, null, newMd5);
        return true;
    }

    //new add file
    String oldMd5 = MD5.getMD5(oldFile);

    if (oldMd5.equals(newMd5)) {
        return false;
    }

    if (!bsDiffFile.getParentFile().exists()) {
        bsDiffFile.getParentFile().mkdirs();
    }
    BSDiff.bsdiff(oldFile, newFile, bsDiffFile);

    //超过80%,返回false
    if (Utils.checkBsDiffFileSize(bsDiffFile, newFile)) {
        writeLogFiles(newFile, oldFile, bsDiffFile, newMd5);
    } else {
        FileOperation.copyFileUsingStream(newFile, bsDiffFile);
        writeLogFiles(newFile, null, null, newMd5);
    }
    return true;
}

一经oldFile不设有,则感到newFile为新增Gavin件,直接copy到指标文件(连着so相关目录)。

若oldFile存在,则比对二者md5,假如md5不等同,则一贯开展bsdiff算法,直接在对象地方写入bsdiff产生的bsDiffFile。

自然到此相应早已实现了,可是接下去做了一件相当好玩的事:

持续推断了变通的patch文件是不是早就超过newFile的百分之八十,假使跨越十分之九,则一直copy
newFile到指标目录,直接覆盖了刚生成的patch文件。

那么soPatch整个进程:

  1. 假诺是增创文件,直接copy至指标文件夹,记录log
  2. 若果是改动的文书,patch文件超越新文件的七成,则直接copy新文件至指标文件夹,记录log
  3. 假定是改造的文件,patch文件不超越新文件的百分之七十,则copy
    patch文件至指标文件夹,记录log

如果newFile是res 资源,则会走resDecoder

(2)合成patch

这里的入口为:

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

TinkerPatchService是Intent瑟维斯的子类,这里经过intent设置了八个参数,三个是patch的路线,叁个是resultServiceClass,该值是调用Tinker.install的时候设置的,默以为DefaultTinkerResultService.class。由于是IntentService,直接看onHandleIntent就可以,假如你对IntentService不熟悉,能够查看此文:Android
IntentService完全拆解剖析当Service碰到Handler 。

@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方法,调用的是Upgrade帕特ch.try帕特ch。ps:这里有个有意思的地点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。如若有意思味,能够参照此文:有关
Android
进度保活,你所急需知道的成套。

上面继续回来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相比基本的地点,这些算法跟dex文件格式紧凑关系,即使有时机,然后自身又能看懂的话,前边会单独写篇博客介绍。其它dodola已经有篇博客进行了介绍:Tinker
Dexdiff算法解析

感兴趣的能够翻阅下。

好了,到此我们就大约精通了tinker热修复的规律~~

测试demo地址:

本来这里只深入分析了代码了热修复,后续思考分析财富以致So的热修、大旨的diff算法、以至gradle插件等连锁文化~

(3)resDecoder.patch

@Override
public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
    String name = getRelativePathStringToNewFile(newFile);

    File outputFile = getOutputPath(newFile).toFile();

    if (oldFile == null || !oldFile.exists()) {
        FileOperation.copyFileUsingStream(newFile, outputFile);
        addedSet.add(name);
        writeResLog(newFile, oldFile, TypedValue.ADD);
        return true;
    }

    //new add file
    String newMd5 = MD5.getMD5(newFile);
    String oldMd5 = MD5.getMD5(oldFile);

    //oldFile or newFile may be 0b length
    if (oldMd5 != null && oldMd5.equals(newMd5)) {
        return false;
    }
    if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {
        Logger.d("found modify resource: " + name + ", but it match ignore change pattern, just ignore!");
        return false;
    }
    if (name.equals(TypedValue.RES_MANIFEST)) {
        Logger.d("found modify resource: " + name + ", but it is AndroidManifest.xml, just ignore!");
        return false;
    }
    if (name.equals(TypedValue.RES_ARSC)) {
        if (AndroidParser.resourceTableLogicalChange(config)) {
            Logger.d("found modify resource: " + name + ", but it is logically the same as original new resources.arsc, just ignore!");
            return false;
        }
    }
    dealWithModeFile(name, newMd5, oldFile, newFile, outputFile);
    return true;
}

设若oldFile不设有,则感觉新添文件,直接copy且参加到addedSet集合,并记录log

比如存在,且md5差别实验商量dealWithModeFile(设置的sIgnoreChangePattern、MANIFEST和逻辑上相仿的AHavalSC不做拍卖卡塔尔(قطر‎。

private boolean dealWithModeFile(String name, String newMd5, File oldFile, File newFile, File outputFile) {
    if (checkLargeModFile(newFile)) {
        if (!outputFile.getParentFile().exists()) {
            outputFile.getParentFile().mkdirs();
        }
        BSDiff.bsdiff(oldFile, newFile, outputFile);
        //未超过80%返回true
        if (Utils.checkBsDiffFileSize(outputFile, newFile)) {
            LargeModeInfo largeModeInfo = new LargeModeInfo();
            largeModeInfo.path = newFile;
            largeModeInfo.crc = FileOperation.getFileCrc32(newFile);
            largeModeInfo.md5 = newMd5;
            largeModifiedSet.add(name);
            largeModifiedMap.put(name, largeModeInfo);
            writeResLog(newFile, oldFile, TypedValue.LARGE_MOD);
            return true;
        }
    }
    modifiedSet.add(name);
    FileOperation.copyFileUsingStream(newFile, outputFile);
    writeResLog(newFile, oldFile, TypedValue.MOD);
    return false;
}

那边,首先check了largeFile,即改动的文件是还是不是超过100K(该值能够配备)。

假诺非大文件,则直接copy至指标文件,且记录到modifiedSet,并记录了log。

即使是大文件,则一直bsdiff,生成patch File;接下去也检查了一下patch
file是还是不是当先newFile的十分九,借使当先,则一贯copy newFile覆盖刚生成的patch
File;

全部和so patch基本一致。

到此地,除了dex patch中对转移的dex文件未有做拍卖以外,so 和 res都做了。

接下去试行了:

public boolean patch(File oldFile, File newFile) throws Exception {
    //...

    soPatchDecoder.onAllPatchesEnd();
    dexPatchDecoder.onAllPatchesEnd();
    manifestDecoder.onAllPatchesEnd();
    resPatchDecoder.onAllPatchesEnd();

    //clean resources
    dexPatchDecoder.clean();
    soPatchDecoder.clean();
    resPatchDecoder.clean();
    return true;
}

当中dexPatchDecoder和resPatchDecoder有后续实现。

(4) dexPatchDecoder.onAllPatchesEnd

# DexDiffDecoder
@Override
public void onAllPatchesEnd() throws Exception {
    if (!hasDexChanged) {
        Logger.d("No dexes were changed, nothing needs to be done next.");
        return;
    }

    generatePatchInfoFile();

    addTestDex();
}

借使dex文件并未有修正,直接返回。

private void generatePatchInfoFile() throws IOException {
    generatePatchedDexInfoFile();

    logDexesToDexMeta();

    checkCrossDexMovingClasses();
}

主要看generatePatchedDexInfoFile

private void generatePatchedDexInfoFile() {
    // Generate dex diff out and full patched dex if a pair of dex is different.
    for (AbstractMap.SimpleEntry<File, File> oldAndNewDexFilePair : oldAndNewDexFilePairList) {
        File oldFile = oldAndNewDexFilePair.getKey();
        File newFile = oldAndNewDexFilePair.getValue();
        final String dexName = getRelativeDexName(oldFile, newFile);
        RelatedInfo relatedInfo = dexNameToRelatedInfoMap.get(dexName);
        if (!relatedInfo.oldMd5.equals(relatedInfo.newMd5)) {
            diffDexPairAndFillRelatedInfo(oldFile, newFile, relatedInfo);
        } else {
            // In this case newDexFile is the same as oldDexFile, but we still
            // need to treat it as patched dex file so that the SmallPatchGenerator
            // can analyze which class of this dex should be kept in small patch.
            relatedInfo.newOrFullPatchedFile = newFile;
            relatedInfo.newOrFullPatchedMd5 = relatedInfo.newMd5;
        }
    }
}

oldAndNewDexFilePairList中著录了三个dex文件,然后依据dex
file获取到dexName,再由dexNameToRelatedInfoMap依据name得到到RelatedInfo。

RelatedInfo中包罗了五个dex
file的md5,假诺不一样,则实践diffDexPairAndFillRelatedInfo

private void diffDexPairAndFillRelatedInfo(File oldDexFile, 
                        File newDexFile, RelatedInfo relatedInfo) {
    //outputs/tempPatchedDexes
    File tempFullPatchDexPath = new File(config.mOutFolder 
                + File.separator + TypedValue.DEX_TEMP_PATCH_DIR);
    final String dexName = getRelativeDexName(oldDexFile, newDexFile);

    File dexDiffOut = getOutputPath(newDexFile).toFile();
    ensureDirectoryExist(dexDiffOut.getParentFile());

    // dex diff , 去除loader classes
    DexPatchGenerator dexPatchGen = new DexPatchGenerator(oldDexFile, newDexFile);
    dexPatchGen.setAdditionalRemovingClassPatterns(config.mDexLoaderPattern);

    dexPatchGen.executeAndSaveTo(dexDiffOut);

    relatedInfo.dexDiffFile = dexDiffOut;
    relatedInfo.dexDiffMd5 = MD5.getMD5(dexDiffOut);

    File tempFullPatchedDexFile = new File(tempFullPatchDexPath, dexName);

    try {
        new DexPatchApplier(oldDexFile, dexDiffOut).executeAndSaveTo(tempFullPatchedDexFile);

        Logger.d(
                String.format("Verifying if patched new dex is logically the same as original new dex: %s ...", getRelativeStringBy(newDexFile, config.mTempUnzipNewDir))
        );

        Dex origNewDex = new Dex(newDexFile);
        Dex patchedNewDex = new Dex(tempFullPatchedDexFile);
        checkDexChange(origNewDex, patchedNewDex);

        relatedInfo.newOrFullPatchedFile = tempFullPatchedDexFile;
        relatedInfo.newOrFullPatchedMd5 = MD5.getMD5(tempFullPatchedDexFile);
    } catch (Exception e) {
        e.printStackTrace();
        throw new TinkerPatchException(
                "Failed to generate temporary patched dex, which makes MD5 generating procedure of new dex failed, either.", e
        );
    }

    if (!tempFullPatchedDexFile.exists()) {
        throw new TinkerPatchException("can not find the temporary full patched dex file:" + tempFullPatchedDexFile.getAbsolutePath());
    }
    Logger.d("nGen %s for dalvik full dex file:%s, size:%d, md5:%s", dexName, tempFullPatchedDexFile.getAbsolutePath(), tempFullPatchedDexFile.length(), relatedInfo.newOrFullPatchedMd5);
}

最早针对七个dex文件做dex diff,最终将转移的patch
文件放置在指标文件夹中。

接下去,生成一个有时文件夹,通过DexPatchApplier针对生成的patch文件和old
dex
file,直接做了统一操作,也便是在本土模拟实行了在顾客端上的patch操作。

接下来再对新合併生成的patchedNewDex与事情发生此前的origNewDex,实行了checkDexChange,即那四头类品级比较,应该有着的类都未有差距。

提及底在dexDecoder的onAllPatchesEnd中还举行了二个addTestDex

private void addTestDex() throws IOException {
    //write test dex
    String dexMode = "jar";
    if (config.mDexRaw) {
        dexMode = "raw";
    }

    final InputStream is = DexDiffDecoder.class.getResourceAsStream("/" + TEST_DEX_NAME);
    String md5 = MD5.getMD5(is, 1024);
    is.close();

    String meta = TEST_DEX_NAME + "," + "" + "," + md5 + "," + md5 + "," + 0 + "," + 0 + "," + dexMode;

    File dest = new File(config.mTempResultDir + "/" + TEST_DEX_NAME);
    FileOperation.copyResourceUsingStream(TEST_DEX_NAME, dest);
    Logger.d("nAdd test install result dex: %s, size:%d", dest.getAbsolutePath(), dest.length());
    Logger.d("DexDecoder:write test dex meta file data: %s", meta);

    metaWriter.writeLineToInfoFile(meta);
}

copy了一个test.dex文件至指标文件夹,该公文存款和储蓄在tinker-patch-lib的resources文件夹下,重要用于在app上進展测验。

完毕了装有的diff职业后,后边正是生成patch文件了。

(2)打包全体变化的patch文件

//build patch
PatchBuilder builder = new PatchBuilder(config);
builder.buildPatch();

详见代码:

public PatchBuilder(Configuration config) {
    this.config = config;
    this.unSignedApk = new File(config.mOutFolder, PATCH_NAME + "_unsigned.apk");
    this.signedApk = new File(config.mOutFolder, PATCH_NAME + "_signed.apk");
    this.signedWith7ZipApk = new File(config.mOutFolder, PATCH_NAME + "_signed_7zip.apk");
    this.sevenZipOutPutDir = new File(config.mOutFolder, TypedValue.OUT_7ZIP_FILE_PATH);
}

public void buildPatch() throws Exception {
    final File resultDir = config.mTempResultDir;
    //no file change
    if (resultDir.listFiles().length == 0) {
        return;
    }
generateUnsignedApk(unSignedApk);
    signApk(unSignedApk, signedApk);

    use7zApk(signedApk, signedWith7ZipApk, sevenZipOutPutDir);

    if (!signedApk.exists()) {
        Logger.e("Result: final unsigned patch result: %s, size=%d", unSignedApk.getAbsolutePath(), unSignedApk.length());
    } else {
        long length = signedApk.length();
        Logger.e("Result: final signed patch result: %s, size=%d", signedApk.getAbsolutePath(), length);
        if (signedWith7ZipApk.exists()) {
            long length7zip = signedWith7ZipApk.length();
            Logger.e("Result: final signed with 7zip patch result: %s, size=%d", signedWith7ZipApk.getAbsolutePath(), length7zip);
            if (length7zip > length) {
                Logger.e("Warning: %s is bigger than %s %d byte, you should choose %s at these time!",
                    signedWith7ZipApk.getName(),
                    signedApk.getName(),
                    (length7zip - length),
                    signedApk.getName());
            }
        }
    }

}

根本会调换3个文件:unSignedApksignedApk以及signedWith7ZipApk

unSignedApk只要将tinker_result中的文件收缩到一个压缩包就可以。
signedApk将unSignedApk使用jarsigner进行签订公约。

signedWith7ZipApk首即使对signedApk举行解压再做sevenZip压缩。

好了,到此茫茫长的稿子就终止啦~~~

受限于本身知识,文中难免现身错误,能够一贯留言提议。

九、总结

平素关注tinker的翻新,也在项目中对tinker进行了利用与定制,tinker中蕴藏了大气的可学习的学问,项目自己在也保有极其强的价值。

对此tinker的“本领的初志与百折不屈”一文感触颇深,希望tinker越来越好~

能够阅读以下文章,继续刺探tinker~~

  • Tinker:手艺的最初的心意与精卫填海
  • WechatAndroid热补丁施行形成之路
  • Android
    N混合编写翻译与对热补丁影响拆解深入分析
  • Dev Club
    Wechat热补丁Tinker分享
  • WechatTinker的一切都在此,包涵源码(一卡塔尔国
  • Tinker Dexdiff算法深入分析
  • ART下的法子内联计谋及其对Android热修复方案的震慑解析
  • Tinker MDCC会议
    slide
  • DexDiff格式查看工具

发表评论

电子邮件地址不会被公开。 必填项已用*标注