Android热修复Tinker接入实战

自2016年底Android Studio3.0版本退出以来,Android提出了InstantRun热修复方案,基于这种机制,各种热修复框架竞相涌现,国内的软件大厂纷纷开发了自己的热修复框架。对于热修复的更多介绍大家可以通过下面的文章来了解:全面了解Android热修复技术
Android热修复Tinker接入实战
这些框架主要支持的功能如下:
Android热修复Tinker接入实战
这张图漏掉了阿里的Spofix,该框架可以及时更新,由于目前大多数的热修复框架,缺点是收费,可以通过下面文章来详细了解:阿里第三代非侵入式热修复Sophix

本篇要讲的是如何接入微信的热修复框架Tinker,官网接入资料:Tinker接入指南

Tinker接入

目前,Tinker提供了两种接入方式,一种是基于命令行的方式,类似于AndFix的接入方式;一种就是gradle的方式。官方推荐使用gradle方式接入。

添加gradle依赖

在项目的build.gradle中,添加tinker-patch-gradle-plugin的依赖。

buildscript {
    dependencies {
        classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1')
    }
}

然后,在app的gradle文件app/build.gradle,添加tinker的库依赖以及apply tinker的gradle插件。

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

完善gradle配置

添加完Tinker依赖以后,还需要在gradle文件中做以下配置。

  1. 开启Multidex;
  2. 配置签名文件,方便打包调试;
  3. 引入另一个gradle文件专门来对Tinker生成拆分包的配置。
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

