One, a brief introduction

Tinker is wechat’s official Android hot patch solution. It supports dynamic delivery of code, So libraries, and resources, enabling apps to be updated without needing to be reinstalled. Of course, you can also use Tinker to update your plugin.

It doesn’t say that Tinker can make patches work in real time (also known as unaware updates). It has to restart the App (restart the process) after the patch has been installed for it to work, which is fundamentally different from Ali’s hotfix. Before we start integrating Tinker, it’s important to know what it’s not. Here are the known problems:

  1. Tinker does not support modifying AndroidManifest.xml. Tinker does not support adding four components (1.9.0 supports adding non-export activities).
  2. Dynamic code updates in the GP channel are not recommended due to developer terms on Google Play;
  3. On Android N, patches have a slight impact on app startup time;
  4. Do not support the part of the samsung android – 21 models, load the patch will take the initiative to throw “TinkerRuntimeException: checkDexInstall failed”;
  5. Modifying remoteView is not supported for resource replacement. Examples include transition animations, Notification ICONS, and desktop ICONS.

These deficiencies are due to the principle and system restrictions, we should be clear in the programming, as far as possible to avoid the above problems.

Tinker is open source (which means it’s free), and it runs on hundreds of millions of Android devices on wechat (which means it’s pretty stable). Let’s start the integration and use of Tinker.

Tinker component dependencies

1, in the project build.gradle:

Add dependencies to tinker-patch-gradle-plugin

Buildscript {dependencies {classpath (' com. Tencent. Tinker: tinker - patch - gradle - plugin: 1.9.1 ')}}Copy the code

2, app gradle file (app/build.gradle) :

Note that Tinker needs to use MulitDex, as mentioned in the hot Update API section of the Bugly documentation.

1) Add tinker library dependencies

Gradle versions less than 2.3 write:

Dependencies {the compile "com. Android. Support: multidex:" 1.0.1 / / optional, Used to generate the application class provided (' com. Tencent. Tinker: tinker - android - anno: 1.9.1 ') / / tinker at the core of the library The compile (' com. Tencent. Tinker: tinker - android - lib: 1.9.1 ')}Copy the code

Gradle version 2.3:

Dependencies {implementation "com. Android. Support: multidex:" 1.0.1 / / tinker at the core of the library Implementation (" com. Tencent. Tinker: tinker - android - lib: 1.9.1 ") {changing = true} / / optional, Used to generate the application class annotationProcessor (" com. Tencent. Tinker: tinker - android - anno: 1.9.1 ") {changing = true} CompileOnly (" com. Tencent. Tinker: tinker - android - anno: 1.9.1 ") {changing = true}}Copy the code

2) Enable multiDex

defaultConfig {
		...
        multiDexEnabled true
}
Copy the code

3) Apply Tinker’s Gradle plugin

Leave this part alone, and it will be added in Part 3, Tinker’s Configuration and Missions, Section 2, Configuring Tinker and Missions. You can skip this part and move on.

// Apply Tinker plugin: 'com.0ce.tinker.patchCopy the code

3. Configuration and tasks of Tinker

1. Enable supporting large engineering mode

The Tinker documentation recommends setting jumboMode to true.

