集成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核心原理

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

布局文件

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,安装到手机上,保留基础包

修改布局文件,打包新的apk,

新的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

 

 在studio的terminal面板中输入命令adb push 将补丁文件push到手机

在手机的文件夹中生成了补丁文件

点击加载补丁包,发现新添加的内容显示出来了,完美!!!

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下载地址

猜你喜欢

转载自blog.csdn.net/jingerlovexiaojie/article/details/107197864