apply plugin: 'com.tencent.tinker.patch'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.xzh.demo"
        minSdkVersion 21
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    dexOptions {
        jumboMode = true
    }
    signingConfigs {
        debug {
            keyAlias 'alias'
            keyPassword '123456'
            storeFile file("../tinker.keystore")
            storePassword '123456'
        }
        release {
            keyAlias 'alias'
            keyPassword '123456'
            storeFile file("../tinker.keystore")
            storePassword '123456'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'


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

// 加入Tinker生成补丁包的gradle
apply from: 'tinker.gradle'

其中,buildTinker.gradle是专门为Tinker配置和生成拆分包而写的,具体可以参考官方gradle配置。

由于生成拆分包的时候会涉及到文件的读写权限,所以需要在Manifest中添加如下权限。

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

gradle参数详解

我们将原apk包称为基准apk包,tinkerPatch直接使用基准apk包与新编译出来的apk包做差异,得到最终的补丁包。gradle文件配置涉及到的常见参数包含。

  • tinkerPatch:全局信息相关的配置项;
  • tinkerEnable:是否打开tinker的功能;
  • oldApk:基准apk包的路径,必须输入,否则会报错;
  • newApk:选填,用于编译补丁apk路径。如果路径合法,即不再编译新的安装包,使用oldApk与newApk直接编译;
  • outputFolder null:选填参数,设置编译输出路径。默认输出路径为build/outputs/tinkerPatch中;
  • ignoreWarning:如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。因为这些情况可能会导致编译出来的patch包带来风险:
  • minSdkVersion小于14,但是dexMode的值为"raw";
  • 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
  • 定义在dex.loader用于加载补丁的类不在main dex中;
  • 定义在dex.loader用于加载补丁的类出现修改;
  • resources.arsc改变,但没有使用applyResourceMapping编译。
  • applyMapping:可选参数,在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。这个只是推荐设置,不设置applyMapping也不会影响任何的assemble编译。
  • applyResourceMapping:可选参数,在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常。
  • tinkerId:在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。
  • keepDexApply:如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
  • isProtectedApp:是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。

当然,还有很多其他的参数属性,可以通过build.gradle配置属性来获取详情。

Tinker使用

自定义Tinker封装

为了方便操作了管理,我们还需要自定义对Tinker进行一些简单的封装。该类涉及的代码如下:

public class TinkerManager {

    private static boolean isInstalled = false;
    //ApplicationLike可以理解为Application的载体,可以当成Application去使用
    private static ApplicationLike mAppLike;
    private static SimplePatchListener mPatchListener;

    /**
     * 初始化Tinker
     * @param applicationLike
     */
    public static void installTinker(ApplicationLike applicationLike) {
        mAppLike = applicationLike;
        if (isInstalled) {
            return;
        }
        TinkerInstaller.install(mAppLike);
        isInstalled = true;
    }

    /**
     * 初始化Tinker,带有拓展模块
     * @param applicationLike
     * @param md5Value        服务器下发的md5
     */
    public static void installTinker(ApplicationLike applicationLike, String md5Value) {
        mAppLike = applicationLike;
        if (isInstalled) {
            return;
        }
        mPatchListener = new SimplePatchListener(getApplicationContext());
        mPatchListener.setCurrentMD5(md5Value);
        // Load补丁包时候的监听
        LoadReporter loadReporter = new DefaultLoadReporter(getApplicationContext());
        // 补丁包加载时候的监听
        PatchReporter patchReporter = new DefaultPatchReporter(getApplicationContext());
        AbstractPatch upgradePatchProcessor = new UpgradePatch();
        TinkerInstaller.install(applicationLike,
                loadReporter,
                patchReporter,
                mPatchListener,
                SimpleResultService.class,
                upgradePatchProcessor);
        isInstalled = true;
    }

    /**
     * 添加补丁包路径
     * @param path
     */
    public static void addPatch(String path) {
        if (Tinker.isTinkerInstalled()) {
            TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);
        }
    }


    private static Context getApplicationContext() {
        if (mAppLike != null) {
            return mAppLike.getApplication().getApplicationContext();
        }
        return null;
    }

}

由于Tinker默认Patch检查是没有对文件做Md5校验,所以如果需要可以重写其中进行检验相关的逻辑。

public class SimplePatchListener extends DefaultPatchListener {

    private String currentMD5;
    public void setCurrentMD5(String md5Value) {
        this.currentMD5 = md5Value;
    }

    public SimplePatchListener(Context context) {
        super(context);
    }

    @Override
    protected int patchCheck(String path, String patchMd5) {
        //增加patch文件的md5较验
        if (!MD5Utils.isFileMD5Matched(path, currentMD5)) {
            return ShareConstants.ERROR_PATCH_DISABLE;
        }
        return super.patchCheck(path, patchMd5);
    }
}

当补丁包完成替换安装之后,删除补丁包,然后杀掉进程,我们可以根据实际情况修改修复结果操作。

public class CustomResultService extends DefaultTinkerResultService {

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

    /**
     * patch文件的最终安装结果,默认是安装完成后杀掉自己进程
     * 此段代码主要是复制DefaultTinkerResultService的代码逻辑
     */
    @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) {
            //删除patch包
            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!");
            }
        }
    }
}

不过上面的东西不做定制开发,用不到,只需要按照下面的步骤即可。

Tinker接入

正常情况下,我们会考虑在Application的onCreate函数中去初始化Tinker相关的内容,不过Tinker更推荐下面的写法。

@DefaultLifeCycle(application = ".SimpleTinkerApplication", flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false)
public class SimpleTinkerLike extends ApplicationLike {

    public SimpleTinkerLike(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);
    }
}

ApplicationLike,通过名字你可能会猜到,该类并非Application的子类,而是一个类似Application的类。Tinker建议编写一个ApplicationLike子类,可以理解为Application的载体,可以当成Application去使用。
注意顶部的注解:@DefaultLifeCycle。其application属性,会在编译期生成一个SimpleTinkerInApplication类。该类需要在Manifest中替换我们的默认的Application。

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

如果报错,请重新编译一下。关于这方面的内容可以查看下面的文章链接:
Android 如何编写基于编译时注解的项目
为了方便,我们在主页面按钮的点击事件,来加载放在缓存目录下的补丁包,代码如下:

public class MainActivity extends AppCompatActivity {

    private String mPath;

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