Android {dexOptions {// support jumboMode = true}... }Copy the code

2. Configure Tinker and tasks

Copy and paste the following configuration to the end of app gradle file (app/build.gradle). It’s a lot of content, but for now you just need to read the bakPath and ext brackets.

// Tinker配置与任务
def bakPath = file("${buildDir}/bakApk/")
ext {
    // 是否使用Tinker(当你的项目处于开发调试阶段时,可以改为false)
    tinkerEnabled = true
    // 基础包文件路径(名字这里写死为old-app.apk。用于比较新旧app以生成补丁包,不管是debug还是release编译)
    tinkerOldApkPath = "${bakPath}/old-app.apk"
    // 基础包的mapping.txt文件路径(用于辅助混淆补丁包的生成,一般在生成release版app时会使用到混淆,所以这个mapping.txt文件一般只是用于release安装包补丁的生成)
    tinkerApplyMappingPath = "${bakPath}/old-app-mapping.txt"
    // 基础包的R.txt文件路径(如果你的安装包中资源文件有改动,则需要使用该R.txt文件来辅助生成补丁包)
    tinkerApplyResourcePath = "${bakPath}/old-app-R.txt"
    //only use for build all flavor, if not, just ignore this field
    tinkerBuildFlavorDirectory = "${bakPath}/flavor"
}

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

def getApplyMappingPath() {
    return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}

def getApplyResourceMappingPath() {
    return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}

def getTinkerIdValue() {
    return hasProperty("TINKER_ID") ? TINKER_ID : android.defaultConfig.versionName
}

def buildWithTinker() {
    return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}

def getTinkerBuildFlavorDirectory() {
    return ext.tinkerBuildFlavorDirectory
}

if (buildWithTinker()) {
    //apply tinker插件
    apply plugin: 'com.tencent.tinker.patch'

    // 全局信息相关的配置项
    tinkerPatch {
        tinkerEnable = buildWithTinker()// 是否打开tinker的功能。
        oldApk = getOldApkPath()        // 基准apk包的路径,必须输入,否则会报错。
        ignoreWarning = false           // 是否忽略有风险的补丁包。这里选择不忽略,当补丁包风险时会中断编译。
        useSign = true                  // 在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,我们是否需要为你签名。
        // 编译相关的配置项
        buildConfig {
            applyMapping = getApplyMappingPath()
            // 可选参数;在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。这个只是推荐设置,不设置applyMapping也不会影响任何的assemble编译。
            applyResourceMapping = getApplyResourceMappingPath()
            // 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常。
            tinkerId = getTinkerIdValue()
            // 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。
            keepDexApply = false
            // 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
            isProtectedApp = false // 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。
            supportHotplugComponent = false // 是否支持新增非export的Activity(1.9.0版本开始才有的新功能)
        }
        // dex相关的配置项
        dex {
            dexMode = "jar"
// 只能是'raw'或者'jar'。 对于'raw'模式,我们将会保持输入dex的格式。对于'jar'模式,我们将会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,但是验证md5时比'raw'模式耗时。默认我们并不会去校验md5,一般情况下选择jar模式即可。
            pattern = ["classes*.dex",
                       "assets/secondary-dex-?.jar"]
            // 需要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如assets/...
            loader = [
                    // 定义哪些类在加载补丁包的时候会用到。这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。
                    // 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
                    // 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。这里需要注意的是,这些类的直接引用类也需要加入到loader中。或者你需要将这个类变成非preverify。
            ]
        }
        // 	lib相关的配置项
        lib {
            pattern = ["lib/*/*.so","src/main/jniLibs/*/*.so"]
            // 需要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...
        }
        // res相关的配置项
        res {
            pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
            // 需要处理res路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...,务必注意的是,只有满足pattern的资源才会放到合成后的资源包。
            ignoreChange = [
                    // 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。
                    "assets/sample_meta.txt"
            ]
            largeModSize = 100
            // 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb
        }
        // 用于生成补丁包中的'package_meta.txt'文件
        packageConfig {
            // configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。
            // 在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。
            // 但是建议直接通过修改代码来实现,例如BuildConfig。
            configField("platform", "all")
            configField("patchVersion", "1.0")
//            configField("patchMessage", "tinker is sample to use")
        }
        // 7zip路径配置项,执行前提是useSign为true
        sevenZip {
            zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
        }
    }
    List<String> flavors = new ArrayList<>();
    project.android.productFlavors.each { flavor ->
        flavors.add(flavor.name)
    }
    boolean hasFlavors = flavors.size() > 0
    def date = new Date().format("MMdd-HH-mm-ss")

    /**
     * bak apk and mapping
     */
    android.applicationVariants.all { variant ->
        /**
         * task type, you want to bak
         */
        def taskName = variant.name

        tasks.all {
            if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {

                it.doLast {
                    copy {
                        def fileNamePrefix = "${project.name}-${variant.baseName}"
                        def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"

                        def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
                        from variant.outputs.first().outputFile
                        into destPath
                        rename { String fileName ->
                            fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
                        }

                        from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
                        }

                        from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
                        into destPath
                        rename { String fileName ->
                            fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
                        }
                    }
                }
            }
        }
    }
    project.afterEvaluate {
        //sample use for build all flavor for one time
        if (hasFlavors) {
            task(tinkerPatchAllFlavorRelease) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"

                    }

                }
            }

            task(tinkerPatchAllFlavorDebug) {
                group = 'tinker'
                def originOldPath = getTinkerBuildFlavorDirectory()
                for (String flavor : flavors) {
                    def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
                    dependsOn tinkerTask
                    def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
                    preAssembleTask.doFirst {
                        String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
                        project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
                        project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
                        project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
                    }

                }
            }
        }
    }
}
Copy the code

