天天看點

內建Tinker熱修複

Tinker是什麼

Tinker是微信官方的Android熱更新檔解決方案,它支援動态下發代碼、So庫以及資源,讓應用能夠在不需要重新安裝的情況下實作更新。當然,你也可以使用Tinker來更新你的插件。

它主要包括以下幾個部分:

  1. gradle編譯插件: 

    tinker-patch-gradle-plugin

  2. 核心sdk庫: 

    tinker-android-lib

  3. 非gradle編譯使用者的指令行版本: 

    tinker-patch-cli.jar

為什麼使用Tinker

目前市面的熱更新檔方案有很多,其中比較出名的有阿裡的AndFix、美團的Robust以及QZone的超級更新檔方案。但它們都存在無法解決的問題,這也是正是我們推出Tinker的原因。

內建Tinker熱修複

 Tinker核心原理

  1. 基于android原生的ClassLoader,開發了自己的ClassLoader
  2. 基于android原生的aapt,開發了自己的aapt
  3. 微信團隊自己基于Dex檔案的格式,研發了DexDiff算法

使用Tinker完成bug修複

在app的gradle檔案app/build.gradle,我們需要添加tinker的庫依賴

如果Gradle版本是3.0 (com.android.tools.build:gradle:3.0.0' )或者以上是的話配置

//tinker的核心庫
    api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
    implementation("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }
    annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    implementation "com.android.support:multidex:1.0.1"
           

 gradle版本是3.0以下的配置

dependencies {
  //tinker的核心庫
  implementation("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
   //可選,用于生成application類 
   provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
}
           
implementation 的作用是編譯的時候使用,把庫打包到apk中

provided 的作用是隻參與編譯,不參與把庫打包到apk中,減小apk包體積
           

gradle.properties配置公共參數

android.enableJetifier=true
TINKER_VERSION=1.9.14.5
android.enableR8 = false
           
public class TinkerManager {
    private static boolean isInstalled = false;
    private static ApplicationLike mApplike;//委托類

    /**
     * Tinker的初始化
     * @param applicationLike
     */
    public static void installTinker(CustomTinkerLike applicationLike) {
        mApplike = applicationLike;
        if (isInstalled) {
            return;
        } else {
            TinkerInstaller.install(mApplike);//完成Tinker的初始化
            isInstalled = true;
        }
    }

    //完成Patch檔案的加載
    public static void loadPatch(String path) {
        if (Tinker.isTinkerInstalled()) {
            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),path);
        }
    }

    //通過ApplicationContext擷取Context
    private static Context getApplicationContext() {
        if (mApplike != null) {
            return mApplike.getApplication().getApplicationContext();
        }
        return null;
    }

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


    public CustomTinkerLike(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);
        //使應用支援分包
        MultiDex.install(base);

        TinkerManager.installTinker(this);

    }
}
           

Tinker需要監聽Application對象的周期,通過ApplicationLike 進行委托,通過這個委托,可以在ApplicationLike完成對Application對象的周期的監聽,在不同的生命周期階段,完成不同的操作。

build一下就會生成MyTinkerApplication,在清單檔案裡面使用下。

<application
        android:name=".MyTinkerApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
           

TInker的內建和初始化就完成了

public class MainActivity extends AppCompatActivity {
    private static final String FILE_END = ".apk";
    private String mPatchDir;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.loadPatch).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                loadPatch();
            }
        });

        mPatchDir = getExternalCacheDir().getAbsolutePath() + "/tpatch/";
        //是為了建立我們的檔案夾
        File file = new File(mPatchDir);
        if (file == null || !file.exists()) {
            file.mkdir();
        }
    }


    public void loadPatch() {
        TinkerManager.loadPatch(getPatchName());
    }

    private String getPatchName() {
        return mPatchDir.concat("patch_signed").concat(FILE_END);
    }
}
           

布局檔案

內建Tinker熱修複

patch生成方式

  • 使用 指令行的方式完成Patch包的生成(不介紹了)
  • 使用Gradle插件的方式完成Patch包的生成

內建插件

在項目的build.gradle中,添加

tinker-patch-gradle-plugin

的依賴

buildscript {
    dependencies {
         classpath("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}")
    }
}
           

在Gradle中配置生成patch檔案

  • 在Gradle中正确配置tinker參數
  • 在Android Studio中直接生成patch檔案

 在Gradle中配置tinker參數

apply plugin: 'com.android.application'
def javaVersion = JavaVersion.VERSION_1_7

