Android热修复Tinker集成

1.配置Project下的build.gradle

buildscript {
  repositories {
    ...
    mavenLocal()
  }
  dependencies {
    ...
    classpath "com.tencent.tinker:tinker-patch-gradle-plugin:1.7.11"
  }
}

allprojects {
  repositories {
    ...
    mavenLocal()
    }
}

2.配置Module下的build.gradle

apply plugin: 'com.android.application'
apply plugin: 'com.tencent.tinker.patch'
android {
  ...
  defaultConfig {
    ...
    multiDexEnabled true
    buildConfigField "String", "MESSAGE", "\"I am the base apk\""
    buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""
    buildConfigField "String", "PLATFORM", "\"all\""
  }
}

dependencies {
  ...
  compile "com.android.support:multidex:1.0.1"
  provided('com.tencent.tinker:tinker-android-anno:1.7.11')
  compile('com.tencent.tinker:tinker-android-lib:1.7.11')
}
//定义基准apk构建的位置
def bakPath = file("${buildDir}/bakApk/")

//额外属性,实际上这些也是可以不用写的,腾讯真是良心,可以支持Gradle脚本自定义,这些值实际上都可以在gradle.properites中自定义
ext {
  //是否支持tinker构建
  tinkerEnabled = true
  //如果需要构建patch包,这里需要天上同一个其基准包地址
  tinkerOldApkPath = "${bakPath}/app-debug-05-27.apk"
  //如果需要构建patch包,这里需要天上同一个其基准包的混淆文件
  tinkerApplyMappingPath = "${bakPath}/app-debug-05-27-mapping.txt"
  //如果差分包有修改资源文件,则必须需要输入以前基准包的R文件,主要是为了固定id来用的。
  tinkerApplyResourcePath = "${bakPath}/app-debug-05-27-R.txt"
}