A few of these configurations are described here to help you understand the subsequent operations (when tinkerEnabled = true) :

  • App is generated in the main Module (usually named app) /build/bakApk folder.
  • Patches of the generated path: main Module (usually called app)/build/outputs/apk tinkerPatch/debug/patch_signed_7zip apk.
  • The base package name is old-app.apk and is stored in the bakApk folder.
  • The base package mapping.txt and R.txt files are usually used when compiling a release signed APK.
  • To use the mapping. TXT file, rename it old-app-mapping. TXT and put it in the bakApk folder.
  • When using the r.tb file, rename it to old-app-R.tb and place it in the bakApk folder.

The mapping. TXT and R. TB files are described in the configuration. Please go back to the configuration. The above is just the configuration in my project. In fact, all of these can be customized. It is recommended to customize the configuration after making clear the content.

What is the base pack?

The base package is the APK file that is already on the shelf (assuming version 1.0). This makes sense. Before a new version of the App is released (let’s say 2.0), we use Tinker to fix bugs in the 1.0 version of the App, and that’s when we use Tinker to generate a patch file, and the nature of a patch file, The file difference between the App with the Bug fixed and the 1.0 version of the App. Prior to the release of version 2.0, we may produce new patches to fix the 1.0 version of the App on the user’s phone many times, so the patch must be based on the 1.0 version of the App, which means that the App on the user’s phone is the base pack, which is the APK file in the current App market (the 1.0 version mentioned above).

Four, Tinker packaging and expansion

1. Copy files

Copy all files and folders under the Tinker package provided in Demo to your own project.

These files are exactly copied from the official Tinker Demo, with a few more comments.

Briefly explain the role of these files:

  • SampleUncaughtExceptionHandler: Tinker exception capture device.
  • MyLogImp: Tinker’s log output implementation class.
  • SampleLoadReporter: Some callbacks for patch loading.
  • SamplePatchListener: Filters repair and upgrade requests received by Tinker.
  • SamplePatchReporter: Some callbacks to fix or upgrade patches.
  • SampleTinkerReport: Repair results (success, conflict, failure, etc.)
  • SampleResultService: : Patch The patchmaking process returns the patchmaking result to the class of the main process.
  • TinkerManager: TinkerManager (install and initialize Tinker).
  • TinkerUtils: A utility class that extends the ability to condition patches, lock screens, or restart applications in the background.

These are just extensions and packages of Tinker functionality, and are all optional, but these files can be useful for improving the functionality of your project, and I recommend adding them to your own project. If you just want to fix the bug and don’t do a lot of work (such as uploading the patch information to the server, etc.), you don’t need to worry about the purpose of these files, but you can also wrap them yourself.

For a detailed description of these custom classes and error codes, see the official Tinker Wiki: Optional Custom Classes.

2. Add services to the manifest file

One of the files added earlier is the SampleResultService file, which is one of the four components, so it must be declared in the manifest file.

<service
    android:name="com.lqr.tinker.service.SampleResultService"
    android:exported="false"/>
Copy the code

Write a proxy class for Application

According to Tinker, the Application cannot be dynamically fixed, so there are two options:

  1. Use “inherit TinkerApplication + DefaultApplicationLike”.
  2. Use “DefaultLifeCycle annotation + DefaultApplicationLike”.

Of course, you can ignore this if you don’t think your custom Application will use hotfixes; But remember to copy the initTinker() method in the code below to your project to initialize Tinker.

