热修复——Tinker的集成与使用

1、简述

Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用可以在不须要从新安装的状况下实现更新。固然,你也可使用Tinker来更新你的插件。java

上面是Tinker官方Wiki的原话,意思嘛相信你们都看得明白,但注意啦,它并无说Tinker可让补丁实时生效(也叫无感知更新),它必须在打上补丁后重启App(重启进程),补丁才会发挥做用,这跟阿里的热修复方案有着本质的区别。在开始集成Tinker以前,咱们有必要了解清楚,Tinker有那些不足,下面是Tinker的已知问题:android

  1. Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件(1.9.0支持新增非export的Activity);
  2. 因为Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
  3. 在Android N上,补丁对应用启动时间有轻微的影响;
  4. 不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed";
  5. 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。

上述不足是因为原理与系统限制,咱们在编程中要清楚这些,尽可能避免以上问题的出现。git

尽管Tinker有着这些“小缺点”,但也丝绝不影响Tinker在国内众多热修复方案中的地位,一方面Tinker是开源的(这意味着Tinker自己免费),另外一方面则是Tinker已运行在微信的数亿Android设备上(说明该方案至关稳定)。下面开始进行对Tinker的集成与使用。github

2、Tinker组件依赖

一、在项目的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须要使用到MulitDex,原话在Bugly文档的热更新API接口部分编程

1)添加tinker的库依赖

Gradle版本小于2.3的这么写:api

dependencies {
	compile "com.android.support:multidex:1.0.1"
	//可选,用于生成application类 
	provided('com.tencent.tinker:tinker-android-anno:1.9.1')
	//tinker的核心库
	compile('com.tencent.tinker:tinker-android-lib:1.9.1') 
}
复制代码

Gradle版本大等于2.3的这么写:安全

dependencies {
    implementation "com.android.support:multidex:1.0.1"
    //tinker的核心库
    implementation("com.tencent.tinker:tinker-android-lib:1.9.1") { changing = true }
    //可选,用于生成application类
    annotationProcessor("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }
    compileOnly("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }
}
复制代码

2)开启multiDex

defaultConfig {
		...
        multiDexEnabled true
}
复制代码

3)应用tinker的gradle插件

这部分可先无论,在第三部分《Tinker的配置及任务》的第2节《配置Tinker与任务》中会添加。可跳过这部分继续往下看。服务器

//apply tinker插件
apply plugin: 'com.tencent.tinker.patch
复制代码

3、Tinker的配置及任务

一、开启支持大工程模式

Tinker文档中推荐将jumboMode设置为true。微信

android {
    dexOptions {
        // 支持大工程模式
        jumboMode = true
    }
	...
}
复制代码

二、配置Tinker与任务

将下面的配置所有复制粘贴到app的gradle文件(app/build.gradle)末尾,内容不少,但如今只须要看懂bakPath与ext括号内的东东就行了。

// Tinker配置与任务
def bakPath = file("${buildDir}/bakApk/")
ext {
    // 是否使用Tinker(当你的项目处于开发调试阶段时,能够改成false)
    tinkerEnabled = true
    // 基础包文件路径(名字这里写死为old-app.apk。用于比较新旧app以生成补丁包,无论是debug仍是release编译)
    tinkerOldApkPath = "${bakPath}/old-app.apk"
    // 基础包的mapping.txt文件路径(用于辅助混淆补丁包的生成,通常在生成release版app时会使用到混淆,因此这个mapping.txt文件通常只是用于release安装包补丁的生成)
    tinkerApplyMappingPath = "${bakPath}/old-app-mapping.txt"
    // 基础包的R.txt文件路径(若是你的安装包中资源文件有改动,则须要使用该R.txt文件来辅助生成补丁包)
    tinkerApplyResourcePath = "${bakPath}/old-app-R.txt"
    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/flavor"
}

def getOldApkPath() {
    return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}

def getApplyMappingPath() {
    return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
    return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}

def getTinkerIdValue() {
    return hasProperty("TINKER_ID") ? TINKER_ID : android.defaultConfig.versionName
}