if (ext.tinkerEnabled) {
  //引用patch插件
  apply plugin: 'com.tencent.tinker.patch'
  //tinkerPath任务,补丁关键任务,源码也有,后面我会有详细撰述,这里只知道怎么用即可
  //直接使用基准apk包与新编译出来的apk包做差异,得到最终的补丁包
  tinkerPatch {
    oldApk = getTinkerOldApkPath()
    /**
      * 是否忽略警告
      * 值为false将中断编译。因为这些情况可能会导致编译出来的patch包带来风险:
      * 1. minSdkVersion小于14,但是dexMode的值为"raw";  dexmode下面会有介绍
      * 2. 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
      * 3. 定义在dex.loader用于加载补丁的类不在main dex中;
      * 4. 定义在dex.loader用于加载补丁的类出现修改;
      * 5. resources.arsc改变,但没有使用applyResourceMapping编译。
      */
    ignoreWarning = false
    //是否签名,保证基准包和补丁包的签名一致,代码里有判断逻辑
    useSign = true
    /**
      * 编译相关配置项
      */
    buildConfig {
      //在编译新的apk时候,通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。这个只是推荐设置,不设置applyMapping也不会影响任何的assemble编译
      applyMapping = getTinkerApplyMappingPath()
      //在编译新的apk时候,通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常。
      applyResourceMapping = getTinkerApplyResourcePath()
      //tinkerID
      tinkerId = getTinkerIdValue()
      //如果有多个dex,编译补丁时可能会由于类的移动导致变更增多。若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
      keepDexApply = false
    }
    dex {
      //只能是'raw'或者'jar'。
      //对于'raw'模式,将会保持输入dex的格式。
      //对于'jar'模式,将会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,但是验证md5时比'raw'模式耗时。默认我们并不会去校验md5,一般情况下选择jar模式即可。
      dexMode = "jar"
      //需要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如assets/...
      pattern = ["classes*.dex", "assets/secondary-dex-?.jar"]
      /**
        * 这一项非常重要,它定义了哪些类在加载补丁包的时候会用到。这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。
          这里需要定义的类有:
          1. 你自己定义的Application类;
          2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;
          3. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
          4. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。这里需要注意的是,这些类的直接引用类也需要加入到loader中。或者你需要将这个类变成非preverify。
          5. 使用1.7.6版本之后版本,参数1、2会自动填写。
        */
      loader = [
        //use sample, let BaseBuildInfo unchangeable with tinker
        //                    "tinker.sample.android.app.BaseBuildInfo"
      ]
    }
    lib {
      //需要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...
      pattern = ["lib/armeabi/*.so"]
  }
  res {
    //需要处理res路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...,务必注意的是,只有满足pattern的资源才会放到合成后的资源包。
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

            //支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。
            ignoreChange = ["assets/sample_meta.txt"]

            //对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb
            largeModSize = 100
        }

        packageConfig {
            //configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。但是建议直接通过修改代码来实现,例如BuildConfig。
            configField("patchMessage", "tinker is sample to use")
            /**
             * 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")
        }

        //7zip路径配置项,执行前提是useSign为true
        sevenZip {
            //例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        }
    }

    List 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")
        def date = new Date().format("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")
                        }
                    }
                }
            }
        }
    }
}

def getTinkerApplyResourcePath() {
    return ext.tinkerApplyResourcePath
}

def getTinkerApplyMappingPath() {
    return ext.tinkerApplyMappingPath
}

def getTinkerOldApkPath() {
    return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}

def getTinkerIdValue() {
    return hasProperty("TINKER_ID") ? TINKER_ID : 1
}

3.配置Application

@SuppressWarnings("unused")
@DefaultLifeCycle(application = ".BaseApplication",
    flags = ShareConstants.TINKER_ENABLE_ALL,
    loadVerifyFlag = false)
public class SampleApplication extends DefaultApplicationLike {
  private static final String TAG = "Tinker.SampleApplicationLike";

  public SampleApplication(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
    super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
  }

  /**
   * install multiDex before install tinker
   * so we don't need to put the tinker lib classes in the main dex
   *
   * @param base
   */
  @Override
  public void onBaseContextAttached(Context base) {
    super.onBaseContextAttached(base);
    //you must install multiDex whatever tinker is installed!
    MultiDex.install(base);

    TinkerManager.setTinkerApplicationLike(this);

    TinkerManager.initFastCrashProtect();
    //should set before tinker is installed
    TinkerManager.setUpgradeRetryEnable(true);

    //optional set logIml, or you can use default debug log
    //TinkerInstaller.setLogIml(new MyLogImp());
    TinkerInstaller.setLogIml(TinkerLog.getImpl());

    //installTinker after load multiDex
    //or you can put com.tencent.tinker.** to main dex
    TinkerManager.installTinker(this);
    Tinker tinker = Tinker.with(getApplication());
  }
}

其中注解参数BaseApplication才是真正的Application,需要在AndroidManifest.xml文件中进行配置


4.相关文件如下

1.TinkerManager.java

public class TinkerManager {
  private static final String TAG = "Tinker.TinkerManager";

  private static ApplicationLike applicationLike;
  private static SampleUncaughtExceptionHandler uncaughtExceptionHandler;
  private static boolean isInstalled = false;

  public static void setTinkerApplicationLike(ApplicationLike appLike) {
    applicationLike = appLike;
  }

  public static ApplicationLike getTinkerApplicationLike() {
    return applicationLike;
  }

  public static void initFastCrashProtect() {
    if (uncaughtExceptionHandler == null) {
      uncaughtExceptionHandler = new SampleUncaughtExceptionHandler();
      Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
    }
  }

  public static void setUpgradeRetryEnable(boolean enable) {
    UpgradePatchRetry.getInstance(applicationLike.getApplication()).setRetryEnable(enable);
  }


  /**
   * all use default class, simply Tinker install method
   */
  public static void sampleInstallTinker(ApplicationLike appLike) {
    if (isInstalled) {
      TinkerLog.w(TAG, "install tinker, but has installed, ignore");
      return;
    }
    TinkerInstaller.install(appLike);
    isInstalled = true;

  }

  /**
   * you can specify all class you want.
   * sometimes, you can only install tinker in some process you want!
   *
   * @param appLike
   */
  public static void installTinker(ApplicationLike appLike) {
    if (isInstalled) {
      TinkerLog.w(TAG, "install tinker, but has installed, ignore");
      return;
    }
    //or you can just use DefaultLoadReporter
    LoadReporter loadReporter = new SampleLoadReporter(appLike.getApplication());
    //or you can just use DefaultPatchReporter
    PatchReporter patchReporter = new SamplePatchReporter(appLike.getApplication());
    //or you can just use DefaultPatchListener
    PatchListener patchListener = new SamplePatchListener(appLike.getApplication());
    //you can set your own upgrade patch if you need
    AbstractPatch upgradePatchProcessor = new UpgradePatch();

    TinkerInstaller.install(appLike,
        loadReporter, patchReporter, patchListener,
        SampleResultService.class, upgradePatchProcessor);

    isInstalled = true;
  }
}

2.Utils.java

public class Utils {
  private static final String TAG = "Tinker.Utils";

  /**
   * the error code define by myself
   * should after {@code ShareConstants.ERROR_PATCH_INSERVICE
   */
  public static final int ERROR_PATCH_GOOGLEPLAY_CHANNEL      = -6;
  public static final int ERROR_PATCH_ROM_SPACE               = -7;
  public static final int ERROR_PATCH_MEMORY_LIMIT            = -8;
  public static final int ERROR_PATCH_CRASH_LIMIT             = -9;
  public static final int ERROR_PATCH_CONDITION_NOT_SATISFIED = -10;
  public static final int ERROR_PATCH_ALREADY_APPLY           = -11;
  public static final int ERROR_PATCH_RETRY_COUNT_LIMIT       = -12;

  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;
    }
  }

  public static class ScreenState {
    public interface IOnScreenOff {
      void onScreenOff();
    }

    public ScreenState(final 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)) {
            if (onScreenOffInterface != null) {
              onScreenOffInterface.onScreenOff();
            }
          }
          context.unregisterReceiver(this);
        }
      }, filter);
    }
  }
}

3.SampleLoadReporter.java

public class SampleLoadReporter extends DefaultLoadReporter {

  public SampleLoadReporter(Context context) {
    super(context);
  }

  @Override
  public void onLoadPatchListenerReceiveFail(final File patchFile, int errorCode) {
    super.onLoadPatchListenerReceiveFail(patchFile, errorCode);
    SampleTinkerReport.onTryApplyFail(errorCode);
  }

  @Override
  public void onLoadResult(File patchDirectory, int loadCode, long cost) {
    super.onLoadResult(patchDirectory, loadCode, cost);
    switch (loadCode) {
      case ShareConstants.ERROR_LOAD_OK:
        SampleTinkerReport.onLoaded(cost);
        break;
    }
    Looper.getMainLooper().myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
      @Override
      public boolean queueIdle() {
        if (UpgradePatchRetry.getInstance(context).onPatchRetryLoad()) {
          SampleTinkerReport.onReportRetryPatch();
        }
        return false;
      }
    });
  }

  @Override
  public void onLoadException(Throwable e, int errorCode) {
    super.onLoadException(e, errorCode);
    SampleTinkerReport.onLoadException(e, errorCode);
  }

  @Override
  public void onLoadFileMd5Mismatch(File file, int fileType) {
    super.onLoadFileMd5Mismatch(file, fileType);
    SampleTinkerReport.onLoadFileMisMatch(fileType);
  }

  /**
   * try to recover patch oat file
   *
   * @param file
   * @param fileType
   * @param isDirectory
   */
  @Override
  public void onLoadFileNotFound(File file, int fileType, boolean isDirectory) {
    super.onLoadFileNotFound(file, fileType, isDirectory);
    SampleTinkerReport.onLoadFileNotFound(fileType);
  }

  @Override
  public void onLoadPackageCheckFail(File patchFile, int errorCode) {
    super.onLoadPackageCheckFail(patchFile, errorCode);
    SampleTinkerReport.onLoadPackageCheckFail(errorCode);
  }

  @Override
  public void onLoadPatchInfoCorrupted(String oldVersion, String newVersion, File patchInfoFile) {
    super.onLoadPatchInfoCorrupted(oldVersion, newVersion, patchInfoFile);
    SampleTinkerReport.onLoadInfoCorrupted();
  }

  @Override
  public void onLoadInterpret(int type, Throwable e) {
    super.onLoadInterpret(type, e);
    SampleTinkerReport.onLoadInterpretReport(type, e);
  }

  @Override
  public void onLoadPatchVersionChanged(String oldVersion, String newVersion, File patchDirectoryFile, String currentPatchName) {
    super.onLoadPatchVersionChanged(oldVersion, newVersion, patchDirectoryFile, currentPatchName);
  }

}