        mPath = getExternalCacheDir().getAbsolutePath() + File.separatorChar;
    }

    /**
     * 加载Tinker补丁
     *
     * @param view
     */
    public void Fix(View view) {
        File patchFile = new File(mPath, "patch_signed.apk");
        if (patchFile.exists()) {
            TinkerManager.addPatch(patchFile.getAbsolutePath());
            Toast.makeText(this, "File Exists,Please wait a moment ", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "File No Exists", Toast.LENGTH_SHORT).show();
        }
    }
}

测试

为了验证热修复的效果,我们在MainActivity中新增一个按钮,并增加一个ImageView图像。
然后,找到gradle工具栏(Android Studio的右上角),点击生成Release包,作为1.0版本的程序。
Android热修复Tinker接入实战
在项目的build文件夹下bakAPK(该文件夹是在tink.gradle文件中设置的)文件夹下回看到编译成功的apk文件。
Android热修复Tinker接入实战
将apk安装到手机上,该apk可以认为是old.apk。启动apk看到的效果如下:
Android热修复Tinker接入实战

2,然后在主界面添加加载图片的按钮,同时添加一个drawable文件。

public class MainActivity extends AppCompatActivity {

    private String mPath;
    private ImageView iv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //新增
        iv = (ImageView) findViewById(R.id.iv);

        mPath = getExternalCacheDir().getAbsolutePath() + File.separatorChar;
    }

    /**
     * 加载Tinker补丁
     * @param view
     */
    public void Fix(View view) {
        File patchFile = new File(mPath, "patch_signed.apk");
        if (patchFile.exists()) {
            TinkerManager.addPatch(patchFile.getAbsolutePath());
            Toast.makeText(this, "File Exists,Please wait a moment ", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "File No Exists", Toast.LENGTH_SHORT).show();
        }
    }

    /**
     * 新增的按钮点击事件
     * @param view
     */
    public void Load(View view) {
        iv.setImageResource(R.drawable.bg_content_header);
    }

}

同时记得修改buildTinker.gradle的old安装包的路径,Tinker需要比对前后安装包然后生成补丁包。例如:

ext {
    //开启Tinker
    tinkerEnable = true
    //旧的apk位置,需要我们手动指定
    tinkerOldApkPath = "${bakPath}/app-2018-05-04-17-00-19"
    //旧的混淆映射位置,如果开启了混淆,则需要我们手动指定
    tinkerApplyMappingPath = "${bakPath}/app-2018-05-04-17-00-19"
    //旧的resource位置,需要我们手动指定
    tinkerApplyResourcePath = "${bakPath}/app-2018-05-04-17-00-19"
    //旧的多渠道位置,需要我们手动指定
    tinkerBuildFlavorDirectory = "${bakPath}/app-2018-05-04-17-00-19"
    appKey = "0481b2ba9d770294"
    tinkerID = "1.0"
}

找到gradle工具栏,点击Tinker生成Release补丁包,作为1.0版本的补丁。
Android热修复Tinker接入实战
然后将生成的Release补丁包Push到手机的缓存目录上,运行程序点击修复补丁包,稍等数秒程序会被杀掉,重启点击加载图片按钮。使用Tinker的一个缺点是修复的程序必须重启才能执行。生成的补丁包的位置如下:
Android热修复Tinker接入实战

生成的补丁patch_signed.apk放到手机的包名的cache文件夹下(可以使用应用宝等工具)。例如:
Android热修复Tinker接入实战

然后重启应用,就发现应用加载了差分包的内。
Android热修复Tinker接入实战

当然,本文讲解的只是本地的热修复功能,更多的时候我们会需要将差分包放到服务端,然后由服务器控制热更新。后台管理界面如下:
Android热修复Tinker接入实战
更多内容,请查看Android 热更新服务平台相关的介绍。
除此之外,Tinker还支持Tinker多渠道打包功能。

附:[源码](https://download.csdn.net/download/xiangzhihong8/10392663)
上一篇:Android性能分析工具简介


下一篇:当数据中心碰上云计算