首页 > 系统 > Android > 正文

Android微信Tinker热更新详细使用

2019-10-23 19:52:50
字体:
来源:转载
供稿:网友

先看一下效果图

微信,Tinker,热更新

Tinker已知问题

由于原理与系统限制,Tinker有以下已知问题:

  • Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件;
  • 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
  • 在Android N上,补丁对应用启动时间有轻微的影响;
  • 不支持部分三星android-21机型,加载补丁时会主动抛出”TinkerRuntimeException:checkDexInstall failed”;
  • 由于各个厂商的加固实现并不一致,在1.7.6以及之后的版本,tinker不再支持加固的动态更新;
  • 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。

1.首先在项目的build中,集成tinker插件 ,如下所示(目前最新版是1.7.6)

先看结构图,只有几个类而已:

微信,Tinker,热更新

项目中的build集成

buildscript { repositories {  jcenter() } dependencies {  classpath 'com.android.tools.build:gradle:2.2.3'  classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.7.6')  // NOTE: Do not place your application dependencies here; they belong  // in the individual module build.gradle files }}allprojects { repositories {  jcenter() }}task clean(type: Delete) { delete rootProject.buildDir}

1.再将app的build中的关联属性添加进去,这些属性都是经过测试过的,都有注释显示,如果自己需要其他属性,可以自己去github上查看并集成,文章末尾会送上地址,ps:官方的集成特别麻烦,有时候一整天都有可能搞不定,根据自己的需求和情况来添加,末尾会送上demo

apply plugin: 'com.android.application'def javaVersion = JavaVersion.VERSION_1_7android { compileSdkVersion 23 buildToolsVersion "23.0.2" compileOptions {  sourceCompatibility javaVersion  targetCompatibility javaVersion } //recommend dexOptions {  jumboMode = true } defaultConfig {  applicationId "com.tinker.demo.tinkerdemo"  minSdkVersion 15  targetSdkVersion 22  versionCode 1  versionName "1.0"  testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"  buildConfigField "String", "MESSAGE", "/"I am the base apk/""  buildConfigField "String", "TINKER_ID", "/"${getTinkerIdValue()}/""  buildConfigField "String", "PLATFORM", "/"all/"" }  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'), 'proguard-rules.pro'  }  debug {   debuggable true   minifyEnabled false   signingConfig signingConfigs.debug  } } sourceSets {  main {   jniLibs.srcDirs = ['libs']  } }}dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {  exclude group: 'com.android.support', module: 'support-annotations' }) compile "com.android.support:appcompat-v7:23.1.1" testCompile 'junit:junit:4.12' compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true } provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true } compile "com.android.support:multidex:1.0.1"}def gitSha() { try {  // String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()  String gitRev = "1008611"  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'") }}def bakPath = file("${buildDir}/bakApk/")ext { //for some reason, you may want to ignore tinkerBuild, such as instant run debug build? tinkerEnabled = true //for normal build //old apk file to build patch apk tinkerOldApkPath = "${bakPath}/app-debug-0113-14-01-29.apk" //proguard mapping file to build patch apk tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt" //resource R.txt to build patch apk, must input if there is resource changed tinkerApplyResourcePath = "${bakPath}/app-debug-0113-14-01-29-R.txt" //only use for build all flavor, if not, just ignore this field 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 {  /**   * 默认为null   * 将旧的apk和新的apk建立关联   * 从build / bakApk添加apk   */  oldApk = getOldApkPath()  /**   * 可选,默认'false'   *有些情况下我们可能会收到一些警告   *如果ignoreWarning为true,我们只是断言补丁过程   * case 1:minSdkVersion低于14,但是你使用dexMode与raw。   * case 2:在AndroidManifest.xml中新添加Android组件,   * case 3:装载器类在dex.loader {}不保留在主要的dex,   * 它必须让tinker不工作。   * case 4:在dex.loader {}中的loader类改变,   * 加载器类是加载补丁dex。改变它们是没有用的。   * 它不会崩溃,但这些更改不会影响。你可以忽略它   * case 5:resources.arsc已经改变,但是我们不使用applyResourceMapping来构建   */  ignoreWarning = false  /**   *可选,默认为“true”   * 是否签名补丁文件   * 如果没有,你必须自己做。否则在补丁加载过程中无法检查成功   * 我们将使用sign配置与您的构建类型   */  useSign = true  /**   可选,默认为“true”   是否使用tinker构建   */  tinkerEnable = buildWithTinker()  /**   * 警告,applyMapping会影响正常的android build!   */  buildConfig {   /**    *可选,默认为'null'    * 如果我们使用tinkerPatch构建补丁apk,你最好应用旧的    * apk映射文件如果minifyEnabled是启用!    * 警告:你必须小心,它会影响正常的组装构建!    */   applyMapping = getApplyMappingPath()   /**    *可选,默认为'null'    * 很高兴保持资源ID从R.txt文件,以减少java更改    */   applyResourceMapping = getApplyResourceMappingPath()   /**    *必需,默认'null'    * 因为我们不想检查基地apk与md5在运行时(它是慢)    * tinkerId用于在试图应用补丁时标识唯一的基本apk。    * 我们可以使用git rev,svn rev或者简单的versionCode。    * 我们将在您的清单中自动生成tinkerId    */   tinkerId = getTinkerIdValue()   /**    *如果keepDexApply为true,则表示dex指向旧apk的类。    * 打开这可以减少dex diff文件大小。    */   keepDexApply = false  }  dex {   /**    *可选,默认'jar'    * 只能是'raw'或'jar'。对于原始,我们将保持其原始格式    * 对于jar,我们将使用zip格式重新包装dexes。    * 如果你想支持下面14,你必须使用jar    * 或者你想保存rom或检查更快,你也可以使用原始模式    */   dexMode = "jar"   /**    *必需,默认'[]'    * apk中的dexes应该处理tinkerPatch    * 它支持*或?模式。    */   pattern = ["classes*.dex",      "assets/secondary-dex-?.jar"]   /**    *必需,默认'[]'    * 警告,这是非常非常重要的,加载类不能随补丁改变。    * 因此,它们将从补丁程序中删除。    * 你必须把下面的类放到主要的dex。    * 简单地说,你应该添加自己的应用程序{@code tinker.sample.android.SampleApplication}    * 自己的tinkerLoader,和你使用的类    *    */   loader = [     //use sample, let BaseBuildInfo unchangeable with tinker     "tinker.sample.android.app.BaseBuildInfo"   ]  }  lib {   /**    可选,默认'[]'    apk中的图书馆应该处理tinkerPatch    它支持*或?模式。    对于资源库,我们只是在补丁目录中恢复它们    你可以得到他们在TinkerLoadResult与Tinker    */   pattern = ["lib/armeabi/*.so"]  }  res {   /**    *可选,默认'[]'    * apk中的什么资源应该处理tinkerPatch    * 它支持*或?模式。    * 你必须包括你在这里的所有资源,    * 否则,他们不会重新包装在新的apk资源。    */   pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]   /**    *可选,默认'[]'    *资源文件排除模式,忽略添加,删除或修改资源更改    * *它支持*或?模式。    * *警告,我们只能使用文件没有relative与resources.arsc    */   ignoreChange = ["assets/sample_meta.txt"]   /**    *默认100kb    * *对于修改资源,如果它大于'largeModSize'    * *我们想使用bsdiff算法来减少补丁文件的大小    */   largeModSize = 100  }  packageConfig {   /**    *可选,默认'TINKER_ID,TINKER_ID_VALUE','NEW_TINKER_ID,NEW_TINKER_ID_VALUE'    * 包元文件gen。路径是修补程序文件中的assets / package_meta.txt    * 你可以在您自己的PackageCheck方法中使用securityCheck.getPackageProperties()    * 或TinkerLoadResult.getPackageConfigByName    * 我们将从旧的apk清单为您自动获取TINKER_ID,    * 其他配置文件(如下面的patchMessage)不是必需的    */   configField("patchMessage", "tinker is sample to use")   /**    *只是一个例子,你可以使用如sdkVersion,品牌,渠道...    * 你可以在SamplePatchListener中解析它。    * 然后你可以使用补丁条件!    */   configField("platform", "all")   /**    * 补丁版本通过packageConfig    */   configField("patchVersion", "1.0")  }  //或者您可以添加外部的配置文件,或从旧apk获取元值  //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))  //project.tinkerPatch.packageConfig.configField("test2", "sample")  /**   * 如果你不使用zipArtifact或者path,我们只是使用7za来试试   */  sevenZip {   /**    * 可选,默认'7za'    * 7zip工件路径,它将使用正确的7za与您的平台    */   zipArtifact = "com.tencent.mm:SevenZip:1.1.10"   /**    * 可选,默认'7za'    * 你可以自己指定7za路径,它将覆盖zipArtifact值    *///  path = "/usr/local/bin/7za"  } } List<String> flavors = new ArrayList<>(); project.android.productFlavors.each {flavor ->  flavors.add(flavor.name) } boolean hasFlavors = flavors.size() > 0 /**  * bak apk and mapping  */ 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"     }    }   }  } }}

3.在清单文件中集成application和服务 ,name的application必须是.AMSKY,如果你添加不进去,或者是红色的话,请先build一下,如果你已经有了自己的application,后面我会说怎么来集成,Service中做的操作是在你加载成功热更新插件后,会提示你更新成功,并且这里做了锁屏操作就会加载热更新插件,继续往下看。

 

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tinker.demo.tinkerdemo"> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application  android:allowBackup="true"  android:icon="@mipmap/ic_launcher"  android:label="@string/app_name"  android:supportsRtl="true"  android:name=".AMSKY"  android:theme="@style/AppTheme">  <service   android:name=".service.SampleResultService"   android:exported="false"/>  <activity android:name=".MainActivity">   <intent-filter>    <action android:name="android.intent.action.MAIN" />    <category android:name="android.intent.category.LAUNCHER" />   </intent-filter>  </activity> </application></manifest>

4.到这里就已经基本集成的差不多了,剩下的就是代码里面的集成,首先是application,这里主要说如果是自已已经存在的application的时候改怎么操作 ,这个applicaiton可以说就是自己的一个application,只不过写法,要这样去写,可以在onCreate中做自己的一些操作,只不过清单文件中,要写AMSKY

 

@SuppressWarnings("unused")@DefaultLifeCycle(application = "com.tinker.demo.tinkerdemo.AMSKY",     flags = ShareConstants.TINKER_ENABLE_ALL,     loadVerifyFlag = false)public class SampleApplicationLike extends DefaultApplicationLike { private static final String TAG = "Tinker.SampleApplicationLike";public SampleApplicationLike(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); } /**  * install multiDex before install tinker  * so we don't need to put the tinker lib classes in the main dex  *  * @param base  */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override public void onBaseContextAttached(Context base) {  super.onBaseContextAttached(base);  //MultiDex必须在Tinker初始化之前  MultiDex.install(base);  //这里就是初始化Tinker  TinkerInstaller.install(this,new DefaultLoadReporter(getApplication()),new DefaultPatchReporter(getApplication()),  new DefaultPatchListener(getApplication()),SampleResultService.class,new UpgradePatch());  Tinker tinker = Tinker.with(getApplication());  //这个只是一个Toast提示  Toast.makeText(  getApplication(),"没鸟用,就是Toast提示而已", Toast.LENGTH_SHORT).show(); } @Override public void onCreate() {  super.onCreate();  //这里可以做自己的操作 } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {  getApplication().registerActivityLifecycleCallbacks(callback); }}

5.这里就是在MainActivity中来加载热更新文件,在点击加载的时候,就直接锁屏加载(不要删除service),当然退出app,下次进来也是可以加载的吗,这里加载补丁插件的话,路径可以自己设置,我是放在根目录的debug文件夹当中的,并且我的补丁插件名字叫patch,可以自行更改。

 

public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) {  super.onCreate(savedInstanceState);  setContentView(R.layout.activity_main); } /**  * 加载热补丁插件  * @param v  */ public void loadPatch(View v) {  TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), "/sdcard/debug/patch.apk"); } /**  * 杀死应用加载补丁  * @param v  */ public void killApp(View v) {  ShareTinkerInternals.killAllOtherProcess(getApplicationContext());  android.os.Process.killProcess(android.os.Process.myPid()); } @Override protected void onResume() {  super.onResume();  Utils.setBackground(false); } @Override protected void onPause() {  super.onPause();  Utils.setBackground(true); }}