4.SamplePatchListener.java

public class SamplePatchListener extends DefaultPatchListener {
  private static final String TAG = "Tinker.SamplePatchListener";

  protected static final long NEW_PATCH_RESTRICTION_SPACE_SIZE_MIN = 60 * 1024 * 1024;

  private final int maxMemory;

  public SamplePatchListener(Context context) {
    super(context);
    maxMemory = ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
    TinkerLog.i(TAG, "application maxMemory:" + maxMemory);
  }

  /**
   * because we use the defaultCheckPatchReceived method
   * the error code define by myself should after {@code ShareConstants.ERROR_RECOVER_INSERVICE
   *
   * @param path
   * @param newPatch
   * @return
   */
  @Override
  public int patchCheck(String path) {
    File patchFile = new File(path);
    TinkerLog.i(TAG, "receive a patch file: %s, file size:%d", path, SharePatchFileUtil.getFileOrDirectorySize(patchFile));
    int returnCode = super.patchCheck(path);

    if (returnCode == ShareConstants.ERROR_PATCH_OK) {
      returnCode = Utils.checkForPatchRecover(NEW_PATCH_RESTRICTION_SPACE_SIZE_MIN, maxMemory);
    }

    if (returnCode == ShareConstants.ERROR_PATCH_OK) {
      String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
      SharedPreferences sp = context.getSharedPreferences(ShareConstants.TINKER_SHARE_PREFERENCE_CONFIG, Context.MODE_MULTI_PROCESS);
      //optional, only disable this patch file with md5
      int fastCrashCount = sp.getInt(patchMd5, 0);
      if (fastCrashCount >= SampleUncaughtExceptionHandler.MAX_CRASH_COUNT) {
        returnCode = Utils.ERROR_PATCH_CRASH_LIMIT;
      } else {
        //for upgrade patch, version must be not the same
        //for repair patch, we won't has the tinker load flag
        Tinker tinker = Tinker.with(context);

        if (tinker.isTinkerLoaded()) {
          TinkerLoadResult tinkerLoadResult = tinker.getTinkerLoadResultIfPresent();
          if (tinkerLoadResult != null && !tinkerLoadResult.useInterpretMode) {
            String currentVersion = tinkerLoadResult.currentVersion;
            if (patchMd5.equals(currentVersion)) {
              returnCode = Utils.ERROR_PATCH_ALREADY_APPLY;
            }
          }
        }
      }
      //check whether retry so many times
      if (returnCode == ShareConstants.ERROR_PATCH_OK) {
        returnCode = UpgradePatchRetry.getInstance(context).onPatchListenerCheck(patchMd5)
            ? ShareConstants.ERROR_PATCH_OK : Utils.ERROR_PATCH_RETRY_COUNT_LIMIT;
      }
    }
    // Warning, it is just a sample case, you don't need to copy all of these
    // Interception some of the request
    if (returnCode == ShareConstants.ERROR_PATCH_OK) {
      Properties properties = ShareTinkerInternals.fastGetPatchPackageMeta(patchFile);
      if (properties == null) {
        returnCode = Utils.ERROR_PATCH_CONDITION_NOT_SATISFIED;
      } else {
        String platform = properties.getProperty(Utils.PLATFORM);
        TinkerLog.i(TAG, "get platform:" + platform);
        // check patch platform require
        if (platform == null || !platform.equals(BuildConfig.PLATFORM)) {
          returnCode = Utils.ERROR_PATCH_CONDITION_NOT_SATISFIED;
        }
      }
    }

    SampleTinkerReport.onTryApply(returnCode == ShareConstants.ERROR_PATCH_OK);
    return returnCode;
  }
}

5.SamplePatchReporter.java

public class SamplePatchReporter extends DefaultPatchReporter {
  public SamplePatchReporter(Context context) {
    super(context);
  }

  @Override
  public void onPatchServiceStart(Intent intent) {
    super.onPatchServiceStart(intent);
    SampleTinkerReport.onApplyPatchServiceStart();
  }

  @Override
  public void onPatchDexOptFail(File patchFile, List dexFiles, Throwable t) {
    super.onPatchDexOptFail(patchFile, dexFiles, t);
    SampleTinkerReport.onApplyDexOptFail(t);
  }

  @Override
  public void onPatchException(File patchFile, Throwable e) {
    super.onPatchException(patchFile, e);
    SampleTinkerReport.onApplyCrash(e);
  }

  @Override
  public void onPatchInfoCorrupted(File patchFile, String oldVersion, String newVersion) {
    super.onPatchInfoCorrupted(patchFile, oldVersion, newVersion);
    SampleTinkerReport.onApplyInfoCorrupted();
  }

  @Override
  public void onPatchPackageCheckFail(File patchFile, int errorCode) {
    super.onPatchPackageCheckFail(patchFile, errorCode);
    SampleTinkerReport.onApplyPackageCheckFail(errorCode);
  }

  @Override
  public void onPatchResult(File patchFile, boolean success, long cost) {
    super.onPatchResult(patchFile, success, cost);
    SampleTinkerReport.onApplied(cost, success);
  }

