Android热修复Tinker接入实战详解手机开发

自2016年底Android Studio3.0版本退出以来,Android提出了InstantRun热修复方案,基于这种机制,各种热修复框架竞相涌现,国内的软件大厂纷纷开发了自己的热修复框架。对于热修复的更多介绍大家可以通过下面的文章来了解:全面了解Android热修复技术
这里写图片描述
这些框架主要支持的功能如下:
这里写图片描述
这张图漏掉了阿里的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版本的程序。
这里写图片描述
在项目的build文件夹下bakAPK(该文件夹是在tink.gradle文件中设置的)文件夹下回看到编译成功的apk文件。
这里写图片描述
将apk安装到手机上,该apk可以认为是old.apk。启动apk看到的效果如下:
这里写图片描述

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版本的补丁。
这里写图片描述
然后将生成的Release补丁包Push到手机的缓存目录上,运行程序点击修复补丁包,稍等数秒程序会被杀掉,重启点击加载图片按钮。使用Tinker的一个缺点是修复的程序必须重启才能执行。生成的补丁包的位置如下:
这里写图片描述

生成的补丁patch_signed.apk放到手机的包名的cache文件夹下(可以使用应用宝等工具)。例如:
这里写图片描述

然后重启应用,就发现应用加载了差分包的内。
这里写图片描述

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

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/tech/app/5852.html

(0)
上一篇 2021年7月17日 00:21
下一篇 2021年7月17日 00:21

相关推荐

发表回复

登录后才能评论