手记

Android热修复-微信Tinker

写在前面

正常情况下一旦线上版本出BUG时,这时候得改BUG,重新发布上线,用户重新下载安装,成本未免有点高;基于这种情况下很多热修复框架孕育而生,比较火的有:Andfix、HotFix等;本文旨在帮助没接触过Tinker的童鞋快速集成使用热修复;

本文环境

官方地址
SdkVersion 24
gradle:2.2.0
Tinker版本 1.7.5

集成Tinker

在项目根目录的build.gradle中添加,例:

1

2

3

4

dependencies {

      ···

      classpath('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.5')

  }

切换到module的build.gradle中添加,例:

1

2

3

4

5

dependencies {

            ···

    provided('com.tencent.tinker:tinker-android-anno:1.7.5')

    compile('com.tencent.tinker:tinker-android-lib:1.7.5')

}

这里我把打包签名的jks文件配置到gradle中,例:
因为我这里测试用到的是项目的签名文件,所以不方便给出,需要的自行生成,毕竟这是很简单的事;

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

android {

    ···

    signingConfigs {

        release {

            try {

                storeFile file("./keystore/app.jks")

                storePassword   "password"

                keyAlias   "alias"

                keyPassword   "password"

            } catch (ex) {

                throw new InvalidUserDataException(ex.toString())

            }

        }

        debug {

            try {

                storeFile file("./keystore/app.jks")

                storePassword   "password"

                keyAlias   "alias"

                keyPassword   "password"

            } catch (ex) {

                throw new InvalidUserDataException(ex.toString())

            }

        }

    }

}

OK,到这里配置还是相对简单的,下面配置Tinker的gradle插件,官方demo配置还是比较麻烦的,看着就头晕,建议可以把先前的内容先复制出来,在拷贝官方demo的build.gradle进去一点点修改;

如果还是懒得改,直接把下面内容复制到build.gradle最下面:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

/**-----------------------------------配置开始-----------------------------------*/

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

def gitSha() {

    try {

        String gitRev = '11.2.3.5'

        if (gitRev == null) {

            throw new GradleException("can't get git rev, you should add git to system path or just input   test value, such as 'testTinkerId'")

        }

        return gitRev

    } catch (Exception e) {

        throw new GradleException("can't get git rev, you should add git to system path or just input   test value, such as 'testTinkerId'")

    }

}

ext {

    tinkerEnabled = true

    //旧apk

    tinkerOldApkPath = "${bakPath}/app-debug-0118-15-13-26.apk"

    //旧包混淆文件

    tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"

    //旧apk R文件

    tinkerApplyResourcePath = "${bakPath}/app-debug-0118-15-13-26-R.txt"

    //多渠道

    tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"

}

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 :   gitSha()

}

def buildWithTinker() {

    return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE   : ext.tinkerEnabled

}

def getTinkerBuildFlavorDirectory() {

    return ext.tinkerBuildFlavorDirectory

}

if (buildWithTinker()) {

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

    tinkerPatch {

        /**

         * 基准apk包路径,也就是旧包路径

         * */

        oldApk = getOldApkPath()

        /**

         * 如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。因为这些情况可能会导致编译出来的patch包

         * 带来风险:

         * 1. minSdkVersion小于14,但是dexMode的值为"raw";

         * 2. 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);

         * 3. 定义在dex.loader用于加载补丁的类不在main dex中;

         * 4. 定义在dex.loader用于加载补丁的类出现修改;

         * 5. resources.arsc改变,但没有使用applyResourceMapping编译。

         * */

        ignoreWarning = false

        /**

         * 在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,是否需要为你签名

         * */

        useSign = true

        buildConfig {

            /**

             * 可选参数;在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。这个只

             * 是推荐的,但设置applyMapping会影响任何的assemble编译。

             * */

            applyMapping =   getApplyMappingPath()

            /**

             * 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的

             * 大小,同时也避免由于ResId改变导致remote view异常。

             * */

            applyResourceMapping =   getApplyResourceMappingPath()

            /**

             * 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些

             * 基准包上面,一般来说我们可以使用git版本号、versionName等等。

             * */

            tinkerId =   getTinkerIdValue()

        }

        dex {

            /**

             * 只能是'raw'或者'jar'。

             * 对于'raw'模式,我们将会保持输入dex的格式。

             * 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式

             * ,而且它更省存储空间,但是验证md5时比'raw'模式耗时()

             * */

            dexMode = "jar"

            /**

             * 是否提前生成dex,而非合成的方式。这套方案即回退成Qzone的方案,对于需要使用加固或者多flavor打包(建

             * 议使用其他方式生成渠道包)的用户可使用。但是这套方案需要插桩,会造成Dalvik下性能损耗以及Art补丁包可

             * 能过大的问题,务必谨慎使用。另外一方面,这种方案在Android N之后可能会产生问题,建议过滤N之后的用户。

             */

              usePreGeneratedPatchDex = false

            /**

             * 需要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如/assets/...

             */

            pattern = ["classes*.dex",

                       "assets/secondary-dex-?.jar"]

            /**

             *     这一项非常重要,它定义了哪些类在加载补丁包的时候会用到。这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。

             这里需要定义的类有:

             1. 你自己定义的Application类;

             2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;

             3. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;

             4. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。这里需要注意的是,这些类的直接引用类也

             需要加入到loader中。或者你需要将这个类变成非preverify。

             */

            loader = ["com.tencent.tinker.loader.*",

                      //warning, you must change it with your application

                      //TODO 换成自己的Application

                      "com.tinker.MyApplication",

            ]

        }

        lib {

            /**

             * 需要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如/assets/...

             */

            pattern = ["lib/armeabi/*.so"]

        }

        res {

            /**

             * 需要处理res路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如/assets/...,

             * 务必注意的是,只有满足pattern的资源才会放到合成后的资源包。

             */

            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            /**

             * 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。

             * 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。

             */

            ignoreChange = ["assets/sample_meta.txt"]

            /**

             * 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,但是会增加合成

             * 时的复杂度。默认大小为100kb

             */

            largeModSize = 100

        }

        packageConfig {

            /**

             *   configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动

             * 写入configField。在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。但是建议直接通过修改代码来实现,例如BuildConfig。

             */

            configField("patchMessage", "tinker is sample to use")

        }

        sevenZip {

            /**

             * 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用

             */

            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"

        }

        /**

         *  文件名                              描述

         *  patch_unsigned.apk                  没有签名的补丁包

         *  patch_signed.apk                  签名后的补丁包

         *  patch_signed_7zip.apk              签名后并使用7zip压缩的补丁包,也是我们通常使用的补丁包。但正式发布的时候,最好不要以.apk结尾,防止被运营商挟持。

         *  log.txt                              在编译补丁包过程的控制台日志

         *  dex_log.txt                          在编译补丁包过程关于dex的日志

         *  so_log.txt                          在编译补丁包过程关于lib的日志

         *  tinker_result                      最终在补丁包的内容,包括diff的dex、lib以及assets下面的meta文件

         *  resources_out.zip                  最终在手机上合成的全量资源apk,你可以在这里查看是否有文件遗漏

         *  resources_out_7z.zip              根据7zip最终在手机上合成的全量资源apk

         *  tempPatchedDexes                  在Dalvik与Art平台,最终在手机上合成的完整Dex,我们可以在这里查看dex合成的产物。

         *

         *

         * */

        /**

         * 获得所有渠道集合,并判断数量

         */

        List<String> flavors   = new ArrayList<>();

          project.android.productFlavors.each { flavor ->

              flavors.add(flavor.name)

        }

        boolean hasFlavors = flavors.size() > 0

        /**

         * bak apk and mapping

         *  创建Task并执行文件操作

         */

          android.applicationVariants.all { variant ->

            /**

             * task type, you want   to bak

             */

            def taskName =   variant.name

            def date = new Date().format("MMdd-HH-mm-ss")

            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.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"

                        }

                    }

                }

            }

        }

    }

}

/**-----------------------------------配置结束-----------------------------------*/

注意tinkerId目前暂时是写死的;
新建SampleApplicarion文件继承DefaultApplicationLike,例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

@DefaultLifeCycle(application = "com.tinker.MyApplication",flags = ShareConstants.TINKER_ENABLE_ALL)

public class SampleApplicarion extends DefaultApplicationLike {

    public SampleApplicarion(Application   application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent, Resources[]   resources, ClassLoader[] classLoader, AssetManager[] assetManager) {

        super(application, tinkerFlags,   tinkerLoadVerifyFlag, applicationStartElapsedTime,   applicationStartMillisTime, tinkerResultIntent, resources, classLoader,   assetManager);

    }

    @Override

    public void onBaseContextAttached(Context   base) {

        super.onBaseContextAttached(base);

        TinkerInstaller.install(this);

    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)

    public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks   callback) {

          getApplication().registerActivityLifecycleCallbacks(callback);

    }

}

上面的application=”你的包名.MyApplication”并在上一步将dex中的loader修改为:

1

loader = ["com.tencent.tinker.loader.*","你的包名.MyApplication",]

记得在AndroidManifest.xml中添加:

1

2

3

4

5

6

7

8

9

10

11

12

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

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

<application

    android:name=".MyApplication"

    android:allowBackup="true"

    android:icon="@mipmap/ic_launcher"

    android:label="@string/app_name"

    android:supportsRtl="true"

    android:theme="@style/AppTheme">

    ···

    ···

</application>

OK,到这里就算配置完成了,配置完了当然要测试了;

测试修复BUG

在MainActivity中添加完初始代码后点击Build->Build APK生成APK;
                                               
将outputs里面的apk拷贝出去安装在手机上,然后打开的build.radle修改ext,其它不管,例:

1

2

3

4

5

6

7

8

9

ext {

    tinkerEnabled = true

    //旧apk

    tinkerOldApkPath = "${bakPath}/app-debug-0118-16-38-14.apk"

    ···

    //旧apk R文件

    tinkerApplyResourcePath = "${bakPath}/app-debug-0118-16-38-14-R.txt"

            ···

}

然后切换到MainActivity修复你的bug,这里我添加了一张资源图片(刚好同事在玩支付宝集福,就P了张有9张”敬业福”的图片骗他们,哈哈),修复好代码后点击右侧Gradle找到:app->Tasks->tinker,然后双击tinkerPatchDebug后会在outputs下面生成tinkerPatch文件夹,其中的patch_signed_7zip.apk就是你需要的补丁:

将patch_signed_7zip.apk文件放到你设置的SD卡目录,调用Tinker加载补丁,例:

1

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),   patchPath);

如果加载成功下面的tinker.isTinkerLoaded()返回的是true,例:

1

2

3

4

5

6

7

Tinker tinker = Tinker.with(getApplicationContext());

boolean isLoadSuccess =   tinker.isTinkerLoaded();

if(isLoadSuccess){

            Log.i(TAG,"success");

}else{

            Log.i(TAG,"error");         

}

OK,加载补丁前点击”SHOW”会抛出空指针异常,修复完后点击”SHOW”,BUG解决:

DEMO地址

总结

Tinker是腾讯出品,微信在用,我想兼容性应该不会差到哪去,相比其他热修复框架,这是个很大的优点,而且Tinker支持类替换、so库及资源的替换等,如果在相对稳定的情况下,使用Tinker用于线上产品的功能升级应该没什么问题,其它更高级的用法自行深入研究;

原文链接:http://www.apkbus.com/blog-719059-63038.html

0人推荐
随时随地看视频
慕课网APP