preface

Hot update for a long time to experience, the recent work is not very busy, spare some time to experience, the feeling is very cool.

Development environment:

  • Android Studio 3.5
  • Com. Aliyun. Ams: alicloud - android - hotfix: 3.2.8

Create a project

In order to get close to the actual development scenario, the basic library framework created by myself was introduced in the project:

The project adopts componentization and the project structure is as follows:

  • App: Shell project, which contains MainActivity and Application.
  • Common: A common component that contains a common dependency library declaration, as well as a base class interface and a component function interface.
  • Launch: The launch component, which contains a SplashActivity.
  • Home: Home page component, including a HomeFragment.
  • User: A user component that contains a UserFragment and a WebActivity. The WebActivity is empty and declares a class that inherits from the ActivityAndroidManifest.xmlIs configured in.

Integrated Sophix

1. Create products and applications

First create products and applications, you can refer to ali Cloud official document: Create products and applications.

2. Integrate the SDK

2.1 Adding a Dependency

In the project’s build.gradle add:

repositories {
    maven {
        url "http://maven.aliyun.com/nexus/content/repositories/releases"}}Copy the code

In the app’s build.gradle add:

dependencies {
    // Alige repair
    implementation 'com. Aliyun. Ams: alicloud - android - hotfix: 3.2.8'
}
Copy the code

2.2 Permission Declaration

Declare the required permissions for Sophix in Androidmanifest.xml:

<! --Network access-->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<! --Read permission for the external storage device, required for the debugging tool to load the local patch-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
Copy the code

The READ_EXTERNAL_STORAGE permission is Dangerous Permissions. It is required only for the debugging tool to obtain external patches and does not affect the loading of online patches. During debugging, obtain the runtime permission for android6.0 or higher by yourself.

2.3 Configuring Account Information

Add the following configuration under the Application node in the middle of androidManifest.xml:

<meta-data
    android:name="com.taobao.android.hotfix.IDSECRET"
    android:value="App ID" />
<meta-data
    android:name="com.taobao.android.hotfix.APPSECRET"
    android:value="App Secret" />
<meta-data
    android:name="com.taobao.android.hotfix.RSASECRET"
    android:value=RSA key "" />
Copy the code

You can also not configure the account information here, because the App ID/App Secret will be used for metering and billing, for security considerations, the official suggestion is to use setSecretMetaData in the code to set the account information, which will be described later.

2.4 Configuration Confusion

# Baseline package used to generate mapping.txt
-printmapping mapping.txt

# hotfix-keep class com.taobao.sophix.**{*; } -keep class com.ta.utdid2.device.**{*; } -dontwarn com.alibaba.sdk.android.utils.**To prevent the inline #
-dontoptimize
Copy the code

2.5 R8 is not used

Add the following configuration to the gradle.properties file for your project:

# hot update confusion requires -applymapping mapping.txt, which R8 does not support very well, so we will not use R8 for now
android.enableR8=false
Copy the code

Normally, the code in the official environment is confused. In order for the hot update to work properly, in the case of a fix pack, when confused, you need to use the mapping file generated by the original package (officially called the baseline package), so that the obfuscation rules in the fix pack are the same as in the original package.

To do this, use -applymapping mapping.txt. On Android Studio 3.5, R8 is already the default confounding tool, but R8 doesn’t support this directive very well, I keep failing to pack it when I try. After checking with official technical support, you chose not to use R8.

2.6 Initializing the SDK

2.6.1 new SophixStubApplication
import android.content.Context;

import androidx.annotation.Keep;
import androidx.multidex.MultiDex;

import com.taobao.sophix.SophixApplication;
import com.taobao.sophix.SophixEntry;
import com.taobao.sophix.SophixManager;

public class SophixStubApplication extends SophixApplication {
    // Sophix account information, if configured in androidmanifest.xml, there is no need to configure this,
    // If the account information is configured in the code and androidmanifest.xml at the same time, the code takes precedence.
    private static final String APP_ID = "";
    private static final String APP_SECRET = "";
    private static final String RSA_SECRET = "";
    // User - defined AES secret key. Symmetric encryption is used for patch packs.
    // The value of this parameter must be a combination of 16 digits or letters, which is exactly the same as the AES Key in the patch tool Settings.
    // The patch is properly decrypted and loaded. At this point, the platform is not aware of the secret key,
    // So don't worry that Aliyun mobile platform will use your patch to do something illegal.
    private static final String AES_KEY = "12djahdaufdaldha";

