Android 热修复与插件化 二

一、什么是热修复

  1. 顾名思义,动态的修复或者更新我们的APP的行为,有时候会被称为动态更新

二、热修复的好处

  1. 以前APP修复BUG,或者新添一些小功能,只能通过发布一个版本,覆盖安装才能解决问题,过程成本非常高,严重的话可能导致用户流失

  2. 现在通过热修复,就能无感修复一些BUG或者添加一些小功能

  3. 热修复其实是一种亡羊补牢的手段,热修复和发布的正式版一样,都要经过一些列正规测试

三、流行技术

  1. QQ空间的超级补丁方案

  2. 微信的Tinker

  3. 阿里的AndFix 、dexposed

  4. 美团的Robust 、ele的migo 、百度的hotfix等

  5. 技术对比

      Tinker QZone AndFix Robust
    类替换 yes yes no no
    SO替换 yes no no no
    资源替换 yes yes no no
    全平台支持 yes yes yes yes
    即时生效 no no yes yes
    性能损耗 较小 较大 较小 较小
    补丁大小 较小 较大 一般 一般
    开发透明 yes yes no no
    复杂度 较低 较低 复杂 复杂
    gradle支持 yes no no no
    Rom体积 较大 较小 较小 较小
    成功率 较高 较高 一般 最高
  6. 技术选型

    1. 看需求

    2. 学习成本低,使用简答

    3. 优选大公司方案

四、AndFix https://github.com/alibaba/AndFix

