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)





bakApk

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.





Difference of the subcontract

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:





Running effect drawing

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