DefaultLifeCycle annotations + TinkerApplicationLike. DefaultLifeCycle annotations generate the Application. Let’s write the Application proxy class:

Write TinkerApplicationLike

Copy the code below into your project. The comments are simple and clear, without much explanation:

@ SuppressWarnings (" unused ") @ DefaultLifeCycle (application = "com. LQR. Tinker. MyApplication", / / application class name. You can only use strings, the MyApplication file doesn't exist, However, you can use (name) flags = ShareConstants.TINKER_ENABLE_ALL,// tinkerFlags loaderClass = on the Application tag of AndroidManifest "Com. Tencent. Tinker. Loader. TinkerLoader", / / loaderClassName, we use the default can be here! //tinkerLoadVerifyFlag public class TinkerApplicationLike extends DefaultApplicationLike { private Application mApplication; private Context mContext; private Tinker mTinker; Public TinkerApplicationLike(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) public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) { getApplication().registerActivityLifecycleCallbacks(callback); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override public void onBaseContextAttached(Context base) { super.onBaseContextAttached(base); mApplication = getApplication(); mContext = getApplication(); initTinker(base); // We can move the previous custom onCreate() method from the Application to here... } private void initTinker(Context base) {// Tinker requires you to enable MultiDex multidex.install (base); TinkerManager.setTinkerApplicationLike(this); / / set the global exception handling TinkerManager. InitFastCrashProtect (); / / open upgrade retry function (before installing the Tinker Settings) TinkerManager. SetUpgradeRetryEnable (true); / / set the Tinker log output class TinkerInstaller. SetLogIml (new MyLogImp ()); / / install Tinker (after finish loading multiDex, otherwise you will need to com. Tencent. The Tinker. * * manually on the main dex) TinkerManager. InstallTinker (this); mTinker = Tinker.with(getApplication()); }}Copy the code

2. Carry the operations in the custom Application

Move the actions in your project in the custom Application to the onCreate() or onBaseContextAttached() methods of TinkerApplicationLike.

public class TinkerApplicationLike extends DefaultApplicationLike { ... @Override public void onCreate() { super.onCreate(); // Move the action performed by the onCreate() method from the previous custom Application to here... } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) @Override public void onBaseContextAttached(Context base) { super.onBaseContextAttached(base); mApplication = getApplication(); mContext = getApplication(); initTinker(base); // Or move here... }}Copy the code

3. Register in the manifest file

Will @ DefaultLifeCycle application in the corresponding value, namely “com. LQR. Tinker. MyApplication”, assigned to manifest the application tag name attribute, as follows:

<application
    android:name="com.lqr.tinker.MyApplication"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    ...
</application>
Copy the code

Note: the name attribute will appear in red because the myApplication. Java file does not exist in the project source code, but don’t worry, because it is dynamically generated, just Build the project, and it doesn’t matter.

For a detailed description of the Application proxy class, see “Tinker Official Wiki: Application Proxy Class”.

Tinker is already integrated here, but only locally. Updates will be released after the server releases the patch packs to the app.

6. Common apis

Now let’s take a look at some of Tinker’s key apis that will be used in your code.

1. Request a patch

TinkerInstaller. OnReceiveUpgradePatch (context, patches of local path);Copy the code

2. Uninstall the patch

Tinker.with(getApplicationContext()).cleanPatch(); Tinker.with(getApplicationContext()). CleanPatchByVersion (version number)// Uninstall the patch of the specified versionCopy the code

3. Kill the other processes of the application

ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
Copy the code

4, Hack to fix so

TinkerLoadLibrary.installNavitveLibraryABI(this, abi);
Copy the code

Abi: INDICATES the CPU architecture type

5. Fix so in non-hack mode

TinkerLoadLibrary. LoadLibraryFromTinker (getApplicationContext (), "lib/" + abi, so library module name); / / load any abi library TinkerLoadLibrary. LoadArmLibrary (getApplicationContext (), so library module name); / / applies only to load armeabi library TinkerLoadLibrary. LoadArmV7Library (getApplicationContext (), so the library module name); // Only applies to loading the Armeabi-v7a libraryCopy the code

LoadArmLibrary () and loadArmV7Library() essentially call loadLibraryFromTinker(), interested can check the source code.

For a detailed description of all of Tinker’s apis, see the Tinker Official Wiki: Tinker-API Overview.

Seven, test,

Since the layout is simple and unimportant, here is a picture of the Demo in action, and the rest is up to imagination.

1. Compile the base package

What’s the use of patches when there’s no base package? So, the first step is to pack an APK.

In Terminal, use the command line./ Gradlew assembleDebug. It doesn’t matter if you don’t know the command line, Android Studio provides us with graphical operations, as shown in the following figure:

Double-click assembleRelease if you want to release signed packages, but you also need to configure the signature file, more on that later.

Once compiled, you can automatically create a bakApk folder in the Build directory, which contains the packaged APK file. Since all subsequent patch packages will be based on this apK, this is the base package file (equivalent to the app market).

If the apk file is release signed and is to be placed on the app market, you must save the apk file with r.txt (and a mapping.txt if there is any confusion), remember.

Now install the tinker-local-DEBUg-1206-11-488-42.apk on your phone.

  1. Click the “Say Someting” button to toast “Hello”.
  2. Click on the “Get String From. so” button to toast” Hello LQR”.
  3. Click the “Show Info” button to display “Patch is not loaded”, indicating that no patch is currently loaded.

1. Fix Java code

Here is the method called when the “Say someting” button is clicked, using Toast to display the Hello string:

public void say(View view) {
    Toast.makeText(getApplicationContext(), "Hello", Toast.LENGTH_SHORT).show();
}
Copy the code

1) Fix the code

Now I want it to toast Hello World, so change the code to:

public void say(View view) {
    Toast.makeText(getApplicationContext(), "Hello World", Toast.LENGTH_SHORT).show();
}
Copy the code

2) Make patch packages

Rename the base package (the previous tinker-local-debug-1206-11-488-42.apk file) to old-app.apk, then double-click tinkerPatchDebug, as shown in the figure below:

After the completion of the compilation, the build/outputs/apk/tinkerPatch will produce three patches, patch_signed_7zip is what we need. The apk.

To build/outputs/apk/tinkerPatch directory file and folder details, please refer to: “Tinker, the official Wiki: output file”.

3) Deliver the patch package