  @Override
  public void onPatchTypeExtractFail(File patchFile, File extractTo, String filename, int fileType) {
    super.onPatchTypeExtractFail(patchFile, extractTo, filename, fileType);
    SampleTinkerReport.onApplyExtractFail(fileType);
  }

  @Override
  public void onPatchVersionCheckFail(File patchFile, SharePatchInfo oldPatchInfo, String patchFileVersion) {
    super.onPatchVersionCheckFail(patchFile, oldPatchInfo, patchFileVersion);
    SampleTinkerReport.onApplyVersionCheckFail();
  }
}

6.SampleResultService.java

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) {
      deleteRawPatchFile(new File(result.rawPatchFilePath));

      //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 Utils.ScreenState(getApplicationContext(), new Utils.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());
  }

}

7.SampleTinkerReport.java

public class SampleTinkerReport {
  private static final String TAG = "Tinker.SampleTinkerReport";

  // KEY - PV
  public static final int KEY_REQUEST                   = 0;
  public static final int KEY_DOWNLOAD                  = 1;
  public static final int KEY_TRY_APPLY                 = 2;
  public static final int KEY_TRY_APPLY_SUCCESS         = 3;
  public static final int KEY_APPLIED_START             = 4;
  public static final int KEY_APPLIED                   = 5;
  public static final int KEY_LOADED                    = 6;
  public static final int KEY_CRASH_FAST_PROTECT        = 7;
  public static final int KEY_CRASH_CAUSE_XPOSED_DALVIK = 8;
  public static final int KEY_CRASH_CAUSE_XPOSED_ART    = 9;
  public static final int KEY_APPLY_WITH_RETRY          = 10;

  //Key -- try apply detail
  public static final int KEY_TRY_APPLY_UPGRADE                 = 70;
  public static final int KEY_TRY_APPLY_DISABLE                 = 71;
  public static final int KEY_TRY_APPLY_RUNNING                 = 72;
  public static final int KEY_TRY_APPLY_INSERVICE               = 73;
  public static final int KEY_TRY_APPLY_NOT_EXIST               = 74;
  public static final int KEY_TRY_APPLY_GOOGLEPLAY              = 75;
  public static final int KEY_TRY_APPLY_ROM_SPACE               = 76;
  public static final int KEY_TRY_APPLY_ALREADY_APPLY           = 77;
  public static final int KEY_TRY_APPLY_MEMORY_LIMIT            = 78;
  public static final int KEY_TRY_APPLY_CRASH_LIMIT             = 79;
  public static final int KEY_TRY_APPLY_CONDITION_NOT_SATISFIED = 80;
  public static final int KEY_TRY_APPLY_JIT                     = 81;

  //Key -- apply detail
  public static final int KEY_APPLIED_UPGRADE      = 100;
  public static final int KEY_APPLIED_UPGRADE_FAIL = 101;

  public static final int KEY_APPLIED_EXCEPTION                               = 120;
  public static final int KEY_APPLIED_DEXOPT_OTHER                            = 121;
  public static final int KEY_APPLIED_DEXOPT_EXIST                            = 122;
  public static final int KEY_APPLIED_DEXOPT_FORMAT                           = 123;
  public static final int KEY_APPLIED_INFO_CORRUPTED                          = 124;
  //package check
  public static final int KEY_APPLIED_PACKAGE_CHECK_SIGNATURE                 = 150;
  public static final int KEY_APPLIED_PACKAGE_CHECK_DEX_META                  = 151;
  public static final int KEY_APPLIED_PACKAGE_CHECK_LIB_META                  = 152;
  public static final int KEY_APPLIED_PACKAGE_CHECK_APK_TINKER_ID_NOT_FOUND   = 153;
  public static final int KEY_APPLIED_PACKAGE_CHECK_PATCH_TINKER_ID_NOT_FOUND = 154;
  public static final int KEY_APPLIED_PACKAGE_CHECK_META_NOT_FOUND            = 155;
  public static final int KEY_APPLIED_PACKAGE_CHECK_TINKER_ID_NOT_EQUAL       = 156;
  public static final int KEY_APPLIED_PACKAGE_CHECK_RES_META                  = 157;
  public static final int KEY_APPLIED_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT    = 158;

  //version check
  public static final int KEY_APPLIED_VERSION_CHECK      = 180;
  //extract error
  public static final int KEY_APPLIED_PATCH_FILE_EXTRACT = 181;
  public static final int KEY_APPLIED_DEX_EXTRACT        = 182;
  public static final int KEY_APPLIED_LIB_EXTRACT        = 183;
  public static final int KEY_APPLIED_RESOURCE_EXTRACT   = 184;
  //cost time
  public static final int KEY_APPLIED_SUCC_COST_5S_LESS  = 200;
  public static final int KEY_APPLIED_SUCC_COST_10S_LESS = 201;
  public static final int KEY_APPLIED_SUCC_COST_30S_LESS = 202;
  public static final int KEY_APPLIED_SUCC_COST_60S_LESS = 203;
  public static final int KEY_APPLIED_SUCC_COST_OTHER    = 204;

  public static final int KEY_APPLIED_FAIL_COST_5S_LESS  = 205;
  public static final int KEY_APPLIED_FAIL_COST_10S_LESS = 206;
  public static final int KEY_APPLIED_FAIL_COST_30S_LESS = 207;
  public static final int KEY_APPLIED_FAIL_COST_60S_LESS = 208;
  public static final int KEY_APPLIED_FAIL_COST_OTHER    = 209;


  // KEY -- load detail
  public static final int KEY_LOADED_UNKNOWN_EXCEPTION        = 250;
  public static final int KEY_LOADED_UNCAUGHT_EXCEPTION       = 251;
  public static final int KEY_LOADED_EXCEPTION_DEX            = 252;
  public static final int KEY_LOADED_EXCEPTION_DEX_CHECK      = 253;
  public static final int KEY_LOADED_EXCEPTION_RESOURCE       = 254;
  public static final int KEY_LOADED_EXCEPTION_RESOURCE_CHECK = 255;


