/**
\* tinkerFlags, which types is supported
\* dex only, library only, all support
\* default: TINKER_ENABLE_ALL
*/
private final int tinkerFlags;
private final boolean tinkerLoadVerifyFlag;
private final String delegateClassName;
private final String loaderClassName;
private ITinkerInlineFenceBridge mBridge = null;
private static final String TINKER_LOADER_METHOD = "tryLoad";
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
Thread.setDefaultUncaughtExceptionHandler(new TinkerUncaughtHandler(this));
onBaseContextAttached(base);//main
}
private void onBaseContextAttached(Context base) {
try {
final long applicationStartElapsedTime = SystemClock.elapsedRealtime();
final long applicationStartMillisTime = System.currentTimeMillis();
loadTinker();//main
//补丁应用之后才初始化TinkerApplicationInlineFence类,并在其中反射构造SampleApplicationLike,达到修复application代码的目的
mBridge = createInlineFence(tinkerFlags, delegateClassName,
tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime,
tinkerResultIntent);
mBridge.attachBaseContext(this, base);
//reset save mode
if (useSafeMode) {
ShareTinkerInternals.setSafeModeCount(this, 0);
}
}
}
private void loadTinker() {
try {
//reflect tinker loader, because loaderClass may be define by user!
Class<?> tinkerLoadClass = Class.forName(loaderClassName, false, TinkerApplication.class.getClassLoader());
Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class);
Constructor<?> constructor = tinkerLoadClass.getConstructor();
tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(), this);
} catch (Throwable e) {
//has exception, put exception error code
tinkerResultIntent = new Intent();
ShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION);
tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e);
}
}
class TinkerLoader extends AbstractTinkerLoader {
/**
* only main process can handle patch version change or incomplete
*/
@Override
public Intent tryLoad(TinkerApplication app) {
Log.d(TAG, "tryLoad test test");
Intent resultIntent = new Intent();
long begin = SystemClock.elapsedRealtime();
tryLoadPatchFilesInternal(app, resultIntent);
long cost = SystemClock.elapsedRealtime() - begin;
ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
return resultIntent;
}
private void tryLoadPatchFilesInternal(TinkerApplication app, Intent resultIntent) {
//data dir, such as /data/data/tinker.sample.android/tinker
File patchDirectoryFile = SharePatchFileUtil.getPatchDirectory(app);
//tinker
String patchDirectoryPath = patchDirectoryFile.getAbsolutePath();
//tinker/patch.info
File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectoryPath);
patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
boolean isRemoveNewVersion = patchInfo.isRemoveNewVersion;
// So far new version is not loaded in main process and other processes.
// We can remove new version directory safely.
if (mainProcess && isRemoveNewVersion) {
Log.w(TAG, "found clean patch mark and we are in main process, delete patch file now.");
String patchName = SharePatchFileUtil.getPatchVersionDirectory(newVersion);
if (patchName != null) {
String patchVersionDirFullPath = patchDirectoryPath + "/" + patchName;
SharePatchFileUtil.deleteDir(patchVersionDirFullPath);
SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
}
}
//tinker/patch-641e634c
String patchVersionDirectory = patchDirectoryPath + "/" + patchName;
File patchVersionDirectoryFile = new File(patchVersionDirectory);
if (!patchVersionDirectoryFile.exists()) {
//we may delete patch info file
ShareIntentUtil.setIntentReturnCode(resultIntent, ShareConstants.ERROR_LOAD_PATCH_VERSION_DIRECTORY_NOT_EXIST);
return;
}
//tinker/patch-641e634c/patch-641e634c.apk
final String patchVersionFileRelPath = SharePatchFileUtil.getPatchVersionFile(version);
File patchVersionFile = (patchVersionFileRelPath != null ? new File(patchVersionDirectoryFile.getAbsolutePath(), patchVersionFileRelPath) : null);
ShareSecurityCheck securityCheck = new ShareSecurityCheck(app);
int returnCode = ShareTinkerInternals.checkTinkerPackage(app, tinkerFlag, patchVersionFile, securityCheck);
if (returnCode != ShareConstants.ERROR_PACKAGE_CHECK_OK) {
//now we can load patch jar
if (!isArkHotRuning && isEnabledForDex) {
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, patchVersionDirectory, oatDex, resultIntent, isSystemOTA, isProtectedApp);//main
}
}
/**
\* Load tinker JARs and add them to
\* the Application ClassLoader.
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public static boolean loadTinkerJars(final TinkerApplication application, String directory, String oatDir, Intent intentResult, boolean isSystemOTA, boolean isProtectedApp) {
BaseDexClassLoader classLoader = (BaseDexClassLoader) TinkerDexLoader.class.getClassLoader();
if (classLoader != null) {
Log.i(TAG, "classloader: " + classLoader.toString());
} else {
Log.e(TAG, "classloader is null");
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_CLASSLOADER_NULL);
return false;
}
File optimizeDir = new File(directory + "/" + oatDir);//main
......
try {
SystemClassLoaderAdder.installDexes(application, classLoader, optimizeDir, legalFiles, isProtectedApp);//main
} catch (Throwable e) {
Log.e(TAG, "install dexes failed");
intentResult.putExtra(ShareIntentUtil.INTENT_PATCH_EXCEPTION, e);
ShareIntentUtil.setIntentReturnCode(intentResult, ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION);
return false;
}
return true;
public static void installDexes(Application application, BaseDexClassLoader loader, File dexOptDir, List<File> files, boolean isProtectedApp)
throws Throwable {
Log.i(TAG, "installDexes dexOptDir: " + dexOptDir.getAbsolutePath() + ", dex size:" + files.size());
if (!files.isEmpty()) {
files = createSortedAdditionalPathEntries(files);
ClassLoader classLoader = loader;
if (Build.VERSION.SDK_INT >= 24 && !isProtectedApp) {
classLoader = AndroidNClassLoader.inject(loader, application);
}
//because in dalvik, if inner class is not the same classloader with it wrapper class.
//it won't fail at dex2opt
if (Build.VERSION.SDK_INT >= 23) {
V23.install(classLoader, files, dexOptDir);//main
} else if (Build.VERSION.SDK_INT >= 19) {
V19.install(classLoader, files, dexOptDir);
} else if (Build.VERSION.SDK_INT >= 14) {
V14.install(classLoader, files, dexOptDir);
} else {
V4.install(classLoader, files, dexOptDir);
}
//install done
sPatchDexCount = files.size();
Log.i(TAG, "after loaded classloader: " + classLoader + ", dex size:" + sPatchDexCount);
if (!checkDexInstall(classLoader)) {
//reset patch dex
SystemClassLoaderAdder.uninstallPatchDex(classLoader);
throw new TinkerRuntimeException(ShareConstants.CHECK_DEX_INSTALL_FAIL);
}
/**
\* Installer for platform versions 23.
*/
private static final class V23 {
private static void install(ClassLoader loader, List<File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
/* The patched class loader is expected to be a descendant of
\* dalvik.system.BaseDexClassLoader. We modify its
\* dalvik.system.DexPathList pathList field to append additional DEX
\* file entries.
*/
Field pathListField = ShareReflectUtil.findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makePathElements(dexPathList,
new ArrayList<File>(additionalClassPathEntries), optimizedDirectory,
suppressedExceptions));//main
if (suppressedExceptions.size() > 0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makePathElement", e);
throw e;
}}}
/**
\* A wrapper around
\* {@code private static final dalvik.system.DexPathList#makePathElements}.
*/
private static Object[] makePathElements(
Object dexPathList, ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Method makePathElements;
try {
makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class, File.class,
List.class);
} catch (NoSuchMethodException e) {
Log.e(TAG, "NoSuchMethodException: makePathElements(List,File,List) failure");
try {
makePathElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", ArrayList.class, File.class, ArrayList.class);
} catch (NoSuchMethodException e1) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(ArrayList,File,ArrayList) failure");
try {
Log.e(TAG, "NoSuchMethodException: try use v19 instead");
return V19.makeDexElements(dexPathList, files, optimizedDirectory, suppressedExceptions);
} catch (NoSuchMethodException e2) {
Log.e(TAG, "NoSuchMethodException: makeDexElements(List,File,List) failure");
throw e2;
}
}
}
return (Object[]) makePathElements.invoke(dexPathList, files, optimizedDirectory, suppressedExceptions);
}
}
interface ITinkerInlineFenceBridge {
void attachBaseContext(TinkerApplication app, Context base);
void onCreate(TinkerApplication app);
void onConfigurationChanged(Configuration newConfig);
void onTrimMemory(int level);
void onLowMemory();
void onTerminate();
ClassLoader getClassLoader(ClassLoader cl);
Context getBaseContext(Context base);
AssetManager getAssets(AssetManager assets);
Resources getResources(Resources res);
Object getSystemService(String name, Object service);
}
class TinkerApplicationInlineFence implements ITinkerInlineFenceBridge {
private final Intent mTinkerResultIntent;
private ApplicationLike mApplicationLike = null;
}
@Override
public void attachBaseContext(TinkerApplication app, Context base) {
attachBaseContextImpl_$noinline$(app, base);
}
private void attachBaseContextImpl_$noinline$(TinkerApplication app, Context base) {
try {
dummyThrowExceptionMethod();
} finally {
ensureDelegate(app);//main
if (mApplicationLike != null) {
mApplicationLike.onBaseContextAttached(base);//main
}
}
}
private synchronized void ensureDelegate(TinkerApplication app) {
if (mApplicationLike == null) {
mApplicationLike = (ApplicationLike) createDelegate(app);
}
}
private Object createDelegate(TinkerApplication app) {
try {
// Use reflection to create the delegate so it doesn't need to go into the primary dex.
// And we can also patch it
Class<?> delegateClass = Class.forName(mDelegateClassName, false, this.getClass().getClassLoader());
Constructor<?> constructor = delegateClass.getConstructor(Application.class, int.class, boolean.class,
long.class, long.class, Intent.class);
return constructor.newInstance(app, mTinkerFlags, mTinkerLoadVerifyFlag,
mApplicationStartElapsedTime, mApplicationStartMillisTime, mTinkerResultIntent);
} catch (Throwable e) {
throw new TinkerRuntimeException("createDelegate failed", e);
}
}
public DefaultApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
classDiagram
class ApplicationLike {
+getApplication: Application
+getTinkerResultIntent: Intent
}
ApplicationLifeCycle<|--ApplicationLike
ApplicationLike<|--DefaultApplicationLike
DefaultApplicationLike<|--SampleApplicationLike
graph LR
原先的自定义Application-->|代码迁移到|SampleApplicationLike-->|编译时生成注解中指定的|新的自定义Application-->|代理转发到|SampleApplicationLike
@interface DefaultLifeCycle {
String application();
String loaderClass() default "com.tencent.tinker.loader.TinkerLoader";
int flags();
boolean loadVerifyFlag() default false;
}
@DefaultLifeCycle(application = "tinker.sample.android.app.SampleApplication", //application name to generate
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
class SampleApplicationLike {
/**
* install multiDex before install tinker
* so we don't need to put the tinker lib classes in the main dex
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
//you must install multiDex whatever tinker is installed!
MultiDex.install(base);
SampleApplicationContext.application = getApplication();
SampleApplicationContext.context = getApplication();
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());
//installTinker after load multiDex
//or you can put com.tencent.tinker.** to main dex
TinkerManager.installTinker(this);//main
Tinker tinker = Tinker.with(getApplication());
}
}
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}
private static ApplicationLike applicationLike;
private static SampleUncaughtExceptionHandler uncaughtExceptionHandler;
private static boolean isInstalled = false;
public static void setUpgradeRetryEnable(boolean enable) {
UpgradePatchRetry.getInstance(applicationLike.getApplication()).setRetryEnable(enable);
}
/**
\* 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;
}
TinkerInstaller.install
public static Tinker install(ApplicationLike applicationLike, LoadReporter loadReporter, PatchReporter patchReporter,
PatchListener listener, Class<? extends AbstractResultService> resultServiceClass,
AbstractPatch upgradePatchProcessor) {
Tinker tinker = new Tinker.Builder(applicationLike.getApplication())
.tinkerFlags(applicationLike.getTinkerFlags())
.loadReport(loadReporter)
.listener(listener)
.patchReporter(patchReporter)
.tinkerLoadVerifyFlag(applicationLike.getTinkerLoadVerifyFlag()).build();
Tinker.create(tinker);
tinker.install(applicationLike.getTinkerResultIntent(), resultServiceClass, upgradePatchProcessor);
return tinker;
}
public static class Builder {
public Builder(Context context) {
if (context == null) {
throw new TinkerRuntimeException("Context must not be null.");
}
this.context = context;
this.mainProcess = TinkerServiceInternals.isInMainProcess(context);
this.patchProcess = TinkerServiceInternals.isInTinkerPatchServiceProcess(context);
this.patchDirectory = SharePatchFileUtil.getPatchDirectory(context);
if (this.patchDirectory == null) {
TinkerLog.e(TAG, "patchDirectory is null!");
return;
}
this.patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory.getAbsolutePath());
this.patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory.getAbsolutePath());
TinkerLog.w(TAG, "tinker patch directory: %s", patchDirectory);
}
/**create custom tinker by {@link Tinker.Builder}
* please do it when very first your app start. */
public static void create(Tinker tinker) {
if (sInstance != null) {
throw new TinkerRuntimeException("Tinker instance is already set.");
}
sInstance = tinker;
}
public void install(Intent intentResult) {
install(intentResult, DefaultTinkerResultService.class, new UpgradePatch());
}
public void install(Intent intentResult, Class<? extends AbstractResultService> serviceClass,
AbstractPatch upgradePatch) {
sInstalled = true;
TinkerPatchService.setPatchProcessor(upgradePatch, serviceClass);
TinkerLog.i(TAG, "try to install tinker, isEnable: %b, version: %s", isTinkerEnabled(), ShareConstants.TINKER_VERSION);
if (!isTinkerEnabled()) {
TinkerLog.e(TAG, "tinker is disabled");
return;
}
if (intentResult == null) {
throw new TinkerRuntimeException("intentResult must not be null.");
}
tinkerLoadResult = new TinkerLoadResult();
//从intentResult中获取loadTinker的结果tinkerResultIntent(位于TinkerApplication),这个结果经过反射构造TinkerApplicationInlineFence和SampleApplicationLike等最终传入到此处
tinkerLoadResult.parseTinkerResult(getContext(), intentResult);//main
//after load code set
loadReporter.onLoadResult(patchDirectory, tinkerLoadResult.loadCode, tinkerLoadResult.costTime);//main
if (!loaded) {
TinkerLog.w(TAG, "tinker load fail!");
}
}
public boolean parseTinkerResult(Context context, Intent intentResult) {
Tinker tinker = Tinker.with(context);
loadCode = ShareIntentUtil.getIntentReturnCode(intentResult);
costTime = ShareIntentUtil.getIntentPatchCostTime(intentResult);
systemOTA = ShareIntentUtil.getBooleanExtra(intentResult, ShareIntentUtil.INTENT_PATCH_SYSTEM_OTA, false);
oatDir = ShareIntentUtil.getStringExtra(intentResult, ShareIntentUtil.INTENT_PATCH_OAT_DIR);
useInterpretMode = ShareConstants.INTERPRET_DEX_OPTIMIZE_PATH.equals(oatDir);
final String oldVersion = ShareIntentUtil.getStringExtra(intentResult, ShareIntentUtil.INTENT_PATCH_OLD_VERSION);
final String newVersion = ShareIntentUtil.getStringExtra(intentResult, ShareIntentUtil.INTENT_PATCH_NEW_VERSION);
//found uncaught exception, just return
Throwable exception = ShareIntentUtil.getIntentPatchException(intentResult);
if (exception != null) {
TinkerLog.i(TAG, "Tinker load have exception loadCode:%d", loadCode);
int errorCode = ShareConstants.ERROR_LOAD_EXCEPTION_UNKNOWN;
switch (loadCode) {
case ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION:
errorCode = ShareConstants.ERROR_LOAD_EXCEPTION_UNKNOWN;
break;
case ShareConstants.ERROR_LOAD_PATCH_VERSION_DEX_LOAD_EXCEPTION:
errorCode = ShareConstants.ERROR_LOAD_EXCEPTION_DEX;
break;
case ShareConstants.ERROR_LOAD_PATCH_VERSION_RESOURCE_LOAD_EXCEPTION:
errorCode = ShareConstants.ERROR_LOAD_EXCEPTION_RESOURCE;
break;
case ShareConstants.ERROR_LOAD_PATCH_UNCAUGHT_EXCEPTION:
errorCode = ShareConstants.ERROR_LOAD_EXCEPTION_UNCAUGHT;
break;
default:
break;
}
tinker.getLoadReporter().onLoadException(exception, errorCode);
return false;
}
switch (loadCode) {
case ShareConstants.ERROR_LOAD_OK:
TinkerLog.i(TAG, "oh yeah, tinker load all success");
tinker.setTinkerLoaded(true);
// get load dex
dexes = ShareIntentUtil.getIntentPatchDexPaths(intentResult);
libs = ShareIntentUtil.getIntentPatchLibsPaths(intentResult);
packageConfig = ShareIntentUtil.getIntentPackageConfig(intentResult);
if (useInterpretMode) {
tinker.getLoadReporter().onLoadInterpret(ShareConstants.TYPE_INTERPRET_OK, null);
}
if (isMainProcess && versionChanged) {
//change the old version to new
tinker.getLoadReporter().onLoadPatchVersionChanged(oldVersion, newVersion, patchDirectory, patchVersionDirectory.getName());
}
return true;
@Override
public void onLoadException(Throwable e, int errorCode) {
//for unCaught or dex exception, disable tinker all the time with sp
switch (errorCode) {
case ShareConstants.ERROR_LOAD_EXCEPTION_DEX:
if (e.getMessage().contains(ShareConstants.CHECK_DEX_INSTALL_FAIL)) {
TinkerLog.e(TAG, "patch loadReporter onLoadException: tinker dex check fail:" + e.getMessage());
} else {
TinkerLog.i(TAG, "patch loadReporter onLoadException: patch load dex exception: %s", e);
}
ShareTinkerInternals.setTinkerDisableWithSharedPreferences(context);
TinkerLog.i(TAG, "dex exception disable tinker forever with sp");
break;
case ShareConstants.ERROR_LOAD_EXCEPTION_RESOURCE:
case ShareConstants.ERROR_LOAD_EXCEPTION_UNCAUGHT:
case ShareConstants.ERROR_LOAD_EXCEPTION_UNKNOWN:
......
TinkerLog.e(TAG, "tinker load exception, welcome to submit issue to us: https://github.com/Tencent/tinker/issues");
TinkerLog.printErrStackTrace(TAG, e, "tinker load exception");
Tinker.with(context).setTinkerDisable();
checkAndCleanPatch();
}
@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();//main
}
return false;
}
});
}
//UpgradePatchRetry
public boolean onPatchRetryLoad() {
......
TinkerInstaller.onReceiveUpgradePatch(context, path);//main
}
//TinkerInstaller
/**
* new patch file to install, try install them with :patch process
*/
public static void onReceiveUpgradePatch(Context context, String patchLocation) {
Tinker.with(context).getPatchListener().onPatchReceived(patchLocation);
}
interface PatchListener {
int onPatchReceived(String path);
}
/**
\* when we receive a patch, what would we do?
\* you can overwrite it
\* */
@Override
public int onPatchReceived(String path) {
final File patchFile = new File(path);
final String patchMD5 = SharePatchFileUtil.getMD5(patchFile);
final int returnCode = patchCheck(path, patchMD5);
if (returnCode == ShareConstants.ERROR_PATCH_OK) {
runForgService();
TinkerPatchService.runPatchService(context, path);//main
} else {
Tinker.with(context).getLoadReporter().onLoadPatchListenerReceiveFail(new File(path), returnCode);
}
return returnCode;
}
class TinkerPatchService extends IntentService {
public static void setPatchProcessor(AbstractPatch upgradePatch, Class<? extends AbstractResultService> serviceClass) {
upgradePatchProcessor = upgradePatch;
resultServiceClass = serviceClass;
//try to load
try {
Class.forName(serviceClass.getName());
} catch (ClassNotFoundException e) {
TinkerLog.printErrStackTrace(TAG, e, "patch processor class not found.");
}
}
}
public static void runPatchService(final Context context, final String path) {
TinkerLog.i(TAG, "run patch service...");
Intent intent = new Intent(context, TinkerPatchService.class);
intent.putExtra(PATCH_PATH_EXTRA, path);
intent.putExtra(RESULT_CLASS_EXTRA, resultServiceClass.getName());
try {
context.startService(intent);
} catch (Throwable thr) {
TinkerLog.e(TAG, "run patch service fail, exception:" + thr);
}
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
increasingPriority();
doApplyPatch(this, intent);
}
private static void doApplyPatch(Context context, Intent intent) {
Tinker tinker = Tinker.with(context);
tinker.getPatchReporter().onPatchServiceStart(intent);//超过maxRetryCount会清除patch,最大重试20次
PatchResult patchResult = new PatchResult();
try {
if (upgradePatchProcessor == null) {
throw new TinkerRuntimeException("upgradePatchProcessor is null.");
}
result = upgradePatchProcessor.tryPatch(context, path, patchResult);//main
} catch (Throwable throwable) {
e = throwable;
result = false;
tinker.getPatchReporter().onPatchException(patchFile, e);
}
cost = SystemClock.elapsedRealtime() - begin;
tinker.getPatchReporter()
.onPatchResult(patchFile, result, cost);
patchResult.isSuccess = result;
patchResult.rawPatchFilePath = path;
patchResult.costTime = cost;
patchResult.e = e;
AbstractResultService.runResultService(context, patchResult, getPatchResultExtra(intent));//main
sIsPatchApplying.set(false);
/************************************ :patch process below ***************************************/
/**
\* use for report or some work at the beginning of TinkerPatchService
\* {@code TinkerPatchService.onHandleIntent} begin
*/
@Override
public void onPatchServiceStart(Intent intent) {
TinkerLog.i(TAG, "patchReporter onPatchServiceStart: patch service start");
shouldRetry = false;
UpgradePatchRetry.getInstance(context).onPatchServiceStart(intent);
}
public void onPatchServiceStart(Intent intent) {
String path = TinkerPatchService.getPatchPathExtra(intent);
File patchFile = new File(path);
String patchMd5 = SharePatchFileUtil.getMD5(patchFile);
if (retryInfoFile.exists()) {
retryInfo = RetryInfo.readRetryProperty(retryInfoFile);
if (retryInfo.md5 == null || retryInfo.times == null || !patchMd5.equals(retryInfo.md5)) {
copyToTempFile(patchFile);//main
retryInfo.md5 = patchMd5;
retryInfo.times = "1";
} else {
int nowTimes = Integer.parseInt(retryInfo.times);
if (nowTimes >= maxRetryCount) {
SharePatchFileUtil.safeDeleteFile(tempPatchFile);
TinkerLog.w(TAG, "onPatchServiceStart retry more than max count, delete retry info file!");
return;
} else {
retryInfo.times = String.valueOf(nowTimes + 1);
}
}
} else {
copyToTempFile(patchFile);//main
retryInfo = new RetryInfo(patchMd5, "1");
}
RetryInfo.writeRetryProperty(retryInfoFile, retryInfo);
@Override
public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
//we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {//main
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
return false;
}
if (!ArkHotDiffPatchInternal.tryRecoverArkHotLibrary(manager, signatureCheck,
context, patchVersionDirectory, destPatchFile)) {
return false;
}
if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed");
return false;
}
if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");
return false;
}
DexDiffPatchInternal
protected static boolean tryRecoverDexFiles(Tinker manager, ShareSecurityCheck checker, Context context,
String patchVersionDirectory, File patchFile) {
boolean result = patchDexExtractViaDexDiff(context, patchVersionDirectory, dexMeta, patchFile);
}
private static boolean patchDexExtractViaDexDiff(Context context, String patchVersionDirectory, String meta, final File patchFile) {
String dir = patchVersionDirectory + "/" + DEX_PATH + "/";
if (!extractDexDiffInternals(context, dir, meta, patchFile, TYPE_DEX)) {//main
TinkerLog.w(TAG, "patch recover, extractDiffInternals fail");
return false;
}
final String optimizeDexDirectory = patchVersionDirectory + "/" + DEX_OPTIMIZE_PATH + "/";
return dexOptimizeDexFiles(context, legalFiles, optimizeDexDirectory, patchFile);//main
private static boolean extractDexDiffInternals(Context context, String dir, String meta, File patchFile, int type) {
patchList.clear();
ShareDexDiffPatchInfo.parseDexDiffPatchInfo(meta, patchList);
//check source crc instead of md5 for faster
String rawEntryCrc = String.valueOf(rawApkFileEntry.getCrc());
if (!rawEntryCrc.equals(oldDexCrc)) {
TinkerLog.e(TAG, "apk entry %s crc is not equal, expect crc: %s, got crc: %s", patchRealPath, oldDexCrc, rawEntryCrc);
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
return false;
}
patchDexFile(apk, patch, rawApkFileEntry, patchFileEntry, info, extractedFile);
//patch合成之后比对(合成后的dex文件)和(生成patch之后立刻合成时得到的relatedInfo.newOrFullPatchedMd5)是否一致
//位于assets/dex_meta.txt中的kv[3],产生于logDexesToDexMeta--logToDexMeta
//String meta = fileName + "," + parentRelative + "," + destMd5InDvm + ","
// + destMd5InArt + "," + dexDiffMd5 + "," + oldCrc + "," + newOrFullPatchedCrc + "," + dexMode;
if (!SharePatchFileUtil.verifyDexFileMd5(extractedFile, extractedFileMd5)) {
TinkerLog.w(TAG, "Failed to recover dex file when verify patched dex: " + extractedFile.getPath());
manager.getPatchReporter().onPatchTypeExtractFail(patchFile, extractedFile, info.rawName, type);
SharePatchFileUtil.safeDeleteFile(extractedFile);
return false;
}
private static void patchDexFile(
ZipFile baseApk, ZipFile patchPkg, ZipEntry oldDexEntry, ZipEntry patchFileEntry,
ShareDexDiffPatchInfo patchInfo, File patchedDexFile) throws IOException {
new DexPatchApplier(zis, patchFileStream).executeAndSaveTo(zos);//main
}
//DexDiffPatchInternal
private static boolean dexOptimizeDexFiles(Context context, List<File> dexFiles, String optimizeDexDirectory, final File patchFile) {
//try parallel dex optimizer
TinkerDexOptimizer.optimizeAll(
context, dexFiles, optimizeDexDirectoryFile,
new TinkerDexOptimizer.ResultCallback() {...
TinkerDexOptimizer
public static boolean optimizeAll(Context context, Collection<File> dexFiles, File optimizedDir,
boolean useInterpretMode, String targetISA, ResultCallback cb) {
for (File dexFile : sortList) {
OptimizeWorker worker = new OptimizeWorker(context, dexFile, optimizedDir, useInterpretMode, targetISA, cb);
if (!worker.run()) {
return false;
}
}
return true;
private static class OptimizeWorker {
boolean run() {
String optimizedPath = SharePatchFileUtil.optimizedPathFor(this.dexFile, this.optimizedDir);
if (!ShareTinkerInternals.isArkHotRuning()) {
if (useInterpretMode) {//默认false
interpretDex2Oat(dexFile.getAbsolutePath(), optimizedPath);//main
} else if (Build.VERSION.SDK_INT >= 28
|| (Build.VERSION.SDK_INT >= 27 && Build.VERSION.PREVIEW_SDK_INT != 0)) {
NewClassLoaderInjector.triggerDex2Oat(context, dexFile.getAbsolutePath());//main
} else {
DexFile.loadDex(dexFile.getAbsolutePath(), optimizedPath, 0);//main
}
}
private void interpretDex2Oat(String dexFilePath, String oatFilePath) throws IOException {
final List<String> commandAndParams = new ArrayList<>();
commandAndParams.add("dex2oat");
// for 7.1.1, duplicate class fix
if (Build.VERSION.SDK_INT >= 24) {
commandAndParams.add("--runtime-arg");
commandAndParams.add("-classpath");
commandAndParams.add("--runtime-arg");
commandAndParams.add("&");
}
commandAndParams.add("--dex-file=" + dexFilePath);
commandAndParams.add("--oat-file=" + oatFilePath);
commandAndParams.add("--instruction-set=" + targetISA);
if (Build.VERSION.SDK_INT > 25) {
commandAndParams.add("--compiler-filter=quicken");
} else {
commandAndParams.add("--compiler-filter=interpret-only");
}
final ProcessBuilder pb = new ProcessBuilder(commandAndParams);
pb.redirectErrorStream(true);
final Process dex2oatProcess = pb.start();
StreamConsumer.consumeInputStream(dex2oatProcess.getInputStream());
StreamConsumer.consumeInputStream(dex2oatProcess.getErrorStream());
try {
final int ret = dex2oatProcess.waitFor();
if (ret != 0) {
throw new IOException("dex2oat works unsuccessfully, exit code: " + ret);
}
}
}
//NewClassLoaderInjector
public static void triggerDex2Oat(Context context, File dexOptDir, boolean useDLCOnAPI29AndAbove,
String... dexPaths) throws Throwable {
ClassLoader triggerClassLoader;
if (useDLCOnAPI29AndAbove && Build.VERSION.SDK_INT >= 29) {
triggerClassLoader = createNewClassLoader(context.getClassLoader(), dexOptDir, useDLCOnAPI29AndAbove, dexPaths);
} else {
// Suggestion from Huawei: Only PathClassLoader (Perhaps other ClassLoaders known by system
// like DexClassLoader also works ?) can be used here to trigger dex2oat so that JIT
// mechanism can participate in runtime Dex optimization.
final StringBuilder sb = new StringBuilder();
boolean isFirst = true;
for (String dexPath : dexPaths) {
if (isFirst) {
isFirst = false;
} else {
sb.append(File.pathSeparator);
}
sb.append(dexPath);
}
triggerClassLoader = new PathClassLoader(sb.toString(), ClassLoader.getSystemClassLoader());
}
}
……
class AbstractResultService extends IntentService {
public static void runResultService(Context context, PatchResult result, String resultServiceClass) {
if (resultServiceClass == null) {
throw new TinkerRuntimeException("resultServiceClass is null.");
}
try {
Intent intent = new Intent();
intent.setClassName(context, resultServiceClass);
intent.putExtra(RESULT_EXTRA, result);
context.startService(intent);
} catch (Throwable throwable) {
TinkerLog.e(TAG, "run result service fail, exception:" + throwable);
}
}
protected void onHandleIntent(Intent intent) {
PatchResult result = (PatchResult) ShareIntentUtil.getSerializableExtra(intent, RESULT_EXTRA);
onPatchResult(result);//main
}
}
/**
\* we may want to use the new patch just now!!
*/
@Override
public void onPatchResult(PatchResult result) {
if (result == null) {
TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");
return;
}
TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());
//first, we want to kill the recover process
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
// if success and newPatch, it is nice to delete the raw file, and restart at once
// only main process can load an upgrade patch!
if (result.isSuccess) {
deleteRawPatchFile(new File(result.rawPatchFilePath));
if (checkIfNeedKill(result)) {
android.os.Process.killProcess(android.os.Process.myPid());
} else {
TinkerLog.i(TAG, "I have already install the newly patch version!");
}
}
}
@Override
public void onPatchResult(final PatchResult result) {
//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());
}
/**clean all patch files */
public void cleanPatch() {
final File patchInfoFile = SharePatchFileUtil.getPatchInfoFile(patchDirectory.getAbsolutePath());
final File patchInfoLockFile = SharePatchFileUtil.getPatchInfoLockFile(patchDirectory.getAbsolutePath());
final SharePatchInfo patchInfo = SharePatchInfo.readAndCheckPropertyWithLock(patchInfoFile, patchInfoLockFile);
if (patchInfo != null) {
patchInfo.isRemoveNewVersion = true;//标记目录:/data/data/${packageName}/tinker/patch.info中的标记信息,下次loadTinker时会根据这个标记清除对应patch目录信息
/*patchinfo.oldVersion表示当前正在使用的已经apply/load完成之后的patch
patchinfo.newVersion表示当前已经成功patch,但还没有apply完成的patch*/
SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, patchInfo, patchInfoLockFile);
}
}
@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.myQueue().addIdleHandler(() -> {
if (UpgradePatchRetry.getInstance(context).onPatchRetryLoad()) {
SampleTinkerReport.onReportRetryPatch();
}
return false;
});
}
UpgradePatchRetry::onPatchRetryLoad
构建出的patch会包含test.dex文件,内部符合dex文件结构,仅仅包含一个class“com.tencent.tinker.loader.TinkerTestDexLoad”,其静态检测方法返回true,用于在install之后进行SystemClassLoaderAdder.checkDexInstall校验,以判别是否成功插入修复的dex文件
位置:(work/Desktop20200621/)
patch过程会将patch文件放入/data/data/${applicationId}/tinker/,目录结构为
tinker–patch.info
–info.lock
–patch332ed5cc–res–
–dex–tinker_classN.apk(内含多个dex,其中既有test.dex重命名之后的dex)
–odex–tinker_classN.dex
patch-332ed5cc.apk