Place Patch_signed_7zip. apk in the SD card directory of the mobile phone:

Is not necessarily the SD card catalog, location is determined by our developers, Demo called TinkerInstaller. OnReceiveUpgradePatch (the context, the local path of patches) method, the second parameter specifies the SD card, so such operation.

4) Patch

You can see that before you install patch, when you click the “Say someting” button, it’s still toast “Hello”. After clicking the “Install Patch” button, it will prompt “Patch success,please restart process”, indicating that Tinker has been patched. Click “Show Info” to see “Patch is not loaded”, indicating that the current patch has not taken effect. Finally, click the “Kill Myself” button to kill the current app (process).

After reopening the app, click the “Say Someting” button again and toast “Hello World”. Click “Show Info” again, you can see “Patch is loaded”, indicating that the patch takes effect after the app restarts.

Summary: Tinker hotfix does not make the patch take effect in real time. The patch will not take effect until the process is restarted. Tinker automatically applies the patch after the app restarts.

2. Fix the SO library

So files are stored in the SRC /main/jniLibs directory. Because the default library directory of Android Studio is libs (which is the same as SRC), this needs to be configured in the build.gradle file of the app. Specify the folder where the SO library resides.

Here is the method called when the “Get String from. so” button is clicked:

public void string_from_so(View view) {
    String string = JniUtil.hello();
    Toast.makeText(getApplicationContext(), string, Toast.LENGTH_SHORT).show();
}
Copy the code

The JniUtil code looks like this:

public class JniUtil {
    public static String LIB_NAME = "LQRJni";
    public JniUtil() {}
    static {
        System.loadLibrary(LIB_NAME);
    }
    public static native String hello();
}
Copy the code