  public static final int KEY_LOADED_MISMATCH_DEX       = 300;
  public static final int KEY_LOADED_MISMATCH_LIB       = 301;
  public static final int KEY_LOADED_MISMATCH_RESOURCE  = 302;
  public static final int KEY_LOADED_MISSING_DEX        = 303;
  public static final int KEY_LOADED_MISSING_LIB        = 304;
  public static final int KEY_LOADED_MISSING_PATCH_FILE = 305;
  public static final int KEY_LOADED_MISSING_PATCH_INFO = 306;
  public static final int KEY_LOADED_MISSING_DEX_OPT    = 307;
  public static final int KEY_LOADED_MISSING_RES        = 308;
  public static final int KEY_LOADED_INFO_CORRUPTED     = 309;

  //load package check
  public static final int KEY_LOADED_PACKAGE_CHECK_SIGNATURE                 = 350;
  public static final int KEY_LOADED_PACKAGE_CHECK_DEX_META                  = 351;
  public static final int KEY_LOADED_PACKAGE_CHECK_LIB_META                  = 352;
  public static final int KEY_LOADED_PACKAGE_CHECK_APK_TINKER_ID_NOT_FOUND   = 353;
  public static final int KEY_LOADED_PACKAGE_CHECK_PATCH_TINKER_ID_NOT_FOUND = 354;
  public static final int KEY_LOADED_PACKAGE_CHECK_TINKER_ID_NOT_EQUAL       = 355;
  public static final int KEY_LOADED_PACKAGE_CHECK_PACKAGE_META_NOT_FOUND    = 356;
  public static final int KEY_LOADED_PACKAGE_CHECK_RES_META                  = 357;
  public static final int KEY_LOADED_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT    = 358;


  public static final int KEY_LOADED_SUCC_COST_500_LESS  = 400;
  public static final int KEY_LOADED_SUCC_COST_1000_LESS = 401;
  public static final int KEY_LOADED_SUCC_COST_3000_LESS = 402;
  public static final int KEY_LOADED_SUCC_COST_5000_LESS = 403;
  public static final int KEY_LOADED_SUCC_COST_OTHER     = 404;

  public static final int KEY_LOADED_INTERPRET_GET_INSTRUCTION_SET_ERROR = 450;
  public static final int KEY_LOADED_INTERPRET_INTERPRET_COMMAND_ERROR   = 451;
  public static final int KEY_LOADED_INTERPRET_TYPE_INTERPRET_OK         = 452;


  interface Reporter {
    void onReport(int key);

    void onReport(String message);
  }

  private static Reporter reporter = null;

  public void setReporter(Reporter reporter) {
    this.reporter = reporter;
  }

  public static void onTryApply(boolean success) {
    if (reporter == null) {
      return;
    }
    reporter.onReport(KEY_TRY_APPLY);

    reporter.onReport(KEY_TRY_APPLY_UPGRADE);

    if (success) {
      reporter.onReport(KEY_TRY_APPLY_SUCCESS);
    }
  }

  public static void onTryApplyFail(int errorCode) {
    if (reporter == null) {
      return;
    }
    switch (errorCode) {
      case ShareConstants.ERROR_PATCH_NOTEXIST:
        reporter.onReport(KEY_TRY_APPLY_NOT_EXIST);
        break;
      case ShareConstants.ERROR_PATCH_DISABLE:
        reporter.onReport(KEY_TRY_APPLY_DISABLE);
        break;
      case ShareConstants.ERROR_PATCH_INSERVICE:
        reporter.onReport(KEY_TRY_APPLY_INSERVICE);
        break;
      case ShareConstants.ERROR_PATCH_RUNNING:
        reporter.onReport(KEY_TRY_APPLY_RUNNING);
        break;
      case ShareConstants.ERROR_PATCH_JIT:
        reporter.onReport(KEY_TRY_APPLY_JIT);
        break;
      case Utils.ERROR_PATCH_ROM_SPACE:
        reporter.onReport(KEY_TRY_APPLY_ROM_SPACE);
        break;
      case Utils.ERROR_PATCH_GOOGLEPLAY_CHANNEL:
        reporter.onReport(KEY_TRY_APPLY_GOOGLEPLAY);
        break;
      case Utils.ERROR_PATCH_ALREADY_APPLY:
        reporter.onReport(KEY_TRY_APPLY_ALREADY_APPLY);
        break;
      case Utils.ERROR_PATCH_CRASH_LIMIT:
        reporter.onReport(KEY_TRY_APPLY_CRASH_LIMIT);
        break;
      case Utils.ERROR_PATCH_MEMORY_LIMIT:
        reporter.onReport(KEY_TRY_APPLY_MEMORY_LIMIT);
        break;
      case Utils.ERROR_PATCH_CONDITION_NOT_SATISFIED:
        reporter.onReport(KEY_TRY_APPLY_CONDITION_NOT_SATISFIED);
        break;

    }
  }

  public static void onLoadPackageCheckFail(int errorCode) {
    if (reporter == null) {
      return;
    }
    switch (errorCode) {
      case ShareConstants.ERROR_PACKAGE_CHECK_SIGNATURE_FAIL:
        reporter.onReport(KEY_LOADED_PACKAGE_CHECK_SIGNATURE);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_DEX_META_CORRUPTED:
        reporter.onReport(KEY_LOADED_PACKAGE_CHECK_DEX_META);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_LIB_META_CORRUPTED:
        reporter.onReport(KEY_LOADED_PACKAGE_CHECK_LIB_META);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_PATCH_TINKER_ID_NOT_FOUND:
        reporter.onReport(KEY_LOADED_PACKAGE_CHECK_PATCH_TINKER_ID_NOT_FOUND);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_APK_TINKER_ID_NOT_FOUND:
        reporter.onReport(KEY_LOADED_PACKAGE_CHECK_APK_TINKER_ID_NOT_FOUND);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_TINKER_ID_NOT_EQUAL:
        reporter.onReport(KEY_LOADED_PACKAGE_CHECK_TINKER_ID_NOT_EQUAL);

        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_PACKAGE_META_NOT_FOUND:
        reporter.onReport(KEY_LOADED_PACKAGE_CHECK_PACKAGE_META_NOT_FOUND);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_RESOURCE_META_CORRUPTED:
        reporter.onReport(KEY_LOADED_PACKAGE_CHECK_RES_META);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT:
        reporter.onReport(KEY_LOADED_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT);
        break;
    }
  }