作为一个Android依赖库使用,支持Android版本2.3到7.0,ARM和x86架构,Dalvik和ART,32bit和64bit

  1. 原理:对方法完成一个替换,使得有bug的方法永远不会执行到,从而完成Bug的修复

  2. 集成阶段

    • gradle中添加AndFix依赖,注意是app中的gradle,不是工程的gradle,因为app下的gradle,说白了都是一些方法和类,如果仅仅是Java方法、类,那么就在app的gradle写compile或者implementation,如果包含一些gradle脚本,那就需要在工程的gradle的dependencies写入classpath

      dependencies {
          compile 'com.alipay.euler:andfix:0.5.0@aar'
      }
    • 在代码中完成对AndFix的初始化

       patchManager = new PatchManager(context);
       patchManager.init(Utils.getVersionName(context));
       patchManager.loadPatch();
       //官网上建议以上代码在Application的onCreate()方法中执行,加载AndFix模块
  3. 安装BUG APK 修复APK阶段

    • 生成release 版本,安装在手机上

      //1.新建一个按钮,点击闪退
      String error = null;
      Log.d(TAG,error)
      //2.新建一个按钮,点击修复
      patchManager.loadPatch(path);//path是指你存放.apatch文件的路径
      //生成release版本
      ./gradlew assembleRelease
    • 生成apatch文件

      • 修改闪退的按钮的点击事件,例如弹出千年Hello World

      • 官网Github上下载apkpatch-tools

      • apkpatch -f 生成一个apatch文件

        apkpatch -m 合并多个apatch文件

      • apkpatch -f new.apk -t old.apk -o outputs/ -k android.jks -p android -a android -e android (outputs目录下会有.apatch文件,具体命令含义直接输入apkpatch,会将 -f/-m 参数列出)

    • 修复BUG

      • 将生成的.apatch文件放到loadPatch方法制定的目录(实际上会将这部分组件化,方便复用,其中apatch文件可以通过网络每次比较然后下载到指定的路径下,然后调用loadPatch方法)

      • 点击之前新建的按钮(loadPatch()加载apatch文件),然后点击闪退的那个按钮,会发现弹出Hello World

  4. AndFix源码分析

    1. 初始化代码通过Android Studio一步一步点进去,都是一些类的初始化,没什么好讲的

    2. 主要代码就在得到patch文件后的loadPatch(String path)方法里,它会调用内部的loadPatch(Patch patch)方法,最后调用的是mAndFixManager.fix(File ,ClassLoader,List)方法,这个是替换方法的重要代码,具体可以查看以下代码,可以发现后半部分代码和Android的ClassLoader的loadClass方法里代码有点类似。

          /**
           * fix
           * 
           * @param file
           *            patch file
           * @param classLoader
           *            classloader of class that will be fixed
           * @param classes
           *            classes will be fixed
           */
          public synchronized void fix(File file, ClassLoader classLoader,
                  List classes) {
              if (!mSupport) {
                  return;
              }
      ​
              if (!mSecurityChecker.verifyApk(file)) {// security check fail
                  return;
              }
      ​
              try {
                  File optfile = new File(mOptDir, file.getName());
                  boolean saveFingerprint = true;
                  if (optfile.exists()) {
                      // need to verify fingerprint when the optimize file exist,
                      // prevent someone attack on jailbreak device with
                      // Vulnerability-Parasyte.
                      // btw:exaggerated android Vulnerability-Parasyte
                      // http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html
                      if (mSecurityChecker.verifyOpt(optfile)) {
                          saveFingerprint = false;
                      } else if (!optfile.delete()) {
                          return;
                      }
                  }
      ​
                  final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
                          optfile.getAbsolutePath(), Context.MODE_PRIVATE);
      ​
                  if (saveFingerprint) {
                      mSecurityChecker.saveOptSig(optfile);
                  }
      ​
                  ClassLoader patchClassLoader = new ClassLoader(classLoader) {
                      @Override
                      protected Class findClass(String className)
                              throws ClassNotFoundException {
                          Class clazz = dexFile.loadClass(className, this);
                          if (clazz == null
                                  && className.startsWith("com.alipay.euler.andfix")) {
                              return Class.forName(className);// annotation’s class
                                                              // not found
                          }
                          if (clazz == null) {
                              throw new ClassNotFoundException(className);
                          }
                          return clazz;
                      }
                  };
                  Enumeration entrys = dexFile.entries();
                  Class clazz = null;
                  while (entrys.hasMoreElements()) {
                      String entry = entrys.nextElement();
                      if (classes != null && !classes.contains(entry)) {
                          continue;// skip, not need fix
                      }
                      //将名字转换成字节码
                      clazz = dexFile.loadClass(entry, patchClassLoader);
                      if (clazz != null) {
                          fixClass(clazz, classLoader);
                      }
                  }
              } catch (IOException e) {
                  Log.e(TAG, "pacth", e);
              }
          }

      最后在fixClass里面会通过native方法getAnnotationNative()寻找字节码中被注解的方法,最后通过native的replaceMethod(Method dest,Method src)方法替换,最后通过native的setFieldFlag()方法init

    3. 替换方法的实际操作都在阿里写的本地方法内,java层都是一些操作patch、查找方法等常规操作

    4. 原理简单,集成简单,使用简单,即时生效

    5. 只能修复方法级别的bug,极大的限制了使用场景

五、Tinker https://github.com/Tencent/tinker