    private static OnPatchLoadStatusListener onPatchLoadStatusListener = null;

    // here SophixEntry should specify the real Application and ensure that the RealApplicationStub class name is not confused.
    @Keep
    @SophixEntry(Application.class)
    class RealApplicationStub {}@Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);

        // The number of solutions exceeds the 65536 limit. This should be called before initSophix
        // Remove MultiDex from the original Application to avoid repeated calls causing problems.
        MultiDex.install(this);

        initSophix();
    }

    public static void setOnPatchLoadStatusListener(OnPatchLoadStatusListener listener) {
        onPatchLoadStatusListener = listener;
    }

    private void initSophix(a) {
        String appVersion = "0.0.0";
        try {
            appVersion = this.getPackageManager()
                    .getPackageInfo(this.getPackageName(), 0)
                    .versionName;
        } catch (Exception e) {
            e.printStackTrace();
        }

        SophixManager instance = SophixManager.getInstance();
        instance.setContext(this)
                .setAppVersion(appVersion)
                // Set the account information
                .setSecretMetaData(APP_ID, APP_SECRET, RSA_SECRET)
                // Set a custom AES secret key
                .setAesKey(AES_KEY)
                .setEnableDebug(false) 
                .setPatchLoadStatusStub((mode, code, info, handlePatchVersion) -> {
                    if(onPatchLoadStatusListener ! =null) {
                        onPatchLoadStatusListener.onLoad(mode, code, info, handlePatchVersion);
                    }
                })
                .initialize();
    }

    public interface OnPatchLoadStatusListener {
        void onLoad(int mode, int code, String info, int handlePatchVersion); }}Copy the code
2.6.2 configuration SophixStubApplication

Modify the Androidmanifest.xml file to remove the original Application and set it to SophixStubApplication:

<application
        android:name=".application.SophixStubApplication" 
        ./>
Copy the code
2.6.3 Precautions
2.6.3.1 Write SophixStubApplication in Java

In the actual project, I basically use Kotlin development, for SophixStubApplication should be written in Java, refer to the error troubleshooting steps from Hotfix patch tool.

2.6.3.2 Do not write business logic in SophixStubApplication

SophixStubApplication is an entry class for Sophix, designed to initialize Sophix, and should not contain any business logic.

This class must be inherited from SophixApplication; the onCreate method does not need to be implemented.

This class should not have any logic to call each other from other classes in the project and must be completely isolated.

Note that there is no need to initialize Sophix repeatedly in the original Application class, and you need to avoid confusing the original Application class.

2.7 (Optional) Adding a Tag

You can use the setTags interface [added in v3.2.7] to set the label for the patch pull up to support more conditional grayscale distribution. Here is a simple example:

List<String> tags = new ArrayList<>();
tags.add("test");
// This is called before the queryAndLoadNewPatch() methodSophixManager. GetInstance (). SetTags (tags);Copy the code
List<String> tags = new ArrayList<>();
tags.add("production");
// This is called before the queryAndLoadNewPatch() methodSophixManager. GetInstance (). SetTags (tags);Copy the code

In this way, you can test the same patch in the same version without any impact on the two environments. In this way, you can test the same patch in the same version. Tags can be added more than one, the structure is before and after the non-empty string. When you build a patch, use the same tags for the baseline and fix packs.

2.8 Handling the patch loading callback

In SophixStubApplication, instead of writing the patch-load callback processing logic, an interface is created to keep the processing logic out of the loop by means of the observer pattern. The purpose of this is that the hot update does not work until the SophixManager is initialized; code written before the initialization does not work.

In this project, the processing logic is put into the actual Application:

class Application : Application() {