  public static void onLoaded(long cost) {
    if (reporter == null) {
      return;
    }
    reporter.onReport(KEY_LOADED);

    if (cost < 0L) {
      TinkerLog.e(TAG, "hp_report report load cost failed, invalid cost");
      return;
    }

    if (cost <= 500) {
      reporter.onReport(KEY_LOADED_SUCC_COST_500_LESS);
    } else if (cost <= 1000) {
      reporter.onReport(KEY_LOADED_SUCC_COST_1000_LESS);
    } else if (cost <= 3000) {
      reporter.onReport(KEY_LOADED_SUCC_COST_3000_LESS);
    } else if (cost <= 5000) {
      reporter.onReport(KEY_LOADED_SUCC_COST_5000_LESS);
    } else {
      reporter.onReport(KEY_LOADED_SUCC_COST_OTHER);
    }
  }

  public static void onLoadInfoCorrupted() {
    if (reporter == null) {
      return;
    }
    reporter.onReport(KEY_LOADED_INFO_CORRUPTED);
  }

  public static void onLoadFileNotFound(int fileType) {
    if (reporter == null) {
      return;
    }
    switch (fileType) {
      case ShareConstants.TYPE_DEX_OPT:
        reporter.onReport(KEY_LOADED_MISSING_DEX_OPT);
        break;
      case ShareConstants.TYPE_DEX:
        reporter.onReport(KEY_LOADED_MISSING_DEX);
        break;
      case ShareConstants.TYPE_LIBRARY:
        reporter.onReport(KEY_LOADED_MISSING_LIB);
        break;
      case ShareConstants.TYPE_PATCH_FILE:
        reporter.onReport(KEY_LOADED_MISSING_PATCH_FILE);
        break;
      case ShareConstants.TYPE_PATCH_INFO:
        reporter.onReport(KEY_LOADED_MISSING_PATCH_INFO);
        break;
      case ShareConstants.TYPE_RESOURCE:
        reporter.onReport(KEY_LOADED_MISSING_RES);
        break;
    }
  }

  public static void onLoadInterpretReport(int type, Throwable e) {
    if (reporter == null) {
      return;
    }
    switch (type) {
      case ShareConstants.TYPE_INTERPRET_GET_INSTRUCTION_SET_ERROR:
        reporter.onReport(KEY_LOADED_INTERPRET_GET_INSTRUCTION_SET_ERROR);
        reporter.onReport("Tinker Exception:interpret occur exception " + Utils.getExceptionCauseString(e));
        break;
      case ShareConstants.TYPE_INTERPRET_COMMAND_ERROR:
        reporter.onReport(KEY_LOADED_INTERPRET_INTERPRET_COMMAND_ERROR);
        reporter.onReport("Tinker Exception:interpret occur exception " + Utils.getExceptionCauseString(e));
        break;
      case ShareConstants.TYPE_INTERPRET_OK:
        reporter.onReport(KEY_LOADED_INTERPRET_TYPE_INTERPRET_OK);
        break;
    }
  }

  public static void onLoadFileMisMatch(int fileType) {
    if (reporter == null) {
      return;
    }
    switch (fileType) {
      case ShareConstants.TYPE_DEX:
        reporter.onReport(KEY_LOADED_MISMATCH_DEX);
        break;
      case ShareConstants.TYPE_LIBRARY:
        reporter.onReport(KEY_LOADED_MISMATCH_LIB);
        break;
      case ShareConstants.TYPE_RESOURCE:
        reporter.onReport(KEY_LOADED_MISMATCH_RESOURCE);
        break;
    }
  }

  public static void onLoadException(Throwable throwable, int errorCode) {
    if (reporter == null) {
      return;
    }
    boolean isCheckFail = false;
    switch (errorCode) {
      case ShareConstants.ERROR_LOAD_EXCEPTION_DEX:
        if (throwable.getMessage().contains(ShareConstants.CHECK_DEX_INSTALL_FAIL)) {
          reporter.onReport(KEY_LOADED_EXCEPTION_DEX_CHECK);
          isCheckFail = true;
          TinkerLog.e(TAG, "tinker dex check fail:" + throwable.getMessage());
        } else {
          reporter.onReport(KEY_LOADED_EXCEPTION_DEX);
          TinkerLog.e(TAG, "tinker dex reflect fail:" + throwable.getMessage());
        }
        break;
      case ShareConstants.ERROR_LOAD_EXCEPTION_RESOURCE:
        if (throwable.getMessage().contains(ShareConstants.CHECK_RES_INSTALL_FAIL)) {
          reporter.onReport(KEY_LOADED_EXCEPTION_RESOURCE_CHECK);
          isCheckFail = true;
          TinkerLog.e(TAG, "tinker res check fail:" + throwable.getMessage());
        } else {
          reporter.onReport(KEY_LOADED_EXCEPTION_RESOURCE);
          TinkerLog.e(TAG, "tinker res reflect fail:" + throwable.getMessage());
        }
        break;
      case ShareConstants.ERROR_LOAD_EXCEPTION_UNCAUGHT:
        reporter.onReport(KEY_LOADED_UNCAUGHT_EXCEPTION);
        break;
      case ShareConstants.ERROR_LOAD_EXCEPTION_UNKNOWN:
        reporter.onReport(KEY_LOADED_UNKNOWN_EXCEPTION);
        break;
    }
    //reporter exception, for dex check fail, we don't need to report stacktrace
    if (!isCheckFail) {
      reporter.onReport("Tinker Exception:load tinker occur exception " + Utils.getExceptionCauseString(throwable));
    }
  }