def buildWithTinker() {
    return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

if (buildWithTinker()) {
    //apply tinker插件
    apply plugin: 'com.tencent.tinker.patch'

    // 全局信息相关的配置项
    tinkerPatch {
        tinkerEnable = buildWithTinker()// 是否打开tinker的功能。
        oldApk = getOldApkPath()        // 基准apk包的路径,必须输入,不然会报错。
        ignoreWarning = false           // 是否忽略有风险的补丁包。这里选择不忽略,当补丁包风险时会中断编译。
        useSign = true                  // 在运行过程当中,咱们须要验证基准apk包与补丁包的签名是否一致,咱们是否须要为你签名。
        // 编译相关的配置项
        buildConfig {
            applyMapping = getApplyMappingPath()
            // 可选参数;在编译新的apk时候,咱们但愿经过保持旧apk的proguard混淆方式,从而减小补丁包的大小。这个只是推荐设置,不设置applyMapping也不会影响任何的assemble编译。
            applyResourceMapping = getApplyResourceMappingPath()
            // 可选参数;在编译新的apk时候,咱们但愿经过旧apk的R.txt文件保持ResId的分配,这样不只能够减小补丁包的大小,同时也避免因为ResId改变致使remote view异常。
            tinkerId = getTinkerIdValue()
            // 在运行过程当中,咱们须要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,通常来讲咱们可使用git版本号、versionName等等。
            keepDexApply = false
            // 若是咱们有多个dex,编译补丁时可能会因为类的移动致使变动增多。若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
            isProtectedApp = false // 是否使用加固模式,仅仅将变动的类合成补丁。注意,这种模式仅仅能够用于加固应用中。
            supportHotplugComponent = false // 是否支持新增非export的Activity(1.9.0版本开始才有的新功能)
        }
        // dex相关的配置项
        dex {
            dexMode = "jar"
// 只能是'raw'或者'jar'。 对于'raw'模式,咱们将会保持输入dex的格式。对于'jar'模式,咱们将会把输入dex从新压缩封装到jar。若是你的minSdkVersion小于14,你必须选择‘jar’模式,并且它更省存储空间,可是验证md5时比'raw'模式耗时。默认咱们并不会去校验md5,通常状况下选择jar模式便可。
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            // 须要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如assets/...
            loader = [
                    // 定义哪些类在加载补丁包的时候会用到。这些类是经过Tinker没法修改的类,也是必定要放在main dex的类。
                    // 若是你自定义了TinkerLoader,须要将它以及它引用的全部类也加入loader中;
                    // 其余一些你不但愿被更改的类,例如Sample中的BaseBuildInfo类。这里须要注意的是,这些类的直接引用类也须要加入到loader中。或者你须要将这个类变成非preverify。
            ]
        }
        // 	lib相关的配置项
        lib {
            pattern = ["lib/*/*.so","src/main/jniLibs/*/*.so"]
            // 须要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...
        }
        // res相关的配置项
        res {
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            // 须要处理res路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...,务必注意的是,只有知足pattern的资源才会放到合成后的资源包。
            ignoreChange = [
                    // 支持*、?通配符,必须使用'/'分割。若知足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。 最极端的状况,ignoreChange与上面的pattern一致,即会彻底忽略全部资源的修改。
                    "assets/sample_meta.txt"
            ]
            largeModSize = 100
            // 对于修改的资源,若是大于largeModSize,咱们将使用bsdiff算法。这能够下降补丁包的大小,可是会增长合成时的复杂度。默认大小为100kb
        }
        // 用于生成补丁包中的'package_meta.txt'文件
        packageConfig {
            // configField("key", "value"), 默认咱们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。
            // 在这里,你能够定义其余的信息,在运行时能够经过TinkerLoadResult.getPackageConfigByName获得相应的数值。
            // 可是建议直接经过修改代码来实现,例如BuildConfig。
            configField("platform", "all")
            configField("patchVersion", "1.0")
//            configField("patchMessage", "tinker is sample to use")
        }
        // 7zip路径配置项,执行前提是useSign为true
        sevenZip {
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        }
    }
    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")

    /**
     * bak apk and mapping
     */
    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
                        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"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
                        }
                    }
                }
            }
        }
    }
    project.afterEvaluate {
        //sample use for build all flavor for one time
        if (hasFlavors) {
            task(tinkerPatchAllFlavorRelease) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"

                    }

                }
            }

            task(tinkerPatchAllFlavorDebug) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
                    }

                }
            }
        }
    }
}
复制代码