android {
    signingConfigs {
        release {
            storeFile file('D:\\MyDownload\\TinkerDemo\\app\\newSign.jks')
            storePassword '888888'
            keyAlias = 'tinker'
            keyPassword '888888'
        }
    }
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    compileOptions {
        sourceCompatibility javaVersion
        targetCompatibility javaVersion
    }

    dexOptions {
        jumboMode = true
    }

    defaultConfig {
        applicationId "com.example.tinkerdemo"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        multiDexEnabled true
        multiDexKeepProguard file("tinker_multidexkeep.pro")

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
    }
    buildTypes {
        release {
            minifyEnabled = true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    //tinker的核心庫
    api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
    implementation("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }
    annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
    implementation "com.android.support:multidex:1.0.1"
}


def bakPath = file("${buildDir}/bakApk/")

ext {
    tinkerEnanle = true
    tinkerOldApkPath = "${bakPath}/app-release-0714-14-29-57.apk"
    tinkerId = "1.0"
    tinkerApplyMappingPath = "${bakPath}/app-release-0714-14-29-57-mapping.text"
    tinkerApplyResourcePath = "${bakPath}/app-release-0714-14-29-57-R.text"
}

def buildWithTinker() {
    return ext.tinkerEnanle
}

def getOldApkPath() {
    return ext.tinkerOldApkPath
}

def getApplyMappingPath() {
    return ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
    return ext.tinkerApplyResourcePath
}

def getTinkerIdValue() {
    return ext.tinkerId
}

if (buildWithTinker()) {
    //啟用tinker
    apply plugin: 'com.tencent.tinker.patch'

    //所有tinker相關的參數配置
    tinkerPatch {

        oldApk = getOldApkPath() //指定old apk檔案

        ignoreWarning = false //不忽略tinker的警告,有則中止patch檔案的生成

        useSign = true//強制patch檔案也使用簽名

        tinkerEnanle = buildWithTinker()//指定是否啟用Tinker

        buildConfig {

            applyMapping = getApplyMappingPath()//指定old apk 打包時所使用的混淆檔案

            applyResourceMapping = getApplyResourceMappingPath()//指定old apk所使用的資源檔案

            tinkerId = getTinkerIdValue()//指定TinkerID,每個patch檔案的唯一辨別符

            keepDexApply = false

            isProtectedApp = false

            supportHotplugComponent = false

        }

        dex {
            dexMode = "jar" //jar、raw

            pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] //指定dex檔案目錄

            loader = ["com.example.tinkerdemo.MyTinkerApplication"]//加載patch包所用的類
        }

        lib {
            pattern = ["lib/*/*.so"]
        }

        res {
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
//指定tinker可以修改的所有資源路徑

            ignoreChange = ["assets/sample_meta.txt"]//修改了也不想patch包裡生效

            largeModSize = 100 //資源修改大小的預設值
        }

        packageConfig {
            configField("patchMessage", "fix the 1.0 version's bugs")

            configField("patchVersion", "1.0")

        }
    }
}
           

 注意事項1:打開混淆  minifyEnabled true,要不然不能生産mapping檔案

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

 注意事項2:tinker相關的參數配置的時候要把自動生成的MyTinkerApplication填進去

loader = ["com.example.tinkerdemo.MyTinkerApplication"]//加載patch包所用的類
           

添加混淆 

# tinker混淆規則
-keepattributes SourceFile,LineNumberTable

-dontwarn com.google.**

-dontwarn com.android.**
           

接下來,ext裡面的檔案路徑還沒有指定。可以引入腳本,自動将檔案儲存到bakPath中 

List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0
    def date = new Date().format("MMdd-HH-mm-ss")
 /**
     * 複制基準包和其他必須檔案到指定目錄
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name

        tasks.all {
            if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {

                it.doLast {
                    copy {
                        def fileNamePrefix = "${project.name}-${variant.baseName}"
                        def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"

                        def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath

                        if (variant.metaClass.hasProperty(variant, 'packageApplicationProvider')) {
                            def packageAndroidArtifact = variant.packageApplicationProvider.get()
                            if (packageAndroidArtifact != null) {
                                try {
                                    from new File(packageAndroidArtifact.outputDirectory.getAsFile().get(), variant.outputs.first().apkData.outputFileName)
                                } catch (Exception e) {
                                    from new File(packageAndroidArtifact.outputDirectory, variant.outputs.first().apkData.outputFileName)
                                }
                            } else {
                                from variant.outputs.first().mainOutputFile.outputFile
                            }
                        } else {
                            from variant.outputs.first().outputFile
                        }

                        into destPath
                        rename { String fileName ->
                            fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
                        }

                        from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
                        }

                        from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
                        from "${buildDir}/intermediates/symbol_list/${variant.dirName}/R.txt"
                        from "${buildDir}/intermediates/runtime_symbol_list/${variant.dirName}/R.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
                        }
                    }
                }
            }
        }
    }
           
這裡是可以從官方文檔複制
           

準備階段

  • build一個old apk 并安裝到手機
  • 修改一些功能後,build一個new apk

打包apk,安裝到手機上,保留基礎包

內建Tinker熱修複

修改布局檔案,打包新的apk,

內建Tinker熱修複

新的apk生成以後,我們要将ext裡面的檔案路徑補充完整,這個是剛剛我們基礎包的檔案路徑,不要弄錯。

ext {
    tinkerEnanle = true
    tinkerOldApkPath = "${bakPath}/app-release-0709-15-46-10.apk"
    tinkerId ="1.0"
    tinkerApplyMappingPath = "${bakPath}/app-release-0709-15-46-10-mapping.txt"
    tinkerApplyResourcePath ="${bakPath}/app-release-0709-15-46-10-R.text"
}
           

  點選tinkerPatchRelease就可以生成patch

內建Tinker熱修複
內建Tinker熱修複
內建Tinker熱修複

 在studio的terminal面闆中輸入指令adb push 将更新檔檔案push到手機

內建Tinker熱修複

在手機的檔案夾中生成了更新檔檔案

內建Tinker熱修複

點選加載更新檔包,發現新添加的内容顯示出來了,完美!!!

內建Tinker熱修複

tinker自定義行為

主要是自定義patch安裝以後的行為,tinker預設實作是殺掉目前應用的程序,app閃退,第二次啟動修複了bug,我們進行優化,不要讓app閃退

public void onPatchResult(PatchResult result) {
        if (result == null) {
            TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");
            return;
        }
        TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());

        //first, we want to kill the recover process
        TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());

        // if success and newPatch, it is nice to delete the raw file, and restart at once
        // only main process can load an upgrade patch!
        if (result.isSuccess) {
            deleteRawPatchFile(new File(result.rawPatchFilePath));
            if (checkIfNeedKill(result)) {
                android.os.Process.killProcess(android.os.Process.myPid());
            } else {
                TinkerLog.i(TAG, "I have already install the newly patch version!");
            }
        }
    }
           
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
           

patch加載完以後,會删除手機裡的patch包,節省手機的空間

android.os.Process.killProcess(android.os.Process.myPid());
           

 這個是殺掉目前的線程,導緻app閃退,app的體驗非常不好

我們需要重寫這個方法

​
public class CustomResultSerice extends DefaultTinkerResultService {

    private static final String TAG ="Tinker.SampleResultService";

    //傳回patch檔案的安裝結果
    @Override
    public void onPatchResult(PatchResult result) {
        if (result == null) {
            TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");
            return;
        }
        TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());

        //first, we want to kill the recover process
        TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());

        // if success and newPatch, it is nice to delete the raw file, and restart at once
        // only main process can load an upgrade patch!
        if (result.isSuccess) {
            deleteRawPatchFile(new File(result.rawPatchFilePath));
        }
    }
}

​
           

這樣自定義以後,app不會閃退,加載完patch包以後,第二次啟動的時候生效。

public class TinkerManager {
    private static boolean isInstalled = false;
    private static ApplicationLike mApplike;//委托類

    /**
     * Tinker的初始化
     * @param applicationLike
     */
    public static void installTinker(ApplicationLike applicationLike) {
        mApplike = applicationLike;
        if (isInstalled) {
            return;
        } else {
            LoadReporter loadReporter = new DefaultLoadReporter(applicationLike.getApplication());//日志上報
            PatchReporter patchReporter = new DefaultPatchReporter(applicationLike.getApplication());//日志上報
            DefaultPatchListener mPatchListener = new DefaultPatchListener(applicationLike.getApplication());
            AbstractPatch upgradePatchProcessor = new UpgradePatch();

            TinkerInstaller.install(applicationLike, loadReporter, patchReporter, mPatchListener, CustomResultSerice.class, upgradePatchProcessor);
            isInstalled = true;
        }
    }

    
    //完成Patch檔案的加載
    public static void loadPatch(String path) {
        if (Tinker.isTinkerInstalled()) {
            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),path);
        }
    }

    //通過ApplicationContext擷取Context
    private static Context getApplicationContext() {
        if (mApplike != null) {
            return mApplike.getApplication().getApplicationContext();
        }
        return null;
    }

}
           

注意再manifest注冊這個service

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.tinkerdemo">

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

    <application
        android:name=".MyTinkerApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".CustomResultSerice"
            android:permission="android.permission.BIND_JOB_SERVICE"
            android:exported="false"/>

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.example.tinkerdemo.fileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>

    </application>

</manifest>
           

在這個過程中如果發現onPatchResult的result.isSuccess一直傳回false,那麼在清單檔案裡面加上provider的代碼

provider_paths.xml内容如下

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- /storage/emulated/0/Download/${applicationId}/.beta/apk-->
    <external-path name="beta_external_path" path="Download/"/>
    <!--/storage/emulated/0/Android/data/${applicationId}/files/apk/-->
    <external-path name="beta_external_files_path" path="Android/data/"/>
</paths>
           

總結,tinker不僅适用于bug修複,也适用于小功能的添加。

demo下載下傳位址