There are two points to note when loading the SO library:

  1. System.loadlibrary (libname) loads the library files in a fixed directory, and system.load (filename) loads the library files in a specified directory.
  2. The libname parameter of system.loadLibrary (libname) refers to the module name of the library, not the name of the so file. For example, the module name of the liblqrjni.so file is actually LQRJni.

So file production code included in the Demo, interested friends can try to make their own.

1) Replace the SO file

Back to the point, now the text I get in the SO library is “Hello LQR”, now change that, I need to get the text is “Hello CSDN_LQR”, just replace the old SO file with the new so file.

2) Check Tinker lib matching rules

In the build.gradle file of the app, we have the following configuration in the third part “Tinker configuration and Tasks” section 2 “Tinker Configuration and Tasks” :

lib {
    pattern = ["lib/*/*.so", "src/main/jniLibs/*/*.so"]
}
Copy the code

This is Tinker’s lib matching rule. During the patch generation process, it will match the libraries that match the rule with the libraries in the base package, and then put the different libraries into the fix package. The Tinker Demo does not have the “SRC /main/jniLibs/*/*. So” section in the official Tinker Demo configuration, which will cause Tinker to not check for changes to files in the SRC /main/jniLibs directory when it generates the fix, so the fix will not be included in the fix. This is important. Remember that.

3) Generate patches and deliver patch packages

The process of generating a patch and delivering a patch package is the same as before, so we won’t repeat it here, but let’s see how tinkerPatch is different from before:

Finally, remember to put Patch_signed_7zip. apk in the SD card directory of the phone.

4) Uninstall the patch

Multiple patches can be installed, and the version number of the patch is used to distinguish them. During the uninstallation, the patch can be uninstalled according to the version number of the patch, or all the previous patches can be uninstalled. In the actual development, it depends on the project requirements to solve which way to uninstall the patch. Here’s the click event for the “Uninstall Patch” button:

public void uninstall_patch(View view) {
    ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
    Tinker.with(getApplicationContext()).cleanPatch();
}
Copy the code

Before you uninstall the patch, kill the other processes of the current App. After the patch is uninstalled, the App is not secure (because Tinker is already initialized), and ideally, you need to restart the App.

5) Patch

Now let’s patch it again, as shown in the figure below:

You can see that when you click the “Get String from.so” button before you install the patch, you still have the “Hello LQR” toast. After clicking the “Install Patch” button, it will prompt “Patch success,please restart process”, indicating that Tinker has been patched. Click “Show Info” to see “Patch is not loaded”, indicating that the current patch has not taken effect. Finally, click the “Kill Myself” button to kill the current app (process).

Now restart the app, and ideally, when we click the “Get String From_so” button again, it will toast “Hello CSDN_LQR”.

However, the toast is still “Hello LQR”, there is no change, and after clicking the “Show Info” button, you can see “Patch is loaded”, which means the patch is loaded. Why is that?

Take a look at the following:

When you restart the app, first click the “Load Library (hack)” button, then click the” Get String From. So “button, and the toast appears as “Hello CSDN_LQR”.

As you can see from the red line, Tinker doesn’t distinguish between abi and abi, and it doesn’t automatically load the so library when the app is launched. It’s up to the developer to decide.

Here is the method called by clicking the “Load Library (hack)” button:

public void load_library_hack(View view) { String CPU_ABI = android.os.Build.CPU_ABI; // Register tinker Library's CPU_ABI architecture so in the system's library path. TinkerLoadLibrary.installNavitveLibraryABI(this, CPU_ABI); }Copy the code

This is Tinker’s Hack for loading the so library in a patch. It’s just a method call, nothing special. As for the non-hack way to load the patch, I have not tested successfully, it is very strange, I can not understand the reason of the problem, the official document is not clear, if there are friends who know the reason for the failure of the Demo loading, please give me some advice, THX.

Summary: Tinker will automatically load the patch after the app restarts, but it will not automatically load the so file in the patch. It is up to the developer to determine the ABI to load the SO file.

3. Restore the resource file

This part is highly compatible with the previous part, so we will not do the demo. You can try to replace the avatar in the demo in the patch pack, which is basically the same as the operation of fixing the Java file. In this part, it is necessary to remind that the Tinker configuration in the app build.gradle file is as follows:

res {
    pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
    ...
}
Copy the code

It’s easy to understand that this is Tinker’s matching rule for resource files, which is sufficient for daily development. If your project puts resource files in a directory that is not available here, you need to modify this part of the configuration.

Eight, the details

1. Packing steps

1) Debug packaging

  1. Calling assembleDebug to compile a DEBUG signed APK (old APK), which is the base APK.
  2. Modify code, update RES file, so, etc.
  3. Rename old APk to the specified name according to gradle parameter rules, again in the bakApk directory (which can be changed).
  4. Call tinkerPatchDebug generated patches in/build/outputs/tinkerPatch/directory (default is patch_signed_7zip. Apk).
  5. Copy the patch package to the SD card directory (the directory can be changed), call the patch method in the program, and restart the APP to achieve hot fix.

2) Release packaging step

  1. Using assembleRelease produces a release signed APK (old APK), which is the base APK, and a mapping file.
  2. Modify code, update RES file, so, etc.
  3. Rename the old apk file and the Mapping file to the specified name according to gradle parameter rules, or put them in the bakApk directory (which can be changed).
  4. Call tinkerPatchRelease generated patches in/build/outputs/tinkerPatch/directory (default is patch_signed_7zip. Apk).
  5. Copy the patch package to the SD card directory (the directory can be changed), call the patch method in the program, and restart the APP to achieve hot fix.

Since tinker’s release packaging requires information about the signature file, you must also configure the signature file in your app’s build.gradle.

android {
    ...
    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'), project.file('proguard-rules.pro')
        }
        debug {
            debuggable true
            minifyEnabled false
            signingConfig signingConfigs.debug
        }
    }
}
Copy the code

In fact, the difference between debug and release packaging is that release packaging uses two more files (mapping.txt, R.txt) than debug packaging, in addition to executing different commands.

2. Notes and findings on using Tinker

  • Instant Run needs to be disabled when Tinker builds.
  • Tinker needs MultiDex.
  • Copy apK, mapping. TXT, and R.t. xt files compiled with assembleRelease before the installation.
  • If multiple patch packages are of the same version, patch installation is not affected. For example, if the first patch version is 1.0 and the second patch version is 1.0, the second patch can be successfully installed.
  • After the patch is installed successfully, the original patch file will be deleted. Therefore, you do not need to worry about the problem of clearing the original patch file in the project.

3. Errors you may encounter

1) onLoadPatchListenerReceiveFail code was 2

Error:

receive a patch file: /storage/emulated/0/patch_signed_7zip.apk, file size:3604
patch loadReporter onLoadPatchListenerReceiveFail: patch receive fail: /storage/emulated/0/patch_signed_7zip.apk, code: -2
Copy the code

If this occurs, perform the following two steps:

  1. Check whether the file path is normal.
  2. Check whether the SD card access permission is added in the manifest file.

If you have an Android7.0 phone, consider the FileProvider (Android7.0 does not support direct access to sd cards).

2) onLoadPatchListenerReceiveFail code for – 24

Error:

receive a patch file: /storage/emulated/0/patch_signed_7zip.apk, file size:3665
get platform:null
patch loadReporter onLoadPatchListenerReceiveFail: patch receive fail: /storage/emulated/0/patch_signed_7zip.apk, code: -24
Copy the code

Tinker can’t get the value of platform. Please check the app build.gradle file for the following configuration, which specifies the platform and version numbers supported by Tinker:

PackageConfig {configField("platform", "all") configField("patchVersion", "1.0")}Copy the code

Nine, other

For the multi-channel package patch file, there is no research yet, please refer to the official Tinker Wiki.

This Demo is based on Tinker official Demo and documents. The following is the link of Tinker official documents:

  • Tinker’s Wiki page
  • Tinker Access Guide
  • Tinker- Custom extension
  • Tinker – an overview of the API
  • Tinker- Frequently asked questions

Finally post the Demo link

Github.com/GitLqr/HotF…

Module description in Demo:

  1. App: Demo of hot repair principles
  2. Tinker-local: integrates tinker hotfix Demo locally
  3. Jnitest: Demo that generates a simple SO file