其中,有几点配置在这里说明一下,方便理解后续的操做(当tinkerEnabled = true的状况下):

  • app的生成目录是:主Module(通常是名为app)/build/bakApk文件夹。
  • 补丁包的生成路径:主Module(通常是名为app)/build/outputs/apk/tinkerPatch/debug/patch_signed_7zip.apk。
  • 基础包的名字:old-app.apk,放于bakApk文件夹下。
  • 基础包的mapping.txt和R.txt文件通常在编译release签名的apk时才会用到。
  • 在用到mapping.txt文件时,须要重命名为old-app-mapping.txt,放于bakApk文件夹下。
  • 在用到R.txt文件时,须要重命名为old-app-R.txt,放于bakApk文件夹下。

对于mapping.txt和R.txt文件,在配置中有说明,请回配置中仔细看。
上面只是我项目中的配置,这些其实都是能够自定义的,建议在搞清楚配置内容以后再去自定义修改。

什么是基础包??

基础包就是已经上架的apk文件(假设是1.0版本)。这其实很好理解,在新版本的App上架以前(假设是2.0版本),咱们会用到Tinker来修复1.0版App中存在的bug,这时就须要用到Tinker来产生补丁包文件,而补丁包文件的本质,就是修复好Bug的App与1.0版本App之间的文件差别。在2.0版本上架以前,咱们可能会屡次产生新的补丁包,用于修复在用户手机上的1.0版App,因此补丁包必须以1.0版App做为参考标准,也就是说用户手机上的app就是基础包,即当前应用市场上的apk文件(前面说的1.0版本)。

4、Tinker封装与拓展

一、拷贝文件

将Demo中提供的tinker包下的全部文件及文件夹都拷贝到本身项目中。

这些文件其实就是Tinker官方Demo中的文件彻底复制过来的,只是多加了一些注释。

简单说明下,这几个文件的做用:

  • SampleUncaughtExceptionHandler:Tinker的全局异常捕获器。
  • MyLogImp:Tinker的日志输出实现类。
  • SampleLoadReporter:加载补丁时的一些回调。
  • SamplePatchListener:过滤Tinker收到的补丁包的修复、升级请求。
  • SamplePatchReporter:修复或者升级补丁时的一些回调。
  • SampleTinkerReport:修复结果(成功、冲突、失败等)。
  • SampleResultService::patch补丁合成进程将合成结果返回给主进程的类。
  • TinkerManager:Tinker管理器(安装、初始化Tinker)。
  • TinkerUtils:拓展补丁条件断定、锁屏或后台时应用重启功能的工具类。

这些只是对Tinker功能的拓展和封装罢了,都是可选的,但这些文件对项目的功能完善会有所帮助,建议加入到本身的项目中。
若是你仅仅只是为了修复bug,而不作过多的工做(如:上传打补丁信息到服务器等),则无须理会这些文件的做用,固然你也能够本身封装。

对于这些自定义类及错误码的详细说明,请参考:「Tinker官方Wiki:可选的自定义类」

二、清单文件中添加服务

前面添加的文件中,有一个SampleResultService文件,是四大组件之一,因此必须在清单文件中声明。

<service
    android:name="com.lqr.tinker.service.SampleResultService"
    android:exported="false"/>
复制代码

5、编写Application的代理类

Tinker表示,Application没法动态修复,因此有两种选择:

  1. 使用「继承TinkerApplication + DefaultApplicationLike」。
  2. 使用「DefaultLifeCycle注解 + DefaultApplicationLike」。

固然,若是你以为你自定义的Application不会用到热修复,可无视这部分;
但下方代码中的initTinker()方法记得要拷贝到你项目中,用于初始化Tinker。

第1种方式感受比较鸡肋,这里使用第2种(Tinker官方推荐的方式):「DefaultLifeCycle注解 + TinkerApplicationLike」,DefaultLifeCycle注解生成Application,下面就来编写Application的代理类:

一、编写TinkerApplicationLike

将下方的代码拷贝到项目中,注释简单明了,很少解释:

@SuppressWarnings("unused")
@DefaultLifeCycle(application = "com.lqr.tinker.MyApplication",// application类名。只能用字符串,这个MyApplication文件是不存在的,但能够在AndroidManifest.xml的application标签上使用(name)
        flags = ShareConstants.TINKER_ENABLE_ALL,// tinkerFlags
        loaderClass = "com.tencent.tinker.loader.TinkerLoader",//loaderClassName, 咱们这里使用默认便可!(可不写)
        loadVerifyFlag = false)//tinkerLoadVerifyFlag
public class TinkerApplicationLike extends DefaultApplicationLike {

    private Application mApplication;
    private Context mContext;
    private Tinker mTinker;

    // 固定写法
    public TinkerApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
        super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
    }

    // 固定写法
    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
        getApplication().registerActivityLifecycleCallbacks(callback);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        mApplication = getApplication();
        mContext = getApplication();
        initTinker(base);
        // 能够将以前自定义的Application中onCreate()方法所执行的操做搬到这里...
    }

    private void initTinker(Context base) {
        // tinker须要你开启MultiDex
        MultiDex.install(base);

        TinkerManager.setTinkerApplicationLike(this);
        // 设置全局异常捕获
        TinkerManager.initFastCrashProtect();
        //开启升级重试功能(在安装Tinker以前设置)
        TinkerManager.setUpgradeRetryEnable(true);
        //设置Tinker日志输出类
        TinkerInstaller.setLogIml(new MyLogImp());
        //安装Tinker(在加载完multiDex以后,不然你须要将com.tencent.tinker.**手动放到main dex中)
        TinkerManager.installTinker(this);
        mTinker = Tinker.with(getApplication());
    }

}
复制代码

二、搬运自定义Application中的操做

把项目中在自定义Application的操做移到TinkerApplicationLike的onCreate()或onBaseContextAttached()方法中。

public class TinkerApplicationLike extends DefaultApplicationLike {
	...
    @Override
    public void onCreate() {
        super.onCreate();
		// 将以前自定义的Application中onCreate()方法所执行的操做搬到这里...
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
        super.onBaseContextAttached(base);
        mApplication = getApplication();
        mContext = getApplication();
        initTinker(base);
        // 或搬到这里...
    }
}
复制代码

三、清单文件中注册

将@DefaultLifeCycle中application对应的值,即"com.lqr.tinker.MyApplication",赋值给清单文件的application标签的name属性,以下:

<application
    android:name="com.lqr.tinker.MyApplication"
    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">
    ...
</application>
复制代码

注意:
此时name属性会报红,由于项目源码中根本不存在MyApplication.java文件,但没必要担忧,由于它是动态生成的,Build一下项目就行了,无论它也无所谓。

对于Application代理类的详细说明,请参考:「Tinker官方Wiki:Application代理类」

到这里就已经集成好Tinker了,但只是本地集成而已,服务端下发补丁包到app的文章以后会陆续发布更新。

6、经常使用API

如今来了解下代码中会用到的几个Tinker的重要API。

一、请求打补丁

TinkerInstaller.onReceiveUpgradePatch(context, 补丁包的本地路径);
复制代码

二、卸载补丁

Tinker.with(getApplicationContext()).cleanPatch();// 卸载全部的补丁
Tinker.with(getApplicationContext()).cleanPatchByVersion(版本号)// 卸载指定版本的补丁
复制代码

三、杀死应用的其余进程

ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
复制代码

四、Hack方式修复so

TinkerLoadLibrary.installNavitveLibraryABI(this, abi);
复制代码

abi:cpu架构类型

五、非Hack方式修复so

TinkerLoadLibrary.loadLibraryFromTinker(getApplicationContext(), "lib/" + abi, so库的模块名); // 加载任意abi库
TinkerLoadLibrary.loadArmLibrary(getApplicationContext(), so库的模块名); // 只适用于加载armeabi库
TinkerLoadLibrary.loadArmV7Library(getApplicationContext(), so库的模块名); // 只适用于加载armeabi-v7a库 
复制代码

loadArmLibrary()与loadArmV7Library()本质是调用了loadLibraryFromTinker(),有兴趣的能够查看下源码。

对于Tinker全部API的详细说明,请参考:「Tinker官方Wiki:Tinker-API概览」

7、测试

由于布局简单且不是重点,这里就给出一张Demo的运行图片,剩下的就靠想像了。

一、编译基础包