  public static void onApplyPatchServiceStart() {
    if (reporter == null) {
      return;
    }
    reporter.onReport(KEY_APPLIED_START);
  }

  public static void onApplyDexOptFail(Throwable throwable) {
    if (reporter == null) {
      return;
    }
    if (throwable.getMessage().contains(ShareConstants.CHECK_DEX_OAT_EXIST_FAIL)) {
      reporter.onReport(KEY_APPLIED_DEXOPT_EXIST);
    } else if (throwable.getMessage().contains(ShareConstants.CHECK_DEX_OAT_FORMAT_FAIL)) {
      reporter.onReport(KEY_APPLIED_DEXOPT_FORMAT);
    } else {
      reporter.onReport(KEY_APPLIED_DEXOPT_OTHER);
      reporter.onReport("Tinker Exception:apply tinker occur exception " + Utils.getExceptionCauseString(throwable));
    }
  }

  public static void onApplyInfoCorrupted() {
    if (reporter == null) {
      return;
    }
    reporter.onReport(KEY_APPLIED_INFO_CORRUPTED);
  }

  public static void onApplyVersionCheckFail() {
    if (reporter == null) {
      return;
    }
    reporter.onReport(KEY_APPLIED_VERSION_CHECK);
  }

  public static void onApplyExtractFail(int fileType) {
    if (reporter == null) {
      return;
    }
    switch (fileType) {
      case ShareConstants.TYPE_DEX:
        reporter.onReport(KEY_APPLIED_DEX_EXTRACT);
        break;
      case ShareConstants.TYPE_LIBRARY:
        reporter.onReport(KEY_APPLIED_LIB_EXTRACT);
        break;
      case ShareConstants.TYPE_PATCH_FILE:
        reporter.onReport(KEY_APPLIED_PATCH_FILE_EXTRACT);
        break;
      case ShareConstants.TYPE_RESOURCE:
        reporter.onReport(KEY_APPLIED_RESOURCE_EXTRACT);
        break;
    }
  }

  public static void onApplied(long cost, boolean success) {
    if (reporter == null) {
      return;
    }
    if (success) {
      reporter.onReport(KEY_APPLIED);
    }

    if (success) {
      reporter.onReport(KEY_APPLIED_UPGRADE);
    } else {
      reporter.onReport(KEY_APPLIED_UPGRADE_FAIL);
    }

    TinkerLog.i(TAG, "hp_report report apply cost = %d", cost);

    if (cost < 0L) {
      TinkerLog.e(TAG, "hp_report report apply cost failed, invalid cost");
      return;
    }

    if (cost <= 5000) {
      if (success) {
        reporter.onReport(KEY_APPLIED_SUCC_COST_5S_LESS);
      } else {
        reporter.onReport(KEY_APPLIED_FAIL_COST_5S_LESS);
      }
    } else if (cost <= 10 * 1000) {
      if (success) {
        reporter.onReport(KEY_APPLIED_SUCC_COST_10S_LESS);
      } else {
        reporter.onReport(KEY_APPLIED_FAIL_COST_10S_LESS);
      }
    } else if (cost <= 30 * 1000) {
      if (success) {
        reporter.onReport(KEY_APPLIED_SUCC_COST_30S_LESS);
      } else {
        reporter.onReport(KEY_APPLIED_FAIL_COST_30S_LESS);
      }
    } else if (cost <= 60 * 1000) {
      if (success) {
        reporter.onReport(KEY_APPLIED_SUCC_COST_60S_LESS);
      } else {
        reporter.onReport(KEY_APPLIED_FAIL_COST_60S_LESS);
      }
    } else {
      if (success) {
        reporter.onReport(KEY_APPLIED_SUCC_COST_OTHER);
      } else {
        reporter.onReport(KEY_APPLIED_FAIL_COST_OTHER);
      }
    }
  }

  public static void onApplyPackageCheckFail(int errorCode) {
    if (reporter == null) {
      return;
    }
    TinkerLog.i(TAG, "hp_report package check failed, error = %d", errorCode);

    switch (errorCode) {
      case ShareConstants.ERROR_PACKAGE_CHECK_SIGNATURE_FAIL:
        reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_SIGNATURE);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_DEX_META_CORRUPTED:
        reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_DEX_META);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_LIB_META_CORRUPTED:
        reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_LIB_META);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_PATCH_TINKER_ID_NOT_FOUND:
        reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_PATCH_TINKER_ID_NOT_FOUND);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_APK_TINKER_ID_NOT_FOUND:
        reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_APK_TINKER_ID_NOT_FOUND);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_TINKER_ID_NOT_EQUAL:
        reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_TINKER_ID_NOT_EQUAL);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_PACKAGE_META_NOT_FOUND:
        reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_META_NOT_FOUND);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_RESOURCE_META_CORRUPTED:
        reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_RES_META);
        break;
      case ShareConstants.ERROR_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT:
        reporter.onReport(KEY_APPLIED_PACKAGE_CHECK_TINKERFLAG_NOT_SUPPORT);
        break;
    }
  }

  public static void onApplyCrash(Throwable throwable) {
    if (reporter == null) {
      return;
    }
    reporter.onReport(KEY_APPLIED_EXCEPTION);
    reporter.onReport("Tinker Exception:apply tinker occur exception " + Utils.getExceptionCauseString(throwable));
  }

  public static void onFastCrashProtect() {
    if (reporter == null) {
      return;
    }
    reporter.onReport(KEY_CRASH_FAST_PROTECT);
  }

  public static void onXposedCrash() {
    if (reporter == null) {
      return;
    }
    if (ShareTinkerInternals.isVmArt()) {
      reporter.onReport(KEY_CRASH_CAUSE_XPOSED_ART);
    } else {
      reporter.onReport(KEY_CRASH_CAUSE_XPOSED_DALVIK);
    }
  }

  public static void onReportRetryPatch() {
    if (reporter == null) {
      return;
    }
    reporter.onReport(KEY_APPLY_WITH_RETRY);
  }

}