    override fun onCreate(a) {
        super.onCreate()

        SophixStubApplication.setOnPatchLoadStatusListener { mode, code, info, handlePatchVersion ->
            when (code) {
                PatchStatus.CODE_LOAD_SUCCESS -> {
                    LogUtil.i("sophix load patch success!")
                }
                PatchStatus.CODE_LOAD_RELAUNCH -> {
                    // The patch is successfully loaded. You need to restart the App to take effect
                    // My logic here is to kill the App and restart the App after 100 milliseconds
                    LogUtil.i("sophix preload patch success. restart app to make effect.")
                    RestartAppTool.restart(this.100, packageName)
                    SophixManager.getInstance().killProcessSafely()
                }
                else -> {
                    LogUtil.i("sophix load error, code is $code"}}}... }}Copy the code

It is important to note that process.killprocess (process.mypid ()) should not be called directly to kill the Process, as this would disturb the internal state of the Sophix. . So if you need to kill the process, the proposal calls SophixManager getInstance () killProcessSafely () kill process, this method can be in internal do some appropriate processing after the killing of process.

2.9 Added the logic for checking patch Updates

Alisophix is charged by the number of times the queryAndLoadNewPatch method is called, with a certain threshold of free per month:

  • Monthly Active Devices (MAUs): 50,000 Free for 50,000 devices per month, per account.
  • Average daily query times: 20. On average, each application under each account accesses each device for 20 free patch queries a day.
  • Patch distribution: There is no limit on the number of patches and no extra traffic charge.

Be cautious about calling queryAndLoadNewPatch.

Because it is Demo, my processing logic here is very simple, the home page has a button, click on it to call queryAndLoadNewPatch.

override fun initListener(a) {
    super.initListener()
  
    btnCheckUpdate.onClick {
        // Check for updates
        LogUtil.i("Check for Updates")
        SophixManager.getInstance().queryAndLoadNewPatch()
    }
}
Copy the code

Initial pack (baseline pack)

Project Effect:

1. Back up the original package

The app/build/outputs/apk/release the formal package under the backup, renamed the app – release – 0. Apk.

2. Move the maping. TXT

The app/build/outputs/mapping/release under the path of maping. TXT moved to/app directory path,

3. Modify the file

To confuse the original file:

# Baseline package used to generate mapping.txt
-printmapping mapping.txt

# hotfix-keep class com.taobao.sophix.**{*; } -keep class com.ta.utdid2.device.**{*; } -dontwarn com.alibaba.sdk.android.utils.**To prevent the inline #
-dontoptimize
Copy the code

Is adjusted for:

# After the repair of the project to use, ensure that the results of confusion consistent
-applymapping mapping.txt

# hotfix-keep class com.taobao.sophix.**{*; } -keep class com.ta.utdid2.device.**{*; } -dontwarn com.alibaba.sdk.android.utils.**To prevent the inline #
-dontoptimize
Copy the code

-printMapping mapping.txt -printMapping mapping.txt -printMapping mapping.txt -printMapping mapping.txt -printMapping mapping.txt

First hot update – Modify resource

1. Update the code

  1. Change the background image of Splash, but do not change the name of the image. Note: SplashActivity does not set the layout file, but set the background image by setting the theme:

    <activity
        android:name=".SplashActivity"
        android:configChanges="orientation|keyboardHidden|screenSize"
        android:screenOrientation="portrait"
        android:theme="@style/SplashTheme" >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    
    <style name="SplashTheme" parent="AppTheme">
            <item name="android:windowNoTitle">true</item>
            <item name="android:windowFullscreen">true</item>
            <item name="android:windowContentOverlay">@null</item>
            <! -- Startup page Background -->
            <item name="android:windowBackground">@drawable/launch_splash_1</item>
        </style>
    Copy the code
  2. Replace the first image on the front page without changing the name of the image. Note that the image is placed under the drawable file.

    Replace the second image on the front page without changing the name of the image. Note that the image is under assets.

    Change the color of the button text in the layout file.

    Modify the text of the button in the string resource file.

  3. Replace my first image and change the name of the image, noting that the image is placed under the drawable file.

    Replace my second image and change the name of the image, noting that it is under assets.

    Change the color of the button text in the layout file.

    Modify the text of the button in the string resource file.

    Replace my Jump to Baidu button click event in toast copy.

2. Generate patches

2.1 Generating a new package

Run the package command to generate the new package and rename it app-release-1.apk.

2.2 Downloading the Patch Installation Tool

If you do not have the patchtool SophixPatchTool, you can download it by clicking the following link:

  • Click to download the Mac version packaging tool
  • Click to download the Windows version packaging tool
  • Click to download the Linux version packaging tool

2.3 choose package

Run the patching tool and select the package:

2.4 Adjusting Settings

2.5 Adjusting Advanced

Note that if check initialization is checked here, the patching tool will check the code in SophixStubApplication and will report an error that it cannot contain non-system apis. If AndroidX MultiDex is used, the tool will also report errors. You can make sure that there are no non-system apis used in SophixStubApplication without checking initialization. The tool will then identify AndroidX libraries.

For other options, select them as required.

2.6 generate Patch

Click the button GO! To generate a patch.

3. Add a version

Go to mobile R&D platform EMAS -> Product Created -> Mobile Hotfix -> Patch Management, then click add Version button, fill in the version

Note: the version here must be exactly the same as the setAppVersion content in the code. The new console version number is unlimited.

In the code of SophixStubApplication, we choose the versionName of App as the parameter of setAppVersion, so the content filled in here is the versionName of the corresponding version App.

I’m running into a hole here. Before I uploaded my App to the App market, the App market would read the version number in the Apk, so I didn’t have to fill it in manually. I thought it would be the same here. I thought that filling in the version number was for the convenience of users, so I added “V” as the prefix, as shown in the picture below:

As a result, after the patch was released, the App could not detect the new patch and doubted life for a time.

4. Upload the patch

Currently, the maximum patch size supported is 30MB. See from Mobile Hotfix: What is the patch size limit?

5. Local tests

Download the local debugger and install both the debugger and app-release-0.apk on your phone.

So I’m going to open the App, open the call tool, and I’m going to hit Connect App,

Hover the mouse over the hint position:

Click to scan the QR code, install the patch, restart the App, and check whether the App takes effect:

6. Gray scale test

When grayscale is published, you can specify the previously mentioned tag.

Test effect:

7. Official launch

After local test and gray scale test, select full release after confirmation:

8. Hot update results

Change a The results of
Change the background image of Splash. Do not change the name of the image. Note that the image is placed under the drawable file and set by theme Does not take effect
Replace the first image on the front page without changing the name of the image. Note that the image is placed under the drawable file To take effect
Replace the second image on the front page without changing the name of the image. Note that the image is under assets Does not take effect
Change the color of the button text in the layout file To take effect
Modify the text of the button in the string resource file To take effect
Replace my first image and change the name of the image, noting that the image is placed under the drawable file To take effect
Replace my second image and change the name of the image, noting that it is under assets To take effect
Change the color of the button text in the layout file To take effect
Modify the text of the button in the string resource file To take effect
Replace my Jump to Baidu button click event in toast copy To take effect

Second hot update – Use placeholder Activity

1. Update the code

  1. Create a layout file for the empty WebActivity, add routes, receive the route parameter URL, and display the content of the URL with the WebView.

  2. My page jump to Baidu button click, jump to webActivity, carry parameters for Baidu web address.

  3. Change the background image of Splash and change the name of the image.

2. Generate patches and release them

The new package is named app-release-2.apk. Note that the old package is app-release-0.apk instead of app-release-1.apk.

3. Hot update results

Change a The results of
Add empty WebActivity content to display the content of the web page at the specified URL To take effect
My page jump to Baidu button click, jump to webActivity, carry parameters for Baidu web address. To take effect
Change the background image of Splash and change the name of the image Don’t take effect

Third hot update – added So and Style

1. Update the code

  1. Integrate Tencent browsing service TBS (the framework contains so files), and replace the system WebView in WebActivity with TBS WebView.

  2. When you enter the WebActivity, display a Toast to confirm that the TBS integrated Activity is currently displayed.

  3. Add two textViews to my page, using Style for all properties except the location property. The first TextStyle is partly actual values and partly references, and the second TextStyle is all references except android: Gravity.

    <style name="UserTestTextStyle1">
        <item name="android:layout_width">80dp</item>
        <item name="android:layout_height">30dp</item>
        <item name="android:gravity">center</item>
        <item name="android:background">@color/common_red_F41</item>
        <item name="android:textSize" tools:ignore="SpUsage">20dp</item>
        <item name="android:textColor">@color/common_white</item>
        <item name="android:text">@string/user_test_style_1</item>
    </style>
    
    <style name="UserTestTextStyle2">
        <item name="android:layout_width">@dimen/TestText2Width</item>
        <item name="android:layout_height">@dimen/TestText2height</item>
        <item name="android:gravity">center</item>
        <item name="android:background">@color/TestText2Background</item>
        <item name="android:textSize" tools:ignore="SpUsage">@dimen/TestText2TextSize</item>
        <item name="android:textColor">@color/TestText2TextColor</item>
        <item name="android:text">@string/user_test_style_2</item>
    </style>
    