6.Service文件

 

public class SampleResultService extends DefaultTinkerResultService { private static final String TAG = "Tinker.SampleResultService"; @Override public void onPatchResult(final PatchResult result) {  if (result == null) {   TinkerLog.e(TAG, "SampleResultService received null result!!!!");   return;  }  TinkerLog.i(TAG, "SampleResultService receive result: %s", result.toString());  //first, we want to kill the recover process  TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());  Handler handler = new Handler(Looper.getMainLooper());  handler.post(new Runnable() {   @Override   public void run() {    if (result.isSuccess) {     Toast.makeText(getApplicationContext(), "patch success, please restart process", Toast.LENGTH_LONG).show();    } else {     Toast.makeText(getApplicationContext(), "patch fail, please check reason", Toast.LENGTH_LONG).show();    }   }  });  // is success and newPatch, it is nice to delete the raw file, and restart at once  // for old patch, you can't delete the patch file  if (result.isSuccess) {   File rawFile = new File(result.rawPatchFilePath);   if (rawFile.exists()) {    TinkerLog.i(TAG, "save delete raw patch file");    SharePatchFileUtil.safeDeleteFile(rawFile);   }   //not like TinkerResultService, I want to restart just when I am at background!   //if you have not install tinker this moment, you can use TinkerApplicationHelper api   if (checkIfNeedKill(result)) {    if (Utils.isBackground()) {     TinkerLog.i(TAG, "it is in background, just restart process");     restartProcess();    } else {     //we can wait process at background, such as onAppBackground     //or we can restart when the screen off     TinkerLog.i(TAG, "tinker wait screen to restart process");     new ScreenState(getApplicationContext(), new ScreenState.IOnScreenOff() {      @Override      public void onScreenOff() {       restartProcess();      }     });    }   } else {    TinkerLog.i(TAG, "I have already install the newly patch version!");   }  } } /**  * you can restart your process through service or broadcast  */ private void restartProcess() {  TinkerLog.i(TAG, "app is background now, i can kill quietly");  //you can send service or broadcast intent to restart your process  android.os.Process.killProcess(android.os.Process.myPid()); } static class ScreenState {  interface IOnScreenOff {   void onScreenOff();  }  ScreenState(Context context, final IOnScreenOff onScreenOffInterface) {   IntentFilter filter = new IntentFilter();   filter.addAction(Intent.ACTION_SCREEN_OFF);   context.registerReceiver(new BroadcastReceiver() {    @Override    public void onReceive(Context context, Intent in) {     String action = in == null ? "" : in.getAction();     TinkerLog.i(TAG, "ScreenReceiver action [%s] ", action);     if (Intent.ACTION_SCREEN_OFF.equals(action)) {      context.unregisterReceiver(this);      if (onScreenOffInterface != null) {       onScreenOffInterface.onScreenOff();      }     }    }   }, filter);  } }}

7.Utils文件

public class Utils { /**  * the error code define by myself  * should after {@code ShareConstants.ERROR_PATCH_INSERVICE  */ public static final int ERROR_PATCH_GOOGLEPLAY_CHANNEL  = -5; public static final int ERROR_PATCH_ROM_SPACE    = -6; public static final int ERROR_PATCH_MEMORY_LIMIT   = -7; public static final int ERROR_PATCH_ALREADY_APPLY   = -8; public static final int ERROR_PATCH_CRASH_LIMIT    = -9; public static final int ERROR_PATCH_RETRY_COUNT_LIMIT  = -10; public static final int ERROR_PATCH_CONDITION_NOT_SATISFIED = -11; public static final String PLATFORM = "platform"; public static final int MIN_MEMORY_HEAP_SIZE = 45; private static boolean background = false; public static boolean isGooglePlay() {  return false; } public static boolean isBackground() {  return background; } public static void setBackground(boolean back) {  background = back; } public static int checkForPatchRecover(long roomSize, int maxMemory) {  if (Utils.isGooglePlay()) {   return Utils.ERROR_PATCH_GOOGLEPLAY_CHANNEL;  }  if (maxMemory < MIN_MEMORY_HEAP_SIZE) {   return Utils.ERROR_PATCH_MEMORY_LIMIT;  }  //or you can mention user to clean their rom space!  if (!checkRomSpaceEnough(roomSize)) {   return Utils.ERROR_PATCH_ROM_SPACE;  }  return ShareConstants.ERROR_PATCH_OK; } public static boolean isXposedExists(Throwable thr) {  StackTraceElement[] stackTraces = thr.getStackTrace();  for (StackTraceElement stackTrace : stackTraces) {   final String clazzName = stackTrace.getClassName();   if (clazzName != null && clazzName.contains("de.robv.android.xposed.XposedBridge")) {    return true;   }  }  return false; } @Deprecated public static boolean checkRomSpaceEnough(long limitSize) {  long allSize;  long availableSize = 0;  try {   File data = Environment.getDataDirectory();   StatFs sf = new StatFs(data.getPath());   availableSize = (long) sf.getAvailableBlocks() * (long) sf.getBlockSize();   allSize = (long) sf.getBlockCount() * (long) sf.getBlockSize();  } catch (Exception e) {   allSize = 0;  }  if (allSize != 0 && availableSize > limitSize) {   return true;  }  return false; } public static String getExceptionCauseString(final Throwable ex) {  final ByteArrayOutputStream bos = new ByteArrayOutputStream();  final PrintStream ps = new PrintStream(bos);  try {   // print directly   Throwable t = ex;   while (t.getCause() != null) {    t = t.getCause();   }   t.printStackTrace(ps);   return toVisualString(bos.toString());  } finally {   try {    bos.close();   } catch (IOException e) {    e.printStackTrace();   }  } } private static String toVisualString(String src) {  boolean cutFlg = false;  if (null == src) {   return null;  }  char[] chr = src.toCharArray();  if (null == chr) {   return null;  }  int i = 0;  for (; i < chr.length; i++) {   if (chr[i] > 127) {    chr[i] = 0;    cutFlg = true;    break;   }  }  if (cutFlg) {   return new String(chr, 0, i);  } else {   return src;  } }}

到这里就已经集成完毕,下面来说下使用的方法

这是有bug的版本,我们测试就使用assembleDebug来测试 ,注意没点击assembleDebug之前,build文件夹里面是没有bakApk文件夹的

微信,Tinker,热更新微信,Tinker,热更新

2.点击assembleDebug之后会出现bakApk这个文件夹,里面就有apk文件,如果失败,记得clean一下,然后build一下

微信,Tinker,热更新

3.接下来在build文件夹里面,更改ext中的属性,将bakApk中生成的apk文件和R文件复制到ext这里,如果你打的release包有mapping的话同样复制到这里,我们这里是debug测试,所以没有mapping文件

微信,Tinker,热更新

4.下面就修改我们需要更新,或者更改的bug,我这里是添加一张图片,并且更改标题显示

这是有bug的版本,我还没添加图片,更改标题

微信,Tinker,热更新

这里我添加了一张aa的图片,并且更改了标题

微信,Tinker,热更新

5.接下来我们运行tinker下面的tinkerPatchDebug,来生成补丁包,这个补丁包在outputs下面

微信,Tinker,热更新

点击完成后,就会生成tinkerPatch文件夹

微信,Tinker,热更新

将tinkerPatch文件夹下面的patch_signed_7zip.apk文件,粘贴出来,改成你的MainActivity中加载的文件名字,我这里叫patch,然后点击加载没加载之前

微信,Tinker,热更新

加载之后,锁频,解锁 ,补丁已经加载出来了,并且文件夹中的补丁已经不在了,因为它和老apk合并了

微信,Tinker,热更新

注意

签名文件的话 在build的signingConfigs中设置,以及左侧的kestore文件夹中设置 ,如下图
微信,Tinker,热更新
微信,Tinker,热更新

项目github地址:TinkerDemo

 

Tinker原项目地址https://github.com/Tencent/tinker
Tinker使用指南:https://github.com/Tencent/tinker/wiki
Tinker一键集成(这个简单,但是不能从自己服务器上下载补丁,不需配置Tinker自己的后台,有部分局限性,自行选择):https://github.com/TinkerPatch/tinkerpatch-sdk/blob/master/docs/tinkerpatch-android-sdk.md
Tinker一键集成后台http://www.tinkerpatch.com/


注:相关教程知识阅读请移步到Android开发频道。
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表