没有基础包,那要补丁有什么用?因此,第一步就是打包一个apk。

在Terminal中使用命令行./gradlew assembleDebug。不会命令行无所谓,Android Studio为咱们提供了图形化操做,根据下图操做便可:

若是你是要release签名的打包,则双击assembleRelease,不过还要配置签名文件,这个后面再说。

编译完成后,能够在build目录下会自动建立一个bakApk文件夹,里面就有打包好的apk文件,由于以后的全部生成的补丁包都以这个apk会标准,因此这就是那个基础包文件(至关于应用市场上的app)。

若是这个apk文件是release签名且是要放到应用市场上的,那么你必须将apk与R.txt(若是有使用混淆的话,还会有一个mapping.txt)这几个文件保存好,切记。

如今就把这个tinker-local-debug-1206-11-48-42.apk安装到手机上(至关因而用户手机上的app)。

  1. 点击"say someting"按钮吐司"Hello"。
  2. 点击"get string from .so"按钮吐司"hello LQR"。
  3. 点击"show info"按钮显示"patch is not loaded",说明当前没有加载补丁。

一、修复java代码

下面是"say someting"按钮点击时,调用的方法,使用Toast显示Hello字符串:

public void say(View view) {
    Toast.makeText(getApplicationContext(), "Hello", Toast.LENGTH_SHORT).show();
}
复制代码

1)修复代码

如今我想让它吐司Hello World,因此代码修改成:

public void say(View view) {
    Toast.makeText(getApplicationContext(), "Hello World", Toast.LENGTH_SHORT).show();
}
复制代码

2)制做补丁包

先将基础包(前面那个tinker-local-debug-1206-11-48-42.apk文件)重命名为old-app.apk,而后双击tinkerPatchDebug,操做以下图所示:

编译完成后,build/outputs/apk/tinkerPatch会产生3个补丁包,咱们要的就是patch_signed_7zip.apk。

对于build/outputs/apk/tinkerPatch目录下文件及文件夹的详细说明,请参考:「Tinker官方Wiki:输出文件详解」

3)下发补丁包

将patch_signed_7zip.apk放到手机的SD卡目录下:

不必定是SD卡目录,位置由咱们开发者决定,Demo中调用TinkerInstaller.onReceiveUpgradePatch(context, 补丁包的本地路径)方法时,第二个参数指定了是SD卡,故如此操做。

4)打补丁

能够看到,在install patch以前,点击"say someting"按钮时,仍是吐司"Hello"。 点击"install patch"按钮后,会提示"patch success,please restart process",说明Tinker已经打上补丁了。 这时点击"show info",能够看到"patch is not loaded",说明当前补丁尚未生效。 最后,点击"kill myself"按钮,杀死当前app(进程)。

在从新打开app,再点击"say someting"按钮,吐司"Hello World"。 再点击"show info",能够看到"patch is loaded",说明app重启后补丁生效了。

小结:
Tinker热修复没法让补丁实时生效,在重启进程后,补丁才会生效。
Tinker会在app重启后自动应用补丁。

二、修复so库

在一开始制做基础包时,工程中就已经加入了一些so文件,存放在src/main/jniLibs目录下,由于Android Studio默认的库目录是libs(与src同级),因此这里须要在app的build.gradle文件中进行配置,指定so库所在文件夹。

下面是"get string from .so"按钮点击时调用的方法:

public void string_from_so(View view) {
    String string = JniUtil.hello();
    Toast.makeText(getApplicationContext(), string, Toast.LENGTH_SHORT).show();
}
复制代码

这个JniUtil的代码以下:

public class JniUtil {
    public static String LIB_NAME = "LQRJni";
    public JniUtil() {}
    static {
        System.loadLibrary(LIB_NAME);
    }
    public static native String hello();
}
复制代码

加载so库有2点须要注意:

  1. System.loadLibrary(libname)加载固定目录下的库文件,而System.load(filename)加载指定目录下的库文件。
  2. System.loadLibrary(libname)的参数libname指的是库的模块名,不是so文件的名字,如libLQRJni.so文件的模块名其实是LQRJni。

so文件的制做代码包含在Demo中,有兴趣的朋友能够尝试本身制做。

1)替换so文件