8.SampleUncaughtExceptionHandler.java

public class SampleUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
  private static final String TAG = "Tinker.SampleUncaughtExHandler";

  private final Thread.UncaughtExceptionHandler ueh;
  private static final long QUICK_CRASH_ELAPSE = 10 * 1000;
  public static final int MAX_CRASH_COUNT = 3;
  private static final String DALVIK_XPOSED_CRASH = "Class ref in pre-verified class resolved to unexpected implementation";

  public SampleUncaughtExceptionHandler() {
    ueh = Thread.getDefaultUncaughtExceptionHandler();
  }

  @Override
  public void uncaughtException(Thread thread, Throwable ex) {
    TinkerLog.e(TAG, "uncaughtException:" + ex.getMessage());
    tinkerFastCrashProtect();
    tinkerPreVerifiedCrashHandler(ex);
    ueh.uncaughtException(thread, ex);
  }

  /**
   * Such as Xposed, if it try to load some class before we load from patch files.
   * With dalvik, it will crash with "Class ref in pre-verified class resolved to unexpected implementation".
   * With art, it may crash at some times. But we can't know the actual crash type.
   * If it use Xposed, we can just clean patch or mention user to uninstall it.
   */
  private void tinkerPreVerifiedCrashHandler(Throwable ex) {
    ApplicationLike applicationLike = TinkerManager.getTinkerApplicationLike();
    if (applicationLike == null || applicationLike.getApplication() == null) {
      TinkerLog.w(TAG, "applicationlike is null");
      return;
    }

    if (!TinkerApplicationHelper.isTinkerLoadSuccess(applicationLike)) {
      TinkerLog.w(TAG, "tinker is not loaded");
      return;
    }

    Throwable throwable = ex;
    boolean isXposed = false;
    while (throwable != null) {
      if (!isXposed) {
        isXposed = Utils.isXposedExists(throwable);
      }

      // xposed?
      if (isXposed) {
        boolean isCausedByXposed = false;
        //for art, we can't know the actually crash type
        //just ignore art
        if (throwable instanceof IllegalAccessError && throwable.getMessage().contains(DALVIK_XPOSED_CRASH)) {
          //for dalvik, we know the actual crash type
          isCausedByXposed = true;
        }

        if (isCausedByXposed) {
          SampleTinkerReport.onXposedCrash();
          TinkerLog.e(TAG, "have xposed: just clean tinker");
          //kill all other process to ensure that all process's code is the same.
          ShareTinkerInternals.killAllOtherProcess(applicationLike.getApplication());

          TinkerApplicationHelper.cleanPatch(applicationLike);
          ShareTinkerInternals.setTinkerDisableWithSharedPreferences(applicationLike.getApplication());
          return;
        }
      }
      throwable = throwable.getCause();
    }
  }

  /**
   * if tinker is load, and it crash more than MAX_CRASH_COUNT, then we just clean patch.
   */
  private boolean tinkerFastCrashProtect() {
    ApplicationLike applicationLike = TinkerManager.getTinkerApplicationLike();

    if (applicationLike == null || applicationLike.getApplication() == null) {
      return false;
    }
    if (!TinkerApplicationHelper.isTinkerLoadSuccess(applicationLike)) {
      return false;
    }

    final long elapsedTime = SystemClock.elapsedRealtime() - applicationLike.getApplicationStartElapsedTime();
    //this process may not install tinker, so we use TinkerApplicationHelper api
    if (elapsedTime < QUICK_CRASH_ELAPSE) {
      String currentVersion = TinkerApplicationHelper.getCurrentVersion(applicationLike);
      if (ShareTinkerInternals.isNullOrNil(currentVersion)) {
        return false;
      }

      SharedPreferences sp = applicationLike.getApplication().getSharedPreferences(ShareConstants.TINKER_SHARE_PREFERENCE_CONFIG, Context.MODE_MULTI_PROCESS);
      int fastCrashCount = sp.getInt(currentVersion, 0) + 1;
      if (fastCrashCount >= MAX_CRASH_COUNT) {
        SampleTinkerReport.onFastCrashProtect();
        TinkerApplicationHelper.cleanPatch(applicationLike);
        TinkerLog.e(TAG, "tinker has fast crash more than %d, we just clean patch!", fastCrashCount);
        return true;
      } else {
        sp.edit().putInt(currentVersion, fastCrashCount).commit();
        TinkerLog.e(TAG, "tinker has fast crash %d times", fastCrashCount);
      }
    }

    return false;
  }
}

5.生成补丁文件
1.生成安装包:
打开终端Terminal执行gradlew assembleDebug命令后,会在build/bakApk目录下生成一个安装包和文本文件。

2017-08-02_173659.png

在build.gradle文件中将后面的编号给为一致,如图所示
Android热修复Tinker集成_第1张图片
image.png

2.生成补丁包:
打开终端Terminal执行gradlew tinkerPatchDebug命令后,在build/outputs/tinkerPatch/debug目录下会生成补丁文件,有签名和不签名的,
Android热修复Tinker集成_第2张图片
image.png

我们将补丁包patch_signed_7zip.apk模拟从服务器下载到手机本地,放在SDcard目录下
6.加载补丁

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");

运行上面代码即可加载补丁。
在打补丁之前,我们点击按钮弹个Toast,在补丁包中更改Toast内容。打完补丁后,再点击按钮即可看见Toast内容已改变为Patch

Android热修复Tinker集成_第3张图片
image.png

你可能感兴趣的:(Android热修复Tinker集成)