    <dimen name="TestText2Width">80dp</dimen>
    <dimen name="TestText2height">30dp</dimen>
    <color name="TestText2Background">#FF4A1E</color>
    <dimen name="TestText2TextSize">20dp</dimen>
    <color name="TestText2TextColor">#FFFFFF</color>
    Copy the code

2. Generate patches and release them

The new package is named app-release-3.apk. Note that the old package is app-release-0.apk instead of app-release-2.apk.

3. Hot update results

Change a The results of
Integrated Tencent browsing service TBS To take effect
The WebActivity enters with a Toast displayed To take effect
I added TestView to my page, using Style, using actual values in part and references in part To take effect
My page added TestView, using Style, in the Style exceptandroid:gravityUse all references To take effect

Fourth hot update – Modify Style

1. Update the code

Change the style of my page’s two TextViews, the first to change the actual value directly, the second to change the value of the reference content.

<style name="UserTestTextStyle1">
    <item name="android:layout_width">140dp</item>
    <item name="android:layout_height">50dp</item>
    <item name="android:gravity">center</item>
    <item name="android:background">@color/common_green_07</item>
    <item name="android:textSize" tools:ignore="SpUsage">30dp</item>
    <item name="android:textColor">@color/common_red_F41</item>
    <item name="android:text">@string/user_test_style_1_1</item>
</style>

<style name="UserTestTextStyle2">
    <item name="android:layout_width">@dimen/TestText2Width</item>
    <item name="android:layout_height">@dimen/TestText2height</item>
    <item name="android:gravity">center</item>
    <item name="android:background">@color/TestText2Background</item>
    <item name="android:textSize" tools:ignore="SpUsage">@dimen/TestText2TextSize</item>
    <item name="android:textColor">@color/TestText2TextColor</item>
    <item name="android:text">@string/user_test_style_2</item>
</style>

<dimen name="TestText2Width">140dp</dimen>
<dimen name="TestText2height">50dp</dimen>
<color name="TestText2Background">#07C27F</color>
<dimen name="TestText2TextSize">30dp</dimen>
<color name="TestText2TextColor">#FF4A1E</color>
Copy the code

2. Generate patches and release them

The new package is named app-release-4.apk. Note that the old package is app-release-0.apk instead of app-release-3.apk.

3. Hot update results

Change a The results of
Style Changes the actual value directly To take effect
Style Modifies the value of the reference To take effect

Fifth Hot Update – Change theme

1. Update the code

Change the startup page theme, cancel full screen, and change the status bar color to red.

<style name="SplashTheme" parent="AppTheme">
    <item name="android:windowNoTitle">false</item>
    <item name="android:windowFullscreen">false</item>
    <item name="colorPrimaryDark">@color/common_red_F41</item>
    <item name="android:windowContentOverlay">@null</item>
    <! -- Startup page Background -->
    <item name="android:windowBackground">@drawable/launch_splash_2</item>
</style>
Copy the code

2. Generate patches and release them

The new package is named app-release-5.apk. Note that the old package is app-release-0.apk instead of app-release-4.apk.

3. Hot update results

Change a The results of
Style Changes the actual value directly Don’t take effect

conclusion

After a few simple rounds of testing, hot updates can do the following:

  1. Support for code tuning of Application (not SophixStubApplication);
  2. Support confusion;
  3. Support for code tweaks;
  4. Support ARouter, at first I was a little worried about not supporting ARouter.
  5. Supports changes to resources in res other than topics.
  6. Support to update pictures in assest, but must be new pictures, that is, the name of the picture must be changed, can not use the original picture name.

There are also some things we can’t do:

  1. Modify not supportedAndroidManifest.xml, so new activities, new permissions, etc., will not take effect. There is also a solution to this problem, which can be solved by pre-configuring an empty Activity placeholder.
  2. It is not supported to change the theme of the Activity, even if the background image of the Activity theme is changed, whether the image is new or the original image name is retained and only the content is replaced.
  3. Do not support updating the image content in assest, keep the original image name.

As for the officially advertised application scenarios: online bug emergency fixes and quick lightweight version upgrades, I think they meet my expectations.