回归正题,如今so库中获得的文字是"Hello LQR",如今变一下,我须要获得的文字是"Hello CSDN_LQR",将新的so文件替换掉旧的so文件便可。

2)检查Tinker的lib匹配规则

在app的build.gradle文件中,咱们前面在第三部分《Tinker的配置及任务》的第2节《配置Tinker与任务》中,有以下一段配置:

lib {
    pattern = ["lib/*/*.so", "src/main/jniLibs/*/*.so"]
}
复制代码

这就是Tinker的lib匹配规则,在生成补丁的过程当中,它会去把符合这个规则的库文件拿出来与基础包中的库文件进行匹配,从而将有差别的库文件放入到补丁包中。而Tinker官方Demo的配置中是没有"src/main/jniLibs/*/*.so"这一段的,这将致使Tinker在产生补丁包时不会去检查src/main/jniLibs目录下的文件变化,进而补丁包中不会包含修复好的so文件,这很重要,切记。

3)生成补丁与下发补丁包

生成补丁与下发补丁包的过程与以前的操做一致,这里再也不重覆,不过咱们来看看tinkerPatch跟以前有什么区别吧:

最后记得将patch_signed_7zip.apk放到手机的SD卡目录下。

4)卸载补丁

补丁是能够打多个的,用补丁的版本号作区分,在卸载的时候,能够根据补丁的版本号来卸载,也能够把以前全部的补丁卸载掉,实际开发中,看项目需求来解决用哪一种方式来卸载补丁,这里我选择清理以前全部的补丁,下面是"uninstall patch"按钮的点击事件:

public void uninstall_patch(View view) {
    ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
    Tinker.with(getApplicationContext()).cleanPatch();
}
复制代码

卸载补丁以前,要先杀死当前App的其余进程。
卸载补丁以后,App是不安全的(由于此时Tinker已经初始化完成),最好,须要重启一下App。

5)打补丁

如今咱们再来打一次补丁,操做以下图所示:

能够看到,在install patch以前,点击"get string from .so"按钮时,仍是吐司"Hello LQR"。 点击"install patch"按钮后,会提示"patch success,please restart process",说明Tinker已经打上补丁了。 这时点击"show info",能够看到"patch is not loaded",说明当前补丁尚未生效。 最后,点击"kill myself"按钮,杀死当前app(进程)。

如今重启app,理想状态下,当咱们再次点击"get string from .so"按钮时,会吐司"Hello CSDN_LQR"。

然而,吐司仍是"Hello LQR",并无变化,并且点击"show info"按钮后,能够看到"patch is loaded",说明补丁已经加载了,这是为啥?

再来看看下面的操做:

重启app后,先点击"load library(hack)"按钮,再点击"get string from .so"按钮,出现了,吐司变成了"Hello CSDN_LQR"。

上图是Tinker官网Wiki的文档部分截图,从红线部分能够知道,由于部分手机判断abi并不许确(可能由于Android碎片化比较严重吧),Tinker没有区分abi,天然也不会在app启动时,自动加载对应的so库,这须要开发者本身判断。

下面是"load library(hack)"按钮点击调用的方法:

public void load_library_hack(View view) {
    String CPU_ABI = android.os.Build.CPU_ABI;
    // 将tinker library中的 CPU_ABI架构的so 注册到系统的library path中。
    TinkerLoadLibrary.installNavitveLibraryABI(this, CPU_ABI);
}
复制代码

这是Tinker提供的使用Hack方式加载补丁中的so库,只是一个方法调用而已,并无什么特别。对于非Hack方式加载补丁的方式,我本人是没有测试成功的,很奇怪,搞不明白问题的缘由,官方的文档也写得不清不楚的,有知道本Demo加载不成功的缘由的朋友请不吝赐教一下哈,thx。

小结:
Tinker虽然会在app重启后自动加载补丁,但不会自动加载补丁中的so文件,开发者需本身断定好abi来加载so文件。

三、修复资源文件

这部分跟前面的重合度极高,故不作演示了,你能够在补丁包中对本demo中的头像进行替换试试,与修复java文件的操做基本一致,这部分须要提醒的是,app的build.gradle文件中Tinker配置有以下这一段:

res {
    pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
    ...
}
复制代码

不难理解,这就是Tinker对资源文件的匹配规则,平常开发够用,若是你的项目中把资源文件放到了这里没有的目录下,须要修改这部分的配置。

8、细节

一、打包步骤

1)debug打包

  1. 调用assembleDebug编译获得一个debug签名的apk(old apk),这是基础apk。
  2. 修改代码、更新res文件、so等。
  3. 将old apk按gradle中的参数规则,重命名为指定名字,仍是放在bakApk目录下(该目录可更改)。
  4. 调用tinkerPatchDebug生成补丁包于/build/outputs/tinkerPatch/目录(默认是patch_signed_7zip.apk)。
  5. 将补丁包复制到SD卡目录下(目录可更改),在程序中调用打补丁方法,重启app便可实现热修复。

2)release打包步骤

  1. 调用assembleRelease编译获得一个release签名的apk(old apk),这是基础apk,还有一个mapping文件。
  2. 修改代码、更新res文件、so等。
  3. 将old apk与mapping文件按gradle中的参数规则,分别重命名为指定名字,仍是放在bakApk目录下(该目录可更改)。
  4. 调用tinkerPatchRelease生成补丁包于/build/outputs/tinkerPatch/目录(默认是patch_signed_7zip.apk)。
  5. 将补丁包复制到SD卡目录下(目录可更改),在程序中调用打补丁方法,重启app便可实现热修复。

由于调用tinker的release打包须要用到签名文件的信息,因此还必须在app的build.gradle中配置好签名文件。

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

        debug {
            storeFile file("./keystore/debug.keystore")
        }
    }
	...
    buildTypes {
        release {
            minifyEnabled true
            signingConfig signingConfigs.release
            proguardFiles getDefaultProguardFile('proguard-android.txt'), project.file('proguard-rules.pro')
        }
        debug {
            debuggable true
            minifyEnabled false
            signingConfig signingConfigs.debug
        }
    }
}
复制代码

其实说白了,debug与release打包的差异,除了执行的命令不同以外,release打包比debug打包多用到2个文件(mapping.txt、R.txt)。

二、使用tinker的注意事项与发现

  • tinker编译时须要禁用instant run。
  • tinker须要MultiDex。
  • 上架前用assembleRelease编译获得的apk、mapping.txt、R.txt这3个文件要备份好,制做补丁时会用到。
  • 多个补丁包的版本同样时,不影响打补丁(如:第一次补丁版本是1.0,第二次补丁仍是1.0版本,是能够成功打上第二次补丁的)。
  • 成功打上补丁后,补丁原文件会被删除,故项目中没必要担忧补丁原文件清理的问题。

三、可能会遇到的错误

1)onLoadPatchListenerReceiveFail code为-2

报错原文以下:

receive a patch file: /storage/emulated/0/patch_signed_7zip.apk, file size:3604
patch loadReporter onLoadPatchListenerReceiveFail: patch receive fail: /storage/emulated/0/patch_signed_7zip.apk, code: -2
复制代码

出现这种状况,请按以下两步进行排查:

  1. 查看文件路径是否正常。
  2. 查看清单文件中是否有添加SD卡访问权限。

若是你的手机是Android7.0请要考虑FileProvider(Android7.0不支持直接访问sd卡)。

2)onLoadPatchListenerReceiveFail code为-24

报错原文以下:

receive a patch file: /storage/emulated/0/patch_signed_7zip.apk, file size:3665
get platform:null
patch loadReporter onLoadPatchListenerReceiveFail: patch receive fail: /storage/emulated/0/patch_signed_7zip.apk, code: -24
复制代码

提示很明显,Tinker获取不到platform的值,请检查在app的build.gradle文件中是否有以下配置,这部分配置了Tinker补丁包支持的平台与版本号:

packageConfig {
	configField("platform", "all")
	configField("patchVersion", "1.0")
}
复制代码

9、其余

对于多渠道打包的补丁文件,暂时没有研究,请自行参考Tinker的官方Wiki。

本Demo基于Tinker官方Demo及文档制做,如下是Tinker的官方文档连接:

最后贴下Demo连接

github.com/GitLqr/HotF…

Demo中的Module说明:

  1. app:热修复原理Demo
  2. tinker-local:本地集成Tinker热修复Demo
  3. jnitest:生成简单so文件的Demo

欢迎关注微信公众号:全栈行动
相关文章
相关标签/搜索