background
The normal development process of an App should be like this: the launch of a new version –> user installation –> Bug discovery –> urgent repair –> re-release of a new version –> prompt users to install updates. On the surface, such a development process is logical, but there are many disadvantages: 1. It’s time consuming, it’s expensive, and sometimes it can be a very small problem, but you have to take it down and update it. 2. Poor user experience and high installation cost. A small bug will cause users to re-download the entire application installation package to cover the installation, and also increase users’ traffic expenditure. So the question is, is there a way to fix the Bug dynamically, without the need to download the App again, at a lower cost without the user’s awareness? The answer is yes, thermal repair can.
An overview of the
At present, there are many implementation schemes of hot repair, among which the well-known ones are Ali’s AndFix, Meituan’s Robust, QZone’s super patch and wechat’s Tinker. This paper will simply analyze the access and implementation principle of Tinker, and there will be no further details about Tinker here. Note that Tinker is not a panacea and has limitations: 1. Tinker does not support modification of Androidmanifest.xml; 2. Tinker does not support four new components; 3. On Android N, patches have a slight impact on app startup time; 4. Some Samsung Android-21 models are not supported, and exceptions will be thrown when loading patches; Tinker no longer supports hardened dynamic updates in 1.7.6 and later. 6. For resource replacement, modifying remoteView is not supported. Examples are the Transition animation, the Notification icon, and the desktop icon. 7, any thermal repair technology can not achieve 100% successful repair.
Access to the
Tinker provides two types of access: Gradle and the command line. Here we use Gradle dependent access as an example. Add the tinker-patch-gradle-plugin dependency to the build.gradle project
buildscript {
dependencies {
classpath ('com. Tencent. Tinker: tinker - patch - gradle - plugin: 1.7.7')}}Copy the code
In app gradle file app/build.gradle, we need to add tinker library dependencies and apply Tinker gradle plugin.
dependencies {
// Optional, used to generate the Application class
provided('com. Tencent. Tinker: tinker - android - anno: 1.7.7')
// Tinker's core library
compile('com. Tencent. Tinker: tinker - android - lib: 1.7.7')}/ / the apply tinker plug-in
apply plugin: 'com.tencent.tinker.patch'Copy the code
Signature configuration
signingConfigs {
release {
try {
storeFile file("./keystore/release.keystore")
storePassword "testres"
keyAlias "testres"
keyPassword "testres"
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
debug {
storeFile file("./keystore/debug.keystore")
}
}
buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
debuggable true
minifyEnabled false
signingConfig signingConfigs.debug}}Copy the code
File directory Configuration
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
//for normal build
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/app-debug-0406-10-59-13.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-debug-0406-10-59-13-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-debug-0406-10-59-13-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/app-debug-0406-10-59-13"
}Copy the code
For details about how to set parameters, see app/build.gradle in tinker Sample. Create a new Application that initializes Tinker in the onCreate() method, but Tinker itself provides a reflection mechanism to implement Application. As you can see from the code, it is not a subclass of Application, as described below.
@SuppressWarnings("unused")
@DefaultLifeCycle(application = ".SampleApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class SampleApplicationLike extends DefaultApplicationLike {
private static final String TAG = "Tinker.SampleApplicationLike";
public SampleApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
}
@Override
public void onCreate(a) {
super.onCreate();
TinkerInstaller.install(this);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) { getApplication().registerActivityLifecycleCallbacks(callback); }}Copy the code
The name of the “application” tag is application and must be consistent with androidmanifest.xml
<application
android:allowBackup="true"
android:name=".SampleApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">... . </application>Copy the code
In the Activity, simulate the hot fix loading PATCH to solve the null pointer exception. Click the settext button to set the “TINKER PATCH” for the TextView. The null pointer exception occurs because the TextView has not been initialized.
public class MainActivity extends AppCompatActivity {
private TextView tv_msg;
private Button btn_loadpatch;
private Button btn_settext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init(a) {
// If you set TextView directly without initializing it, a null pointer will appear
//tv_msg=(TextView)findViewById(R.id.tv_msg);
btn_loadpatch=(Button)findViewById(R.id.btn_loadpatch);
btn_settext=(Button)findViewById(R.id.btn_settext);
btn_settext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// a null pointer exception is reported
tv_msg.setText("TINKER PATCH"); }});// Load the patch
btn_loadpatch.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
Environment.getExternalStorageDirectory().getAbsolutePath() +
"/patch_unsigned.apk"); }}); }}Copy the code
After compiling with Gradle, local packaged APK is generated in build/bakApk (Debug does not generate mapping file)
Since the TextView is not initialized, modify the Activity code to initialize the TextView and resolve the null pointer exception.
public class MainActivity extends AppCompatActivity {
private TextView tv_msg;
private Button btn_loadpatch;
private Button btn_settext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init(a) {
// Initialize TextView here and fix null pointer exception
tv_msg=(TextView)findViewById(R.id.tv_msg);
btn_loadpatch=(Button)findViewById(R.id.btn_loadpatch);
btn_settext=(Button)findViewById(R.id.btn_settext);
btn_settext.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
tv_msg.setText("TINKER PATCH"); }}); btn_loadpatch.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
Environment.getExternalStorageDirectory().getAbsolutePath() +
"/patch_unsigned.apk"); }}); }}Copy the code
Gradlew can be used to generate subpackages using the gradlew command. Before this, you need to set up two apps for comparison in app/build.gradle, where app-debug-0406-10-33-27.
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
//for normal build
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/app-debug-0406-10-33-27.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-debug-0406-10-33-27-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-debug-0406-10-33-27-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/app-debug-0406-10-33-27"
}Copy the code
./gradlew tinkerPatchRelease / / Release package
./gradlew tinkerPatchDebug / / the Debug packagesCopy the code
Poor subcontract in the build/outputs/tinkerPatch directory, patch_unsigned. Apk as no signature patches, patch_signed. The apk is signed patches, Patch_signed_7zip. Apk is a patch package that has been signed and compressed with 7zip, which is also recommended by Tinker. There is no signature package here, so patch_unsigned.
First click “bTN_loadPatch” button to load the patch, then click “settext” button to see that the null pointer exception has been fixed. Operation effect picture:
Operation principle
Tinker compares the two apps to find a subcontractor, patch.dex, and then merges patch.dex with the application’s classes.dex to replace the old dex file.
I. Application generation
The Application is generated using Java annotations, which are generated at compile time. An annotation method is defined under com.tencent.tinker.anno. The annotation is discarded by the compiler, but retains the source file. 3. The class is inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
@Inherited
public @interface DefaultLifeCycle {
String application(a);
String loaderClass(a) default "com.tencent.tinker.loader.TinkerLoader";
int flags(a);
boolean loadVerifyFlag(a) default false;
}Copy the code
A tinkerApplication. TMPL Application template is stored in the com.tencent. Tinker. anno package: %TINKER_FLAGS% corresponds to flags %APPLICATION_LIFE_CYCLE%, which is the full path of ApplicationLike %TINKER_LOADER_CLASS%, The loaderClass attribute %TINKER_LOAD_VERIFY_FLAG% corresponds to loadVerifyFlag
public class %APPLICATION% extends TinkerApplication {
public %APPLICATION% () {super(%TINKER_FLAGS%, "%APPLICATION_LIFE_CYCLE%"."%TINKER_LOADER_CLASS%"%,TINKER_LOAD_VERIFY_FLAG%); }}Copy the code
AbstractProcessor class, com.tencent.tinker. Anno package, inherits the AnnotationProcessor class and has specific implementation. The processDefaultLifeCycle method iterates through the object identified by DefaultLifeCycle, gets the values declared in the annotation, then reads the template, fills in the values, and finally generates an Application instance that inherits from TinkerApplication
private void processDefaultLifeCycle(Set<? extends Element> elements) {
Iterator var2 = elements.iterator();
while(var2.hasNext()) {
Element e = (Element)var2.next();
DefaultLifeCycle ca = (DefaultLifeCycle)e.getAnnotation(DefaultLifeCycle.class);
String lifeCycleClassName = ((TypeElement)e).getQualifiedName().toString();
String lifeCyclePackageName = lifeCycleClassName.substring(0, lifeCycleClassName.lastIndexOf(46));
lifeCycleClassName = lifeCycleClassName.substring(lifeCycleClassName.lastIndexOf(46) + 1);
String applicationClassName = ca.application();
if(applicationClassName.startsWith(".")) {
applicationClassName = lifeCyclePackageName + applicationClassName;
}
String applicationPackageName = applicationClassName.substring(0, applicationClassName.lastIndexOf(46));
applicationClassName = applicationClassName.substring(applicationClassName.lastIndexOf(46) + 1);
String loaderClassName = ca.loaderClass();
if(loaderClassName.startsWith(".")) {
loaderClassName = lifeCyclePackageName + loaderClassName;
}
System.out.println("*");
InputStream is = AnnotationProcessor.class.getResourceAsStream("/TinkerAnnoApplication.tmpl");
Scanner scanner = new Scanner(is);
String template = scanner.useDelimiter("\\A").next();
String fileContent = template.replaceAll("%PACKAGE%", applicationPackageName).replaceAll("%APPLICATION%", applicationClassName).replaceAll("%APPLICATION_LIFE_CYCLE%", lifeCyclePackageName + "." + lifeCycleClassName).replaceAll("%TINKER_FLAGS%"."" + ca.flags()).replaceAll("%TINKER_LOADER_CLASS%"."" + loaderClassName).replaceAll("%TINKER_LOAD_VERIFY_FLAG%"."" + ca.loadVerifyFlag());
try {
JavaFileObject x = this.processingEnv.getFiler().createSourceFile(applicationPackageName + "." + applicationClassName, new Element[0]);
this.processingEnv.getMessager().printMessage(Kind.NOTE, "Creating " + x.toUri());
Writer writer = x.openWriter();
try {
PrintWriter pw = new PrintWriter(writer);
pw.print(fileContent);
pw.flush();
} finally{ writer.close(); }}catch (IOException var21) {
this.processingEnv.getMessager().printMessage(Kind.ERROR, var21.toString()); }}}Copy the code
Second, the execution process
The loadTinker() method is called in the onBaseContextAttached() method of the TinkerApplication
private void loadTinker() {
//disable tinker, not need to install
if (tinkerFlags == TINKER_DISABLE) {
return;
}
tinkerResultIntent = new Intent();
try {
//reflect tinker loader, because loaderClass may be define by user!
Class<? > tinkerLoadClass =Class.forName(loaderClassName, false, getClassLoader());
Method loadMethod = tinkerLoadClass.getMethod(TINKER_LOADER_METHOD, TinkerApplication.class.int.class.boolean.class); Constructor<? > constructor = tinkerLoadClass.getConstructor(); tinkerResultIntent = (Intent) loadMethod.invoke(constructor.newInstance(),this, tinkerFlags, tinkerLoadVerifyFlag);
} catch (Throwable e) {
//has exception, put exception error codeShareIntentUtil.setIntentReturnCode(tinkerResultIntent, ShareConstants.ERROR_LOAD_PATCH_UNKNOWN_EXCEPTION); tinkerResultIntent.putExtra(INTENT_PATCH_EXCEPTION, e); }}Copy the code
Call the tryLoad method in TinkerLoader by reflection in loadTinker
@Override
public Intent tryLoad(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag) {
Intent resultIntent = new Intent();
long begin = SystemClock.elapsedRealtime();
tryLoadPatchFilesInternal(app, tinkerFlag, tinkerLoadVerifyFlag, resultIntent);
long cost = SystemClock.elapsedRealtime() - begin;
ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
return resultIntent;
}Copy the code
In tryLoadPatchFilesInternal () method to load the local patches, dex file comparison and added to the dexList
if (isEnabledForDex) {
//tinker/patch.info/patch-641e634c/dex
boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
if(! dexCheck) {//file not found, do not load patch
Log.w(TAG."tryLoadPatchFiles:dex check fail");
return; }}//now we can load patch jar
if (isEnabledForDex) {
boolean loadTinkerJars = TinkerDexLoader.loadTinkerJars(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent, isSystemOTA);
if(! loadTinkerJars) {Log.w(TAG."tryLoadPatchFiles:onPatchLoadDexesFail");
return; }}//now we can load patch resource
if (isEnabledForResource) {
boolean loadTinkerResources = TinkerResourceLoader.loadTinkerResources(app, tinkerLoadVerifyFlag, patchVersionDirectory, resultIntent);
if(! loadTinkerResources) {Log.w(TAG."tryLoadPatchFiles:onPatchLoadResourcesFail");
return; }}Copy the code
Then fix installDexes in the core class SystemClassLoaderAdde. Depending on the Android version, the method is different. Determine the Android version by installDexes and perform corresponding operations. The Element[] array is then combined and saved to the pathList
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));
if (suppressedExceptions.size(a) >0) {
for (IOException e : suppressedExceptions) {
Log.w(TAG, "Exception in makePathElement", e);
throwe; }}}Copy the code
Tinker starts TinkerPatchService to perform the merge. TinkerPatchService inherits from IntentService. Just focus on onHandleIntent(). In this method call upgradepatch.trypatch (), and finally merge the extractDexDiffInternals method in the DexDiffPatchInternal class
@Override
protected void onHandleIntent(Intent intent) {
final Context context = getApplicationContext();
Tinker tinker = Tinker.with(context);
tinker.getPatchReporter().onPatchServiceStart(intent);
if (intent == null) {
TinkerLog.e(TAG, "TinkerPatchService received a null intent, ignoring.");
return;
}
String path = getPatchPathExtra(intent);
if (path == null) {
TinkerLog.e(TAG, "TinkerPatchService can't get the path extra, ignoring.");
return;
}
File patchFile = new File(path);
long begin = SystemClock.elapsedRealtime();
boolean result;
long cost;
Throwable e = null;
increasingPriority();
PatchResult patchResult = new PatchResult();
try {
if (upgradePatchProcessor == null) {
throw new TinkerRuntimeException("upgradePatchProcessor is null.");
}
result = upgradePatchProcessor.tryPatch(context, path, patchResult);
} 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));
}Copy the code
For Tinker merger algorithm, you can refer to Tinker Dexdiff algorithm for analysis