├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── keystore │ └── TinkerDemo.keystore ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── tinker │ │ └── deeson │ │ └── mytinkerdemo │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── tinker │ │ │ └── deeson │ │ │ └── mytinkerdemo │ │ │ ├── MainActivity.java │ │ │ ├── SampleApplicationLike.java │ │ │ ├── SampleResultService.java │ │ │ └── Utils.java │ └── res │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── tinker_added.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── tinker │ └── deeson │ └── mytinkerdemo │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 1.8 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TinkerDemo 2 | 3 | **Android热更新之微信Tinker框架的接入与测试** 4 | 5 | 项目教程在这里,简书: 6 | http://www.jianshu.com/p/aadcf2ea69a6 7 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | def javaVersion = JavaVersion.VERSION_1_7 4 | android { 5 | compileSdkVersion 23 6 | buildToolsVersion "23.0.2" 7 | 8 | compileOptions { 9 | sourceCompatibility javaVersion 10 | targetCompatibility javaVersion 11 | } 12 | //recommend 13 | dexOptions { 14 | jumboMode = true 15 | } 16 | 17 | 18 | defaultConfig { 19 | applicationId "com.tinker.deeson.mytinkerdemo" 20 | minSdkVersion 15 21 | targetSdkVersion 22 22 | versionCode 1 23 | versionName "1.0" 24 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 25 | 26 | buildConfigField "String", "MESSAGE", "\"I am the base apk\"" 27 | 28 | buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\"" 29 | buildConfigField "String", "PLATFORM", "\"all\"" 30 | } 31 | 32 | signingConfigs { 33 | release { 34 | try { 35 | storeFile file("./keystore/TinkerDemo.keystore") 36 | storePassword "TinkerDemo" 37 | keyAlias "TinkerDemo" 38 | keyPassword "TinkerDemo" 39 | } catch (ex) { 40 | throw new InvalidUserDataException(ex.toString()) 41 | } 42 | } 43 | 44 | debug { 45 | storeFile file("./keystore/TinkerDemo.keystore") 46 | storePassword "TinkerDemo" 47 | keyAlias "TinkerDemo" 48 | keyPassword "TinkerDemo" 49 | } 50 | } 51 | 52 | buildTypes { 53 | release { 54 | minifyEnabled true 55 | signingConfig signingConfigs.release 56 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 57 | } 58 | debug { 59 | debuggable true 60 | minifyEnabled false 61 | signingConfig signingConfigs.debug 62 | } 63 | } 64 | 65 | sourceSets { 66 | main { 67 | jniLibs.srcDirs = ['libs'] 68 | } 69 | } 70 | 71 | 72 | } 73 | 74 | dependencies { 75 | compile fileTree(dir: 'libs', include: ['*.jar']) 76 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 77 | exclude group: 'com.android.support', module: 'support-annotations' 78 | }) 79 | compile "com.android.support:appcompat-v7:23.1.1" 80 | testCompile 'junit:junit:4.12' 81 | 82 | compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true } 83 | provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true } 84 | compile "com.android.support:multidex:1.0.1" 85 | } 86 | 87 | def gitSha() { 88 | try { 89 | // String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim() 90 | String gitRev = "1008611" 91 | if (gitRev == null) { 92 | throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'") 93 | } 94 | return gitRev 95 | } catch (Exception e) { 96 | throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'") 97 | } 98 | } 99 | 100 | def bakPath = file("${buildDir}/bakApk/") 101 | 102 | ext { 103 | //for some reason, you may want to ignore tinkerBuild, such as instant run debug build? 104 | tinkerEnabled = true 105 | 106 | //for normal build 107 | //old apk file to build patch apk 108 | tinkerOldApkPath = "${bakPath}/app-release-0421-17-23-16.apk" 109 | //proguard mapping file to build patch apk 110 | tinkerApplyMappingPath = "${bakPath}/app-release-0421-17-23-16-mapping.txt" 111 | //resource R.txt to build patch apk, must input if there is resource changed 112 | tinkerApplyResourcePath = "${bakPath}/app-release-0421-17-23-16-R.txt" 113 | 114 | //only use for build all flavor, if not, just ignore this field 115 | tinkerBuildFlavorDirectory = "${bakPath}/app-0421-17-11-26" 116 | } 117 | 118 | def getOldApkPath() { 119 | return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath 120 | } 121 | 122 | def getApplyMappingPath() { 123 | return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath 124 | } 125 | 126 | def getApplyResourceMappingPath() { 127 | return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath 128 | } 129 | 130 | def getTinkerIdValue() { 131 | return hasProperty("TINKER_ID") ? TINKER_ID : gitSha() 132 | } 133 | 134 | def buildWithTinker() { 135 | return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled 136 | } 137 | 138 | def getTinkerBuildFlavorDirectory() { 139 | return ext.tinkerBuildFlavorDirectory 140 | } 141 | 142 | if (buildWithTinker()) { 143 | apply plugin: 'com.tencent.tinker.patch' 144 | 145 | tinkerPatch { 146 | /** 147 | * 默认为null 148 | * 将旧的apk和新的apk建立关联 149 | * 从build / bakApk添加apk 150 | */ 151 | oldApk = getOldApkPath() 152 | /** 153 | * 可选,默认'false' 154 | *有些情况下我们可能会收到一些警告 155 | *如果ignoreWarning为true,我们只是断言补丁过程 156 | * case 1:minSdkVersion低于14,但是你使用dexMode与raw。 157 | * case 2:在AndroidManifest.xml中新添加Android组件, 158 | * case 3:装载器类在dex.loader {}不保留在主要的dex, 159 | * 它必须让tinker不工作。 160 | * case 4:在dex.loader {}中的loader类改变, 161 | * 加载器类是加载补丁dex。改变它们是没有用的。 162 | * 它不会崩溃,但这些更改不会影响。你可以忽略它 163 | * case 5:resources.arsc已经改变,但是我们不使用applyResourceMapping来构建 164 | */ 165 | ignoreWarning = false 166 | 167 | /** 168 | *可选,默认为“true” 169 | * 是否签名补丁文件 170 | * 如果没有,你必须自己做。否则在补丁加载过程中无法检查成功 171 | * 我们将使用sign配置与您的构建类型 172 | */ 173 | useSign = true 174 | 175 | /** 176 | 可选,默认为“true” 177 | 是否使用tinker构建 178 | */ 179 | tinkerEnable = buildWithTinker() 180 | 181 | /** 182 | * 警告,applyMapping会影响正常的android build! 183 | */ 184 | buildConfig { 185 | /** 186 | *可选,默认为'null' 187 | * 如果我们使用tinkerPatch构建补丁apk,你最好应用旧的 188 | * apk映射文件如果minifyEnabled是启用! 189 | * 警告:你必须小心,它会影响正常的组装构建! 190 | */ 191 | applyMapping = getApplyMappingPath() 192 | /** 193 | *可选,默认为'null' 194 | * 很高兴保持资源ID从R.txt文件,以减少java更改 195 | */ 196 | applyResourceMapping = getApplyResourceMappingPath() 197 | 198 | /** 199 | *必需,默认'null' 200 | * 因为我们不想检查基地apk与md5在运行时(它是慢) 201 | * tinkerId用于在试图应用补丁时标识唯一的基本apk。 202 | * 我们可以使用git rev,svn rev或者简单的versionCode。 203 | * 我们将在您的清单中自动生成tinkerId 204 | */ 205 | tinkerId = getTinkerIdValue() 206 | 207 | /** 208 | *如果keepDexApply为true,则表示dex指向旧apk的类。 209 | * 打开这可以减少dex diff文件大小。 210 | */ 211 | keepDexApply = false 212 | } 213 | 214 | dex { 215 | /** 216 | *可选,默认'jar' 217 | * 只能是'raw'或'jar'。对于原始,我们将保持其原始格式 218 | * 对于jar,我们将使用zip格式重新包装dexes。 219 | * 如果你想支持下面14,你必须使用jar 220 | * 或者你想保存rom或检查更快,你也可以使用原始模式 221 | */ 222 | dexMode = "jar" 223 | 224 | /** 225 | *必需,默认'[]' 226 | * apk中的dexes应该处理tinkerPatch 227 | * 它支持*或?模式。 228 | */ 229 | pattern = ["classes*.dex", 230 | "assets/secondary-dex-?.jar"] 231 | /** 232 | *必需,默认'[]' 233 | * 警告,这是非常非常重要的,加载类不能随补丁改变。 234 | * 因此,它们将从补丁程序中删除。 235 | * 你必须把下面的类放到主要的dex。 236 | * 简单地说,你应该添加自己的应用程序{@code tinker.sample.android.SampleApplication} 237 | * 自己的tinkerLoader,和你使用的类 238 | * 239 | */ 240 | loader = [ 241 | //use sample, let BaseBuildInfo unchangeable with tinker 242 | "tinker.sample.android.app.BaseBuildInfo" 243 | ] 244 | } 245 | 246 | lib { 247 | /** 248 | 可选,默认'[]' 249 | apk中的图书馆应该处理tinkerPatch 250 | 它支持*或?模式。 251 | 对于资源库,我们只是在补丁目录中恢复它们 252 | 你可以得到他们在TinkerLoadResult与Tinker 253 | */ 254 | pattern = ["lib/armeabi/*.so"] 255 | } 256 | 257 | res { 258 | /** 259 | *可选,默认'[]' 260 | * apk中的什么资源应该处理tinkerPatch 261 | * 它支持*或?模式。 262 | * 你必须包括你在这里的所有资源, 263 | * 否则,他们不会重新包装在新的apk资源。 264 | */ 265 | pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] 266 | 267 | /** 268 | *可选,默认'[]' 269 | *资源文件排除模式,忽略添加,删除或修改资源更改 270 | * *它支持*或?模式。 271 | * *警告,我们只能使用文件没有relative与resources.arsc 272 | */ 273 | ignoreChange = ["assets/sample_meta.txt"] 274 | 275 | /** 276 | *默认100kb 277 | * *对于修改资源,如果它大于'largeModSize' 278 | * *我们想使用bsdiff算法来减少补丁文件的大小 279 | */ 280 | largeModSize = 100 281 | } 282 | 283 | packageConfig { 284 | /** 285 | *可选,默认'TINKER_ID,TINKER_ID_VALUE','NEW_TINKER_ID,NEW_TINKER_ID_VALUE' 286 | * 包元文件gen。路径是修补程序文件中的assets / package_meta.txt 287 | * 你可以在您自己的PackageCheck方法中使用securityCheck.getPackageProperties() 288 | * 或TinkerLoadResult.getPackageConfigByName 289 | * 我们将从旧的apk清单为您自动获取TINKER_ID, 290 | * 其他配置文件(如下面的patchMessage)不是必需的 291 | */ 292 | configField("patchMessage", "tinker is sample to use") 293 | /** 294 | *只是一个例子,你可以使用如sdkVersion,品牌,渠道... 295 | * 你可以在SamplePatchListener中解析它。 296 | * 然后你可以使用补丁条件! 297 | */ 298 | configField("platform", "all") 299 | /** 300 | * 补丁版本通过packageConfig 301 | */ 302 | configField("patchVersion", "1.0") 303 | } 304 | //或者您可以添加外部的配置文件,或从旧apk获取元值 305 | //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test")) 306 | //project.tinkerPatch.packageConfig.configField("test2", "sample") 307 | 308 | /** 309 | * 如果你不使用zipArtifact或者path,我们只是使用7za来试试 310 | */ 311 | sevenZip { 312 | /** 313 | * 可选,默认'7za' 314 | * 7zip工件路径,它将使用正确的7za与您的平台 315 | */ 316 | zipArtifact = "com.tencent.mm:SevenZip:1.1.10" 317 | /** 318 | * 可选,默认'7za' 319 | * 你可以自己指定7za路径,它将覆盖zipArtifact值 320 | */ 321 | // path = "/usr/local/bin/7za" 322 | } 323 | } 324 | 325 | List flavors = new ArrayList<>(); 326 | project.android.productFlavors.each {flavor -> 327 | flavors.add(flavor.name) 328 | } 329 | boolean hasFlavors = flavors.size() > 0 330 | /** 331 | * bak apk and mapping 332 | */ 333 | android.applicationVariants.all { variant -> 334 | /** 335 | * task type, you want to bak 336 | */ 337 | def taskName = variant.name 338 | def date = new Date().format("MMdd-HH-mm-ss") 339 | 340 | tasks.all { 341 | if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) { 342 | 343 | it.doLast { 344 | copy { 345 | def fileNamePrefix = "${project.name}-${variant.baseName}" 346 | def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}" 347 | 348 | def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath 349 | from variant.outputs.outputFile 350 | into destPath 351 | rename { String fileName -> 352 | fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk") 353 | } 354 | 355 | from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt" 356 | into destPath 357 | rename { String fileName -> 358 | fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt") 359 | } 360 | 361 | from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt" 362 | into destPath 363 | rename { String fileName -> 364 | fileName.replace("R.txt", "${newFileNamePrefix}-R.txt") 365 | } 366 | } 367 | } 368 | } 369 | } 370 | } 371 | project.afterEvaluate { 372 | //sample use for build all flavor for one time 373 | if (hasFlavors) { 374 | task(tinkerPatchAllFlavorRelease) { 375 | group = 'tinker' 376 | def originOldPath = getTinkerBuildFlavorDirectory() 377 | for (String flavor : flavors) { 378 | def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release") 379 | dependsOn tinkerTask 380 | def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest") 381 | preAssembleTask.doFirst { 382 | String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15) 383 | project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk" 384 | project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt" 385 | project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt" 386 | 387 | } 388 | 389 | } 390 | } 391 | 392 | task(tinkerPatchAllFlavorDebug) { 393 | group = 'tinker' 394 | def originOldPath = getTinkerBuildFlavorDirectory() 395 | for (String flavor : flavors) { 396 | def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug") 397 | dependsOn tinkerTask 398 | def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest") 399 | preAssembleTask.doFirst { 400 | String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13) 401 | project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk" 402 | project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt" 403 | project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt" 404 | } 405 | 406 | } 407 | } 408 | } 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /app/keystore/TinkerDemo.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeesonWoo/TinkerDemo/9e673a11159b07b28cfe450fb24267baae6255e0/app/keystore/TinkerDemo.keystore -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\programs\adt\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tinker/deeson/mytinkerdemo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.tinker.deeson.mytinkerdemo; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.tinker.deeson.mytinkerdemo", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/tinker/deeson/mytinkerdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.tinker.deeson.mytinkerdemo; 2 | 3 | import android.os.Environment; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.os.Bundle; 6 | import android.view.View; 7 | 8 | import com.tencent.tinker.lib.tinker.TinkerInstaller; 9 | import com.tencent.tinker.loader.shareutil.ShareTinkerInternals; 10 | 11 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_main); 17 | findViewById(R.id.btn_load).setOnClickListener(this); 18 | findViewById(R.id.btn_kill).setOnClickListener(this); 19 | } 20 | 21 | @Override 22 | protected void onResume() { 23 | super.onResume(); 24 | Utils.setBackground(false); 25 | } 26 | 27 | @Override 28 | protected void onPause() { 29 | super.onPause(); 30 | Utils.setBackground(true); 31 | } 32 | 33 | @Override 34 | public void onClick(View v) { 35 | switch (v.getId()){ 36 | case R.id.btn_load: 37 | loadPatch(); 38 | break; 39 | case R.id.btn_kill: 40 | killApp(); 41 | break; 42 | } 43 | } 44 | 45 | /** 46 | * 加载热补丁插件 47 | */ 48 | public void loadPatch() { 49 | TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), 50 | Environment.getExternalStorageDirectory().getAbsolutePath() + "/myTinkerDemo/TinkerPatch"); 51 | } 52 | 53 | /** 54 | * 杀死应用加载补丁 55 | */ 56 | public void killApp() { 57 | ShareTinkerInternals.killAllOtherProcess(getApplicationContext()); 58 | android.os.Process.killProcess(android.os.Process.myPid()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/tinker/deeson/mytinkerdemo/SampleApplicationLike.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Tencent is pleased to support the open source community by making Tinker available. 3 | * 4 | * Copyright (C) 2016 THL A29 Limited, a Tencent company. All rights reserved. 5 | * 6 | * Licensed under the BSD 3-Clause License (the "License"); you may not use this file except in 7 | * compliance with the License. You may obtain a copy of the License at 8 | * 9 | * https://opensource.org/licenses/BSD-3-Clause 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is 12 | * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tinker.deeson.mytinkerdemo; 18 | 19 | import android.annotation.TargetApi; 20 | import android.app.Application; 21 | import android.content.Context; 22 | import android.content.Intent; 23 | import android.os.Build; 24 | import android.support.multidex.MultiDex; 25 | import android.widget.Toast; 26 | 27 | import com.tencent.tinker.anno.DefaultLifeCycle; 28 | import com.tencent.tinker.lib.listener.DefaultPatchListener; 29 | import com.tencent.tinker.lib.patch.UpgradePatch; 30 | import com.tencent.tinker.lib.reporter.DefaultLoadReporter; 31 | import com.tencent.tinker.lib.reporter.DefaultPatchReporter; 32 | import com.tencent.tinker.lib.tinker.Tinker; 33 | import com.tencent.tinker.lib.tinker.TinkerInstaller; 34 | import com.tencent.tinker.loader.app.DefaultApplicationLike; 35 | import com.tencent.tinker.loader.shareutil.ShareConstants; 36 | 37 | 38 | @SuppressWarnings("unused") 39 | @DefaultLifeCycle(application = "com.tinker.deeson.mytinkerdemo.SampleApplication", 40 | flags = ShareConstants.TINKER_ENABLE_ALL, 41 | loadVerifyFlag = false) 42 | public class SampleApplicationLike extends DefaultApplicationLike { 43 | private static final String TAG = "Tinker.SampleApplicationLike"; 44 | 45 | public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, 46 | long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) { 47 | super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent); 48 | } 49 | 50 | /** 51 | * install multiDex before install tinker 52 | * so we don't need to put the tinker lib classes in the main dex 53 | * 54 | * @param base 55 | */ 56 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 57 | @Override 58 | public void onBaseContextAttached(Context base) { 59 | super.onBaseContextAttached(base); 60 | //you must install multiDex whatever tinker is installed! 61 | MultiDex.install(base); 62 | TinkerInstaller.install(this,new DefaultLoadReporter(getApplication()) 63 | ,new DefaultPatchReporter(getApplication()),new DefaultPatchListener(getApplication()),SampleResultService.class,new UpgradePatch()); 64 | Tinker tinker = Tinker.with(getApplication()); 65 | 66 | Toast.makeText(getApplication(),"加载完成", Toast.LENGTH_SHORT).show(); 67 | } 68 | 69 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 70 | public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) { 71 | getApplication().registerActivityLifecycleCallbacks(callback); 72 | } 73 | 74 | @Override 75 | public void onCreate() { 76 | super.onCreate(); 77 | //此处写自己的Application逻辑 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/tinker/deeson/mytinkerdemo/SampleResultService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Tencent is pleased to support the open source community by making Tinker available. 3 | * 4 | * Copyright (C) 2016 THL A29 Limited, a Tencent company. All rights reserved. 5 | * 6 | * Licensed under the BSD 3-Clause License (the "License"); you may not use this file except in 7 | * compliance with the License. You may obtain a copy of the License at 8 | * 9 | * https://opensource.org/licenses/BSD-3-Clause 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is 12 | * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tinker.deeson.mytinkerdemo; 18 | 19 | import android.content.BroadcastReceiver; 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.content.IntentFilter; 23 | import android.os.Handler; 24 | import android.os.Looper; 25 | import android.widget.Toast; 26 | 27 | import com.tencent.tinker.lib.service.DefaultTinkerResultService; 28 | import com.tencent.tinker.lib.service.PatchResult; 29 | import com.tencent.tinker.lib.util.TinkerLog; 30 | import com.tencent.tinker.lib.util.TinkerServiceInternals; 31 | import com.tencent.tinker.loader.shareutil.SharePatchFileUtil; 32 | 33 | import java.io.File; 34 | 35 | 36 | /** 37 | * optional, you can just use DefaultTinkerResultService 38 | * we can restart process when we are at background or screen off 39 | */ 40 | public class SampleResultService extends DefaultTinkerResultService { 41 | private static final String TAG = "Tinker.SampleResultService"; 42 | 43 | 44 | @Override 45 | public void onPatchResult(final PatchResult result) { 46 | if (result == null) { 47 | TinkerLog.e(TAG, "SampleResultService received null result!!!!"); 48 | return; 49 | } 50 | TinkerLog.i(TAG, "SampleResultService receive result: %s", result.toString()); 51 | 52 | //first, we want to kill the recover process 53 | TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext()); 54 | 55 | Handler handler = new Handler(Looper.getMainLooper()); 56 | handler.post(new Runnable() { 57 | @Override 58 | public void run() { 59 | if (result.isSuccess) { 60 | Toast.makeText(getApplicationContext(), "patch success, please restart process", Toast.LENGTH_LONG).show(); 61 | } else { 62 | Toast.makeText(getApplicationContext(), "patch fail, please check reason", Toast.LENGTH_LONG).show(); 63 | } 64 | } 65 | }); 66 | // is success and newPatch, it is nice to delete the raw file, and restart at once 67 | // for old patch, you can't delete the patch file 68 | if (result.isSuccess) { 69 | File rawFile = new File(result.rawPatchFilePath); 70 | if (rawFile.exists()) { 71 | TinkerLog.i(TAG, "save delete raw patch file"); 72 | SharePatchFileUtil.safeDeleteFile(rawFile); 73 | } 74 | //not like TinkerResultService, I want to restart just when I am at background! 75 | //if you have not install tinker this moment, you can use TinkerApplicationHelper api 76 | if (checkIfNeedKill(result)) { 77 | if (Utils.isBackground()) { 78 | TinkerLog.i(TAG, "it is in background, just restart process"); 79 | restartProcess(); 80 | } else { 81 | //we can wait process at background, such as onAppBackground 82 | //or we can restart when the screen off 83 | TinkerLog.i(TAG, "tinker wait screen to restart process"); 84 | new ScreenState(getApplicationContext(), new ScreenState.IOnScreenOff() { 85 | @Override 86 | public void onScreenOff() { 87 | restartProcess(); 88 | } 89 | }); 90 | } 91 | } else { 92 | TinkerLog.i(TAG, "I have already install the newly patch version!"); 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * you can restart your process through service or broadcast 99 | */ 100 | private void restartProcess() { 101 | TinkerLog.i(TAG, "app is background now, i can kill quietly"); 102 | //you can send service or broadcast intent to restart your process 103 | android.os.Process.killProcess(android.os.Process.myPid()); 104 | } 105 | 106 | static class ScreenState { 107 | interface IOnScreenOff { 108 | void onScreenOff(); 109 | } 110 | 111 | ScreenState(Context context, final IOnScreenOff onScreenOffInterface) { 112 | IntentFilter filter = new IntentFilter(); 113 | filter.addAction(Intent.ACTION_SCREEN_OFF); 114 | context.registerReceiver(new BroadcastReceiver() { 115 | 116 | @Override 117 | public void onReceive(Context context, Intent in) { 118 | String action = in == null ? "" : in.getAction(); 119 | TinkerLog.i(TAG, "ScreenReceiver action [%s] ", action); 120 | if (Intent.ACTION_SCREEN_OFF.equals(action)) { 121 | 122 | context.unregisterReceiver(this); 123 | 124 | if (onScreenOffInterface != null) { 125 | onScreenOffInterface.onScreenOff(); 126 | } 127 | } 128 | } 129 | }, filter); 130 | } 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/java/com/tinker/deeson/mytinkerdemo/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Tencent is pleased to support the open source community by making Tinker available. 3 | * 4 | * Copyright (C) 2016 THL A29 Limited, a Tencent company. All rights reserved. 5 | * 6 | * Licensed under the BSD 3-Clause License (the "License"); you may not use this file except in 7 | * compliance with the License. You may obtain a copy of the License at 8 | * 9 | * https://opensource.org/licenses/BSD-3-Clause 10 | * 11 | * Unless required by applicable law or agreed to in writing, software distributed under the License is 12 | * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 13 | * either express or implied. See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tinker.deeson.mytinkerdemo; 18 | 19 | import android.os.Environment; 20 | import android.os.StatFs; 21 | 22 | import com.tencent.tinker.loader.shareutil.ShareConstants; 23 | 24 | import java.io.ByteArrayOutputStream; 25 | import java.io.File; 26 | import java.io.IOException; 27 | import java.io.PrintStream; 28 | 29 | public class Utils { 30 | 31 | /** 32 | * the error code define by myself 33 | * should after {@code ShareConstants.ERROR_PATCH_INSERVICE 34 | */ 35 | public static final int ERROR_PATCH_GOOGLEPLAY_CHANNEL = -5; 36 | public static final int ERROR_PATCH_ROM_SPACE = -6; 37 | public static final int ERROR_PATCH_MEMORY_LIMIT = -7; 38 | public static final int ERROR_PATCH_ALREADY_APPLY = -8; 39 | public static final int ERROR_PATCH_CRASH_LIMIT = -9; 40 | public static final int ERROR_PATCH_RETRY_COUNT_LIMIT = -10; 41 | public static final int ERROR_PATCH_CONDITION_NOT_SATISFIED = -11; 42 | 43 | public static final String PLATFORM = "platform"; 44 | 45 | public static final int MIN_MEMORY_HEAP_SIZE = 45; 46 | 47 | private static boolean background = false; 48 | 49 | public static boolean isGooglePlay() { 50 | return false; 51 | } 52 | 53 | public static boolean isBackground() { 54 | return background; 55 | } 56 | 57 | public static void setBackground(boolean back) { 58 | background = back; 59 | } 60 | 61 | public static int checkForPatchRecover(long roomSize, int maxMemory) { 62 | if (Utils.isGooglePlay()) { 63 | return Utils.ERROR_PATCH_GOOGLEPLAY_CHANNEL; 64 | } 65 | if (maxMemory < MIN_MEMORY_HEAP_SIZE) { 66 | return Utils.ERROR_PATCH_MEMORY_LIMIT; 67 | } 68 | //or you can mention user to clean their rom space! 69 | if (!checkRomSpaceEnough(roomSize)) { 70 | return Utils.ERROR_PATCH_ROM_SPACE; 71 | } 72 | 73 | return ShareConstants.ERROR_PATCH_OK; 74 | } 75 | 76 | public static boolean isXposedExists(Throwable thr) { 77 | StackTraceElement[] stackTraces = thr.getStackTrace(); 78 | for (StackTraceElement stackTrace : stackTraces) { 79 | final String clazzName = stackTrace.getClassName(); 80 | if (clazzName != null && clazzName.contains("de.robv.android.xposed.XposedBridge")) { 81 | return true; 82 | } 83 | } 84 | return false; 85 | } 86 | 87 | @Deprecated 88 | public static boolean checkRomSpaceEnough(long limitSize) { 89 | long allSize; 90 | long availableSize = 0; 91 | try { 92 | File data = Environment.getDataDirectory(); 93 | StatFs sf = new StatFs(data.getPath()); 94 | availableSize = (long) sf.getAvailableBlocks() * (long) sf.getBlockSize(); 95 | allSize = (long) sf.getBlockCount() * (long) sf.getBlockSize(); 96 | } catch (Exception e) { 97 | allSize = 0; 98 | } 99 | 100 | if (allSize != 0 && availableSize > limitSize) { 101 | return true; 102 | } 103 | return false; 104 | } 105 | 106 | public static String getExceptionCauseString(final Throwable ex) { 107 | final ByteArrayOutputStream bos = new ByteArrayOutputStream(); 108 | final PrintStream ps = new PrintStream(bos); 109 | 110 | try { 111 | // print directly 112 | Throwable t = ex; 113 | while (t.getCause() != null) { 114 | t = t.getCause(); 115 | } 116 | t.printStackTrace(ps); 117 | return toVisualString(bos.toString()); 118 | } finally { 119 | try { 120 | bos.close(); 121 | } catch (IOException e) { 122 | e.printStackTrace(); 123 | } 124 | } 125 | } 126 | 127 | private static String toVisualString(String src) { 128 | boolean cutFlg = false; 129 | 130 | if (null == src) { 131 | return null; 132 | } 133 | 134 | char[] chr = src.toCharArray(); 135 | if (null == chr) { 136 | return null; 137 | } 138 | 139 | int i = 0; 140 | for (; i < chr.length; i++) { 141 | if (chr[i] > 127) { 142 | chr[i] = 0; 143 | cutFlg = true; 144 | break; 145 | } 146 | } 147 | 148 | if (cutFlg) { 149 | return new String(chr, 0, i); 150 | } else { 151 | return src; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 |