[Android]腾讯Tinker热修复框架简单使⽤ 前⾔
⽬前我们所知的热修复⽅案有阿⾥的AndFix、美团的Robust以及QZone的超级补丁⽅案,还有本篇的Tinker,如何在我们的⾃开发的软件上选⽤合适的⽅案呢?
先看看各家的框架效能对⽐,在作参考。
总体来说:
1. AndFix作为native解决⽅案,⾸先⾯临的是稳定性与兼容性问题,更重要的是它⽆法实现类替换,它是需要⼤量额外的开发成本的; 2. Robust兼容性与成功率较⾼,但是它与AndFix⼀样,⽆法新增变量与类只能⽤做的bugFix⽅案;
3. Qzone⽅案可以做到发布产品功能,但是它主要问题是插桩带来Dalvik的性能问题,以及为了解决Art下内存地址问题⽽导致补丁包急速增⼤的。
可以看出,Tinker热补丁⽅案既⽀持类、So和资源的替换,还⽀持了2.x-7.x平台。我们不仅可以⽤做bugfix,甚⾄可以替代功能的发布,况且Tinker已经在数亿Android端的上运⾏使⽤,这个噱头你还不使⽤说明
Tinker提供了两种接⼊⽅式,gradle接⼊和命令⾏接⼊,这⾥先说明gradle的⽅式,这也是⽐较推荐的⽅式。
1.在项⽬的adle中,添加tinker-patch-gradle-plugin的依赖:
1. buildscript {
2. dependencies {
3. classpath ('t.tinker:tinker-patch-gradle-plugin:1.7.10')
4. }
5. }
2.然后在app的gradle⽂件adle,我们需要添加tinker的库依赖以及apply tinker的gradle插件.
1. dependencies {
2. //可选,⽤于⽣成application类
3. provided('t.tinker:tinker-android-anno:1.7.10')
4. //tinker的核⼼库
5. compile('t.tinker:tinker-android-lib:1.7.10')
6. }
1. //apply tinker插件
2. apply plugin: 't.tinker.patch'
3.在/adle中加⼊tinkerPatch task 脚本,
1. def bakPath = file("${buildDir}/bakApk/")
2.
3. ext {
4. //for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
5. tinkerEnabled = true
6.
7. //for normal build
8. //old apk file to build patch apk
9. tinkerOldApkPath = "${bakPath}/app-release-0601-14-29-28.apk"
10. //proguard mapping file to build patch apk
11. tinkerApplyMappingPath = "${bakPath}/"
12. // to build patch apk, must input if there is resource changed
13. tinkerApplyResourcePath = "${bakPath}/"
14.
15. //only use for build all flavor, if not, just ignore this field
16. // tinkerBuildFlavorDirectory = "${bakPath}/app-0526-17-40-51"
17. }
18.
19. //这个⽅法其实就是定义了⼀个tink_id
20. def getSha() {
21. try {
22. String tinkId = "tink_id_000000000"
23. if (tinkId == null) {
24. throw new RuntimeException("you should add tinkeId to system path or just input test value, such as 'testTinkerId'")
25. }
26. return tinkId
27. } catch (Exception e) {
28. throw new RuntimeException("you should add tinkeId to system path or just input test value, such as 'testTinkerId'")
29. }
链式运输机30. }
30. }
31.
32.
33. def getOldApkPath() {
34. return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
35. }
36.
37. def getApplyMappingPath() {
38. return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
39. }
40.
41. def getApplyResourceMappingPath() {
42. return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
43. }
44.
45. def getTinkerIdValue() {
46. return hasProperty("TINKER_ID") ? TINKER_ID : getSha()
47. }
48.
49. def buildWithTinker() {
50. return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
51. }
52.
53. def getTinkerBuildFlavorDirectory() {
54. return ext.tinkerBuildFlavorDirectory
55. }
冯代存
56.
57. if (buildWithTinker()) {
58. apply plugin: 't.tinker.patch'
59.
60. tinkerPatch {
61. /**
62. * necessary,default 'null'
63. * the old apk path, use to diff with the new apk to build
64. * add apk from the build/bakApk
65. */
66. oldApk = getOldApkPath()
67. /**
68. * optional,default 'false'
69. * there are some cases we may get some warnings
70. * if ignoreWarning is true, we would just assert the patch process
71. * case 1: minSdkVersion is below 14, but you are using dexMode with raw.
72. * it must be crash when load.
73. * case 2: newly added Android Component l,
74. * it must be crash when load.
75. * case 3: loader classes in dex.loader{} are not keep in the main dex,
76. * it must be let tinker not work.
77. * case 4: loader classes in dex.loader{} changes,
78. * loader classes is ues to load patch dex. it is useless to change them.
79. * it won't crash, but these changes can't effect. you may ignore it
80. * case 5: resources.arsc has changed, but we don't use applyResourceMapping to build
81. */
82. ignoreWarning = false
83.
84. /**
85. * optional,default 'true'
86. * whether sign the patch file
87. * if not, you must do yourself. otherwise it can't check success during the patch loading
88. * we will use the sign config with your build type
89. */
90. useSign = true
91.
92. /**
93. * optional,default 'true'
94. * whether use tinker to build
95. */
96. tinkerEnable = buildWithTinker()
97.
98. /**
99. * Warning, applyMapping will affect the normal android build!
100. */
101. buildConfig {
102. /**
103. * optional,default 'null'
104. * if we use tinkerPatch to build the patch apk, you'd better to apply the old
105. * apk mapping file if minifyEnabled is enable!
106. * Warning:
107. * you must be careful that it will affect the normal assemble build!
108. */
109. applyMapping = getApplyMappingPath()
110. /**
111. * optional,default 'null'
112. * It is nice to keep the resource id file to reduce java changes
113. */
114. applyResourceMapping = getApplyResourceMappingPath()
115.
116. /**
117. * necessary,default 'null'
118. * because we don't want to check the base apk with md5 in the runtime(it is slow)
119. * tinkerId is use to identify the unique base apk when the patch is tried to apply.
120. * we can use git rev, svn rev or simply versionCode.
120. * we can use git rev, svn rev or simply versionCode.
121. * we will gen the tinkerId in your manifest automatic
122. */
123. tinkerId = getTinkerIdValue()
124.
125. /**
126. * if keepDexApply is true, class in which dex refer to the old apk.
127. * open this can reduce the dex diff file size.
128. */
129. keepDexApply = false
130.
131. /**
132. * optional, default 'false'
133. * Whether tinker should treat the base apk as the one being protected by app
134. * protection tools.
135. * If this attribute is true, the generated patch package will contain a
136. * dex including all changed classes instead of any dexdiff patch-info files.
137. */
138. isProtectedApp = false
139. }
140.
141. dex {
142. /**
143. * optional,default 'jar'
144. * only can be 'raw' or 'jar'. for raw, we would keep its original format
145. * for jar, we would repack dexes with zip format.
146. * if you want to support below 14, you must use jar
147. * or you want to save rom or check quicker, you can use raw mode also
148. */
149. dexMode = "jar"
150.
151. /**
152. * necessary,default '[]'
153. * what dexes in apk are expected to deal with tinkerPatch
154. * it support * or ? pattern.
155. */
156. pattern = ["classes*.dex",
157. "assets/secondary-dex-?.jar"]
158. /**
159. * necessary,default '[]'
160. * Warning, it is very very important, loader classes can't change with patch.
161. * thus, they will be removed from patch dexes.
162. * you must put the following class into main dex.
163. * Simply, you should add your own application {@code tinker.sample.android.SampleApplication} 164. * own tinkerLoader, and the classes you use in them
165. *
166. */
167. loader = [
168. //use sample, let BaseBuildInfo unchangeable with tinker
169. "t.tinker.loader.*",
170. "t.tinker.*",
171. "app.MyApplication"
172. ]
173. }
174.
175. lib {
176. /**
177. * optional,default '[]'
178. * what library in apk are expected to deal with tinkerPatch
179. * it support * or ? pattern.
180. * for library in assets, we would just recover them in the patch directory
181. * you can get them in TinkerLoadResult with Tinker
182. */
183. pattern = ["lib/*/*.so"]
184. }
185.
186. res {
187. /**
188. * optional,default '[]'
189. * what resource in apk are expected to deal with tinkerPatch
190. * it support * or ? pattern.
191. * you must include all your resources in apk here,
192. * otherwise, they won't repack in the new apk resources.
193. */
194. pattern = ["res/*", "assets/*", "resources.arsc", "l"]
195.
196. /**
197. * optional,default '[]'
198. * the resource file exclude patterns, ignore add, delete or modify resource change
199. * it support * or ? pattern.
滤菌器200. * Warning, we can only use for files no relative with resources.arsc
201. */
202. ignoreChange = ["assets/"]
203.
204. /**
205. * default 100kb
206. * for modify resource, if it is larger than 'largeModSize'
207. * we would like to use bsdiff algorithm to reduce patch file size
208. */
209. largeModSize = 100
210. }
210. }
211.
212. packageConfig {
213. /**
214. * optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
215. * package meta file gen. path is assets/ in patch file
216. * you can PackageProperties() in your ownPackageCheck method
217. * PackageConfigByName
218. * we will get the TINKER_ID from the old apk manifest for you automatic,
219. * other config files (such as patchMessage below)is not necessary
220. */
221. configField("TINKER_ID", getSha())
222. configField("app_name", "MyApp")
223. configField("patchMessage", "这是⼀个测试");
224. /**
225. * just a sample case, you can use such as sdkVersion, brand,
226. * you can parse it in the SamplePatchListener.
227. * Then you can use patch conditional!
228. */
229. configField("platform", "all")
230. /**
231. * patch version via packageConfig
232. */
233. configField("patchVersion", "1.0")
234. }
235. //or you can add config filed outside, or get meta value from old apk
236. //project.figField("test1", project.MetaDataFromOldApk("Test")) 237. //project.figField("test2", "sample")
238.
239. /**
240. * if you don't use zipArtifact or path, we just use 7za to try
241. */
242. sevenZip {
243. /**
244. * optional,default '7za'
245. * the 7zip artifact path, it will use the right 7za with your platform
246. */
247. zipArtifact = ":SevenZip:1.1.10"
248. /**
249. * optional,default '7za'
250. * you can specify the 7za path yourself, it will overwrite the zipArtifact value
251. */
252. // path = "/usr/local/bin/7za"
253. }
254. }
255.
256. List<String> flavors = new ArrayList<>();
257. project.android.productFlavors.each {flavor ->
258. flavors.add(flavor.name)
259. }
260. boolean hasFlavors = flavors.size() > 0
261. def date = new Date().format("MMdd-HH-mm-ss")
262.
263. /**
264. * bak apk and mapping
265. */
266. android.applicationVariants.all { variant ->
267. /**
268. * task type, you want to bak
269. */
270. def taskName = variant.name
271.
272. tasks.all {
273. if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {
274.
275. it.doLast {
276. copy {
277. def fileNamePrefix = "${project.name}-${variant.baseName}"
278. def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"
279.
280. def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
281. from variant.outputs.outputFile
282. into destPath
283. rename { String fileName ->
284. place("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
285. }
286.
287. from "${buildDir}/outputs/mapping/${variant.dirName}/"
288. into destPath
289. rename { String fileName ->
290. place("", "${newFileNamePrefix}-")
291. }
292.
293. from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
294. into destPath
295. rename { String fileName ->
296. place("R.txt", "${newFileNamePrefix}-R.txt")
297. }
298. }
299. }
300. }
301. }
302. }
303. project.afterEvaluate {
304. //sample use for build all flavor for one time
305. if (hasFlavors) {
306. task(tinkerPatchAllFlavorRelease) {
307. group = 'tinker'
308. def originOldPath = getTinkerBuildFlavorDirectory()
309. for (String flavor : flavors) {
310. def tinkerTask = ByName("tinkerPatch${flavor.capitalize()}Release")
311. dependsOn tinkerTask
312. def preAssembleTask = ByName("process${flavor.capitalize()}ReleaseManifest")
313. preAssembleTask.doFirst {
314. String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
315. project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"4g手机电子围栏
316. project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-"
317. project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-"
318. }
319. }
320. }
321.
322. task(tinkerPatchAllFlavorDebug) {
323. group = 'tinker'
324. def originOldPath = getTinkerBuildFlavorDirectory()
325. for (String flavor : flavors) {
326. def tinkerTask = ByName("tinkerPatch${flavor.capitalize()}Debug")
327. dependsOn tinkerTask
328. def preAssembleTask = ByName("process${flavor.capitalize()}DebugManifest")
329. preAssembleTask.doFirst {
330. String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
331. project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
332. project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-"
333. project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-"
334. }
335. }
336. }
337. }
338. }
339. }
注意,minifyEnabled⼀定要设置为true,别忘了设置tinkerId,在getSha⽅法⾥可以先写死。
代码⾥我们要做点什么呢?我需要先⾃定义Application类,因为引⼊了Tinker,所以原来的MyApplication必须改造。
涂改液程序启动时会加载默认的Application类,这导致我们补丁包是⽆法对它做修改了。如何规避?在这⾥我们并没有使⽤类似InstantRun hook Application的⽅式,⽽是通过代码框架的⽅式来避免,这也是为了升框架的兼容性。这⾥我们要实现的是完全将原来的Application类隔离起来,即其他任何类都不能再引⽤我们⾃⼰的Application。我们需要做的其实是以下⼏个⼯作:
1. 将我们⾃⼰Application类以及它的继承类的所有代码拷贝到⾃⼰的ApplicationLike继承类中,例如MyApplicationLike。你也可以直接将⾃⼰的Application改为继承ApplicationLike; 2. Application的attachBaseContext⽅法实现要单独移动到onBaseContextAttached中;
3. 对ApplicationLike中,引⽤application的地⽅改成getApplication();
4. 对其他引⽤Application或者它的静态对象与⽅法的地⽅,改成引⽤ApplicationLike的静态对象与⽅法;
我的demo中原来是⾃定义的MyApplication,现在必须把之前的Application中定义的变量转移到⾃定义的ApplicationLike中去,然后⾃动⽣成MyApplication,这⾥使⽤Annotation⽣成Application类(推荐)。
MyApplicationLike.java
1. app;
2.
3. import android.app.Application;
4. t.Context;
5. t.Intent;
6.
7. app.db.dao.DaoMaster;
8. app.db.dao.DaoSession;
9. app.event.MyEventBusIndex;
10. import com.jan.lib.BusPoster;
11. t.tinker.anno.DefaultLifeCycle;
12. t.tinker.lib.tinker.TinkerInstaller;
13. t.tinker.loader.app.DefaultApplicationLike;
14. t.tinker.loader.shareutil.ShareConstants;
15.
16. dao.database.Database;
17.
18. /**
19. * Created by Jan on 2017/5/25.
20. */
21. @DefaultLifeCycle(
22. application = ".MyApplication",
23. flags = ShareConstants.TINKER_ENABLE_ALL, ////tinkerFlags, tinker⽀持的类型,dex,library,还是全部都⽀持!
24. loaderClass = "t.tinker.loader.TinkerLoader",//loaderClassName, 我们这⾥使⽤默认即可!
25. loadVerifyFlag = false) //tinkerLoadVerifyFlag
26. public class MyApplicationLike extends DefaultApplicationLike {
27.
28. public static final boolean ENCRYPTED = false;
29. private DaoSession mDaoSession;
30. public static Context mContext;wntc
31.
32. public MyApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
33. super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
34. }
35.