Tinker是微信官方的Android热补丁方案,它支持动态下发代码,SO库以及资源,让应用能够再不需要重新安装的情况下实现更新,也可以使用Tinker更新插件

  1. 主要包括以下几部分

    • gradle编译插件:tinker-patch-gradle-plugin

    • 核心sdk库:tinker-android-lib

    • 非gradle编译用户的命令版本:tinker-patch-cli.jar

  2. 存在的已知问题

    • 不支持AndroidManifest.xml修改,不支持新增四大组件(1.9.0支持新增非export的Activity)

    • 由于Google Play的开发着条款限制,不建议在Google Play渠道动态更新代码

    • 在Android N (7.x) 上,补丁对应用启动时间有轻微的影响

    • 不支持部分三星android-21机型,加载补丁时会主动抛出 "TinkerRuntimeException:checkDexInstall failed"

    • 对于资源替换,不支持修改remoteView,例如transition动画,notification icon 以及桌面图标

  3. 快速接入Tinker,利用tinker-tools生成patch文件

    //1.gradle.properites
    TINKER_VERSION=1.9.9
    ​
    //2.build.gradle
    //optional, help to generate the final application
    compileOnly "com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}"
    //tinker's main Android lib
    implementation "com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}"
    ​
    //3.AndroidManifest.xml
    
      android:name="TINKER_ID"
      android:value="1.0" />
      
    //4.照着官网写,如下,然后再rebuild一下,就生成了MyTinkerApplication了,然后在AndroidManifest.xml
    //加入Application
    @DefaultLifeCycle(application = ".MyTinkerApplication",flags = ShareConstants.TINKER_ENABLE_ALL,loadVerifyFlag = false)
    public class MyTinkerApplicationLike extends ApplicationLike {
    ​
        public MyTinkerApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
            super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
        }
    ​
        @Override
        public void onBaseContextAttached(Context base) {
            super.onBaseContextAttached(base);
            //you must install multiDex whatever tinker is installed!
            MultiDex.install(base);
    ​
            TinkerManager.installTinker(this);//安装tinker
        }
    }
    //TinkerManager类
    public static void installTinker(ApplicationLike applicationLike){
          mAppLike = applicationLike;
          if(isInstalled) {
              return;
          }
    ​
          //完成Tinker的初始化
          TinkerInstaller.install(mAppLike);
          isInstalled = true;
      }
        
    //5.当然还需要加载patch的代码,这部分和AndFix类似
    //完成patch文件的加载
    public static void loadPath(String path){
       if(Tinker.isTinkerInstalled()){
           TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),path);
       }
    }
    private static Context getApplicationContext(){
        if(mAppLike != null){
            return mAppLike.getApplication().getApplicationContext();
         }
         return null;
     }
    //6. gradlew assembleRelease 生成一个APK
    ​
    //7.修改布局文件等,重新生成一个新的APK
    ​
    //8.官网 wiki 页面有tinker-patch-cli引导使用,下载
    //文件如下
    //tinker-patch-cli-1.7.7.jar
    //tinker_proguard.pro
    //tinker_multidexkeep.pro
    //tinker_config.xml
    //以上最重要的是tinker_config.xml,使用时需要将其中的issue标签内的最后一个loader便签的value设置为
    //我们自定义的MyTinkerApplication,将最后一个issue的storepass 等值设置为jks对应的正确值,毕竟生成
    //生成patch文件也需要签名
    ​
    //9.指定命令生成singed.apk
    java -jar tinker-patch-cli.jar -old old.apk -new new.apk -config tinker_config.xml -out output_path
    ​
    //10.大功告成,安装旧版APK,然后将patch放入指定路径,打开应用点击调用加载patch,当应用重启后,就会生效,当然实际上肯定是从服务端获取,对比md5检查,然后再patch
  4. gradle 生成patch文件

    • 首先和上面一样需要配置app 下的build.gradle,在工程的build.gradle的dependencies中需要配置

      classpath "com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}"
    • 然后新建一个buildTinker.gradle,配置一些有关tinker的配置,都是官网上的,直接复制过来用,其中包括多渠道打包等,地址 : https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97

      def bakPath = file("${buildDir}/bakApk/")
      def gitSha() {
          try {
              String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
              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'")
          }
      }
      /**
       * you can use assembleRelease to build you base apk
       * use tinkerPatchRelease -POLD_APK=  -PAPPLY_MAPPING=  -PAPPLY_RESOURCE= to build patch
       * add apk from the build/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}/"
          //proguard mapping file to build patch apk
          tinkerApplyMappingPath = "${bakPath}/"
          //resource R.txt to build patch apk, must input if there is resource changed
          tinkerApplyResourcePath = "${bakPath}/"
      ​
          //only use for build all flavor, if not, just ignore this field
          tinkerBuildFlavorDirectory = "${bakPath}/"
      }
      ​
      ​
      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") ? Boolean.parseBoolean(TINKER_ENABLE) : ext.tinkerEnabled
      }
      ​
      def getTinkerBuildFlavorDirectory() {
          return ext.tinkerBuildFlavorDirectory
      }
      ​
      if (buildWithTinker()) {
          apply plugin: 'com.tencent.tinker.patch'
      ​
          tinkerPatch {
              /**
               * necessary,default 'null'
               * the old apk path, use to diff with the new apk to build
               * add apk from the build/bakApk
               */
              oldApk = getOldApkPath()
              /**
               * optional,default 'false'
               * there are some cases we may get some warnings
               * if ignoreWarning is true, we would just assert the patch process
               * case 1: minSdkVersion is below 14, but you are using dexMode with raw.
               *         it must be crash when load.
               * case 2: newly added Android Component in AndroidManifest.xml,
               *         it must be crash when load.
               * case 3: loader classes in dex.loader{} are not keep in the main dex,
               *         it must be let tinker not work.
               * case 4: loader classes in dex.loader{} changes,
               *         loader classes is ues to load patch dex. it is useless to change them.
               *         it won't crash, but these changes can't effect. you may ignore it
               * case 5: resources.arsc has changed, but we don't use applyResourceMapping to build
               */
              ignoreWarning = false
      ​
              /**
               * optional,default 'true'
               * whether sign the patch file
               * if not, you must do yourself. otherwise it can't check success during the patch loading
               * we will use the sign config with your build type
               */
              useSign = true
      ​
              /**
               * optional,default 'true'
               * whether use tinker to build
               */
              tinkerEnable = buildWithTinker()
      ​
              /**
               * Warning, applyMapping will affect the normal android build!
               */
              buildConfig {
                  /**
                   * optional,default 'null'
                   * if we use tinkerPatch to build the patch apk, you'd better to apply the old
                   * apk mapping file if minifyEnabled is enable!
                   * Warning:
                   * you must be careful that it will affect the normal assemble build!
                   * old APK打包时所使用的混淆文件
                   */
                  applyMapping = getApplyMappingPath()
                  /**
                   * optional,default 'null'
                   * It is nice to keep the resource id from R.txt file to reduce java changes
                   *指定 old APK 的资源文件
                   */
                  applyResourceMapping = getApplyResourceMappingPath()
      ​
                  /**
                   * necessary,default 'null'
                   * because we don't want to check the base apk with md5 in the runtime(it is slow)
                   * tinkerId is use to identify the unique base apk when the patch is tried to apply.
                   * we can use git rev, svn rev or simply versionCode.
                   * we will gen the tinkerId in your manifest automatic
                   */
                  tinkerId = "1.0"//getTinkerIdValue()
      ​
                  /**
                   * if keepDexApply is true, class in which dex refer to the old apk.
                   * open this can reduce the dex diff file size.
                   */
                  keepDexApply = false
      ​
                  /**
                   * optional, default 'false'
                   * Whether tinker should treat the base apk as the one being protected by app
                   * protection tools.
                   * If this attribute is true, the generated patch package will contain a
                   * dex including all changed classes instead of any dexdiff patch-info files.
                   */
                  isProtectedApp = false
      ​
                  /**
                   * optional, default 'false'
                   * Whether tinker should support component hotplug (add new component dynamically).
                   * If this attribute is true, the component added in new apk will be available after
                   * patch is successfully loaded. Otherwise an error would be announced when generating patch
                   * on compile-time.
                   *
                   * Notice that currently this feature is incubating and only support NON-EXPORTED Activity
                   */
                  supportHotplugComponent = false
              }
      ​
              dex {
                  /**
                   * optional,default 'jar'
                   * only can be 'raw' or 'jar'. for raw, we would keep its original format
                   * for jar, we would repack dexes with zip format.
                   * if you want to support below 14, you must use jar
                   * or you want to save rom or check quicker, you can use raw mode also
                   */
                  dexMode = "jar"
      ​
                  /**
                   * necessary,default '[]'
                   * what dexes in apk are expected to deal with tinkerPatch
                   * it support * or ? pattern.
                   */
                  pattern = ["classes*.dex",
                             "assets/secondary-dex-?.jar"]
                  /**
                   * necessary,default '[]'
                   * Warning, it is very very important, loader classes can't change with patch.
                   * thus, they will be removed from patch dexes.
                   * you must put the following class into main dex.
                   * Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
                   * own tinkerLoader, and the classes you use in them
                   *
                   */
                  loader = [
                          //use sample, let BaseBuildInfo unchangeable with tinker
                        //  "tinker.sample.android.app.BaseBuildInfo"
                          //加载Patch文件用到的类
                          "com.jianxiongrao.tinkerdemo.tinker.MyTinkerApplication"
                  ]
              }
      ​
              lib {
                  /**
                   * optional,default '[]'
                   * what library in apk are expected to deal with tinkerPatch
                   * it support * or ? pattern.
                   * for library in assets, we would just recover them in the patch directory
                   * you can get them in TinkerLoadResult with Tinker
                   */
                  pattern = ["libs/*/*.so"]
              }
      ​
              res {
                  /**
                   * optional,default '[]'
                   * what resource in apk are expected to deal with tinkerPatch
                   * it support * or ? pattern.
                   * you must include all your resources in apk here,
                   * otherwise, they won't repack in the new apk resources.
                   */
                  pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
      ​
                  /**
                   * optional,default '[]'
                   * the resource file exclude patterns, ignore add, delete or modify resource change
                   * it support * or ? pattern.
                   * Warning, we can only use for files no relative with resources.arsc
                   */
                  ignoreChange = ["assets/sample_meta.txt"]
      ​
                  /**
                   * default 100kb
                   * for modify resource, if it is larger than 'largeModSize'
                   * we would like to use bsdiff algorithm to reduce patch file size
                   */
                  largeModSize = 100
              }
      ​
              packageConfig {
                  /**
                   * optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
                   * package meta file gen. path is assets/package_meta.txt in patch file
                   * you can use securityCheck.getPackageProperties() in your ownPackageCheck method
                   * or TinkerLoadResult.getPackageConfigByName
                   * we will get the TINKER_ID from the old apk manifest for you automatic,
                   * other config files (such as patchMessage below)is not necessary
                   */
                  configField("patchMessage", "tinker is sample to use")
                  /**
                   * just a sample case, you can use such as sdkVersion, brand, channel...
                   * you can parse it in the SamplePatchListener.
                   * Then you can use patch conditional!
                   */
                  configField("platform", "all")
                  /**
                   * patch version via packageConfig
                   */
                  configField("patchVersion", "1.0")
              }
              //or you can add config filed outside, or get meta value from old apk
              //project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
              //project.tinkerPatch.packageConfig.configField("test2", "sample")
      ​
              /**
               * if you don't use zipArtifact or path, we just use 7za to try
               */
      //        sevenZip {
      //            /**
      //             * optional,default '7za'
      //             * the 7zip artifact path, it will use the right 7za with your platform
      //             */
      //            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
      //            /**
      //             * optional,default '7za'
      //             * you can specify the 7za path yourself, it will overwrite the zipArtifact value
      //             */
      ////        path = "/usr/local/bin/7za"
      //        }
          }
      ​
          List 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")
      ​
          /**
           * 复制基准包和其他必须文件到指定目录
           */
          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"
                          }
      ​
                      }
                  }
              }
          }
      }
    • 通过Android Stdio右侧的gradle工具 build -> assembleRelease 或者命令行生成一个release APK,安装到手机上,然后需要将上述gradle文件的ext方法内的tinkerOldApkPath 值添加设置为 "${bakPath}/old_apk_name",然后再gradle工具栏找到tinker -> tinkerPatchRelease ,然后去outputs目录下找singed.apk补丁文件即可。

  5. 如果想实现进度条之类的就需要利用TinkerInstaller.install() 五个参数的方法,参数都设置成默认的即可,例如,LoadReporter 可以传递 new DefaultLoadReporter()

  6. 至于源码,利用Android Studio查看,核心方法还在于利用单例和建造者模式生成的Tinker类里面,例如操作字节码,设置偏移量,加载补丁后杀死进程等。

 

 

你可能感兴趣的:(Android学习探索)