Android 热更新集成

安卓热更新技术方案

方案集成之前的预设问题:
  如果考虑付费,推荐选择阿里的Sophix,Sophix是综合优化的产物,功能完善、开发简单透明、提供分发及监控管理。
  如果不考虑付费,只需支持方法级别的Bug修复,不支持资源及so,推荐使用Robust。
  如果不考虑付费,但要考虑需要同时支持资源及so,推荐使用Tinker。

正常开发流程:
新版本上线,发现问题或用户反馈bug,紧急修复,上线版本,用户重新安装,如下图:
图一
热修复流程:
新版本上线,发现问题或用户反馈,紧急修复,上线补丁,自动修复,如下图:
图二

方案对比分析:

方案 Tinker Robust Sophix
类替换 yes no yes
so替换 yes no yes
资源替换 yes no yes
全平台支持 yes yes yes
即时生效 no yes yes
补丁包大小 较小 一般 较小
复杂度 一般 复杂 傻瓜式接入
成功率 较高 非常高 非常高
收费 no no yes

Sophix集成实现:

流程

1.添加工程依赖

gradle远程仓库依赖, 打开项目找到app的build.gradle文件,添加如下配置:添加maven仓库地址:

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

添加gradle坐标版本依赖:

android {
     
    ......
    defaultConfig {
     
        applicationId "com.xxx.xxx" //包名
        ......
        ndk {
     
            //选择要添加的对应cpu类型的.so库。
            //热修复支持五种
            abiFilters 'arm64-v8a', 'armeabi', 'armeabi-v7a', 'x86', 'x86_64'
        }
        ......
    }
    ......
}
dependencies {
     
    ......
        compile 'com.aliyun.ams:alicloud-android-hotfix:3.2.18'
    ......
}

2.添加应用权限:

Sophix SDK使用到以下权限,使用maven依赖或者aar依赖可以不用配置。具体配置在AndroidManifest.xml中。

<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" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

3.配置AndroidManifest文件

在AndroidManifest.xml中间的application节点下添加如下配置:

<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密钥" />

4. 混淆配置

#基线包使用,生成mapping.txt
-printmapping mapping.txt
#生成的mapping.txt在app/build/outputs/mapping/release路径下,移动到/app路径下
#修复后的项目使用,保证混淆结果一致
#-applymapping mapping.txt
#hotfix
-keep class com.taobao.sophix.**{
     *;}
-keep class com.ta.utdid2.device.**{
     *;}
#防止inline
-dontoptimize

5.初始化

初始化的调用应该尽可能的早,必须在Application.attachBaseContext()的最开始(在super.attachBaseContext之后,如果有Multidex,也需要在Multidex.install之后)进行SDK初始化操作,初始化之前不能用到其他自定义类,否则极有可能导致崩溃。而查询服务器是否有可用补丁的操作可以在后面的任意地方。不建议在Application.onCreate()中初始化,因为如果带有ContentProvider,就会使得Sophix初始化时机太迟从而引发问题。
Sophix最新版本引入了新的初始化方式。
原来的初始化方式仍然可以使用。只是新方式可以提供更全面的功能修复支持,将会带来以下优点:
初始化与应用原先业务代码完全隔离,使得原先真正的Application可以修复,并且减少了补丁预加载时间等等。
新方式能够更完美地兼容Android 8.0以后版本。

具体而言,是需要用户自行加入以下这个类:

package com.my.pkg;
import android.app.Application;
import android.content.Context;
import android.support.annotation.Keep;
import android.util.Log;
import com.taobao.sophix.PatchStatus;
import com.taobao.sophix.SophixApplication;
import com.taobao.sophix.SophixEntry;
import com.taobao.sophix.SophixManager;
import com.taobao.sophix.listener.PatchLoadStatusListener;
import com.my.pkg.MyRealApplication;
/**
 * Sophix入口类,专门用于初始化Sophix,不应包含任何业务逻辑。
 * 此类必须继承自SophixApplication,onCreate方法不需要实现。
 * 此类不应与项目中的其他类有任何互相调用的逻辑,必须完全做到隔离。
 * AndroidManifest中设置application为此类,而SophixEntry中设为原先Application类。
 * 注意原先Application里不需要再重复初始化Sophix,并且需要避免混淆原先Application类。
 * 如有其它自定义改造,请咨询官方后妥善处理。
 */
public class SophixStubApplication extends SophixApplication {
     
    private final String TAG = "SophixStubApplication";
    // 此处SophixEntry应指定真正的Application,并且保证RealApplicationStub类名不被混淆。
    @Keep
    @SophixEntry(MyRealApplication.class)
    static class RealApplicationStub {
     }
    @Override
    protected void attachBaseContext(Context base) {
     
        super.attachBaseContext(base);
//         如果需要使用MultiDex,需要在此处调用。
//         MultiDex.install(this);
        initSophix();
    }
    private void initSophix() {
     
        String appVersion = "0.0.0";
        try {
     
            appVersion = this.getPackageManager()
                             .getPackageInfo(this.getPackageName(), 0)
                             .versionName;
        } catch (Exception e) {
     
        }
        final SophixManager instance = SophixManager.getInstance();
        instance.setContext(this)
                .setAppVersion(appVersion)
                .setSecretMetaData(null, null, null)
                .setEnableDebug(true)
                .setEnableFullLog()
                .setPatchLoadStatusStub(new PatchLoadStatusListener() {
     
                    @Override
                    public void onLoad(final int mode, final int code, final String info, final int handlePatchVersion) {
     
                        if (code == PatchStatus.CODE_LOAD_SUCCESS) {
     
                            Log.i(TAG, "sophix load patch success!");
                        } else if (code == PatchStatus.CODE_LOAD_RELAUNCH) {
     
                            // 如果需要在后台重启,建议此处用SharePreference保存状态。
                            Log.i(TAG, "sophix preload patch success. restart app to make effect.");
                        }
                    }
                }).initialize();
    }
}
// queryAndLoadNewPatch不可放在attachBaseContext 中,否则无网络权限,建议放在后面任意时刻,如onCreate中
SophixManager.getInstance().queryAndLoadNewPatch();

这其中,关键一点是:

    @Keep
    @SophixEntry(MyRealApplication.class)
    static class RealApplicationStub {
     }

SophixEntry应指定项目中原先真正的Application(原项目里application的android::name指定的),这里用MyRealApplication指代。并且保证RealApplicationStub类名不被混淆。而SophixStubApplication的类名和包名可以自行取名。
这里的Keep是android.support包中的类,目的是为了防止这个内部静态类的类名被混淆,因为sophix内部会反射获取这个类的SophixEntry。如果项目中没有依赖android.support的话,就需要在progurad里面手动指定RealApplicationStub不被混淆。

在proguard文件里面需要加上下面内容:

-keepclassmembers class com.my.pkg.MyRealApplication {
     
    public <init>();
}
-keep class com.my.pkg.SophixStubApplication$RealApplicationStub

目的是防止真正Application的构造方法被proguard混淆。
最后,需要把AndroidManifest里面的application改为这个新增的SophixStubApplication类:

    <application
        android:name="com.my.pkg.SophixStubApplication"
        ... .../>
        ... ...
        ... ...
        ... ...

Bugly热更新集成实现

1.添加插件依赖

工程根目录下“build.gradle”文件中添加:

buildscript {
     
    repositories {
     
        jcenter()
    }
    dependencies {
     
        // tinkersupport插件, 其中lastest.release指拉取最新版本,也可以指定明确版本号,例如1.0.4
        classpath "com.tencent.bugly:tinker-support:1.1.5"
    }
}

注意:自tinkersupport 1.0.3版本起无需再配tinker插件的classpath。

版本对应关系:
tinker-support 1.1.3 对应 tinker 1.9.8
tinker-support 1.1.2 对应 tinker 1.9.6
tinker-support 1.1.1 对应 tinker 1.9.1
tinker-support 1.0.9 对应 tinker 1.9.0
tinker-support 1.0.8 对应 tinker 1.7.11
tinker-support 1.0.7 对应 tinker 1.7.9
tinker-support 1.0.4 对应 tinker 1.7.7
tinker-support 1.0.3 对应 tinker 1.7.6
tinker-support 1.0.2 对应 tinker 1.7.5(需配置tinker插件的classpath)

2.集成SDK

gradle配置
在app module的“build.gradle”文件中添加(示例配置):

android {
     
        defaultConfig {
     
          ndk {
     
            //设置支持的SO库架构
            abiFilters 'armeabi' //, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
          }
        }
      }
      dependencies {
     
          compile "com.android.support:multidex:1.0.1" // 多dex配置
          //注释掉原有bugly的仓库
          //compile 'com.tencent.bugly:crashreport:latest.release'//其中latest.release指代最新版本号,也可以指定明确的版本号,例如1.3.4
          compile 'com.tencent.bugly:crashreport_upgrade:1.3.6'
          // 指定tinker依赖版本(注:应用升级1.3.5版本起,不再内置tinker)
          compile 'com.tencent.tinker:tinker-android-lib:1.9.9'
          compile 'com.tencent.bugly:nativecrashreport:latest.release' //其中latest.release指代最新版本号,也可以指定明确的版本号,例如2.2.0
      }

后续更新升级SDK时,只需变更配置脚本中的版本号即可。

注意: 升级SDK已经集成crash上报功能,已经集成Bugly的用户需要注释掉原来Bugly的jcenter库; 已经配置过符号表的Bugly用户保留原有符号表配置; Bugly SDK(2.1.5及以上版本)已经将Java Crash和Native Crash捕获功能分开,如果想使用NDK库,需要配置: compile ‘com.tencent.bugly:nativecrashreport:latest.release’

在app module的“build.gradle”文件中添加:

// 依赖插件脚本
apply from: 'tinker-support.gradle'

tinker-support.gradle内容如下所示(示例配置):

注:您需要在同级目录下创建tinker-support.gradle这个文件哦。

apply plugin: 'com.tencent.bugly.tinker-support'

def bakPath = file("${buildDir}/bakApk/")

/**
 * 此处填写每次构建生成的基准包目录
 */
def baseApkDir = "app-0208-15-10-00"

/**
 * 对于插件各参数的详细解析请参考
 */
tinkerSupport {
     

    // 开启tinker-support插件,默认值true
    enable = true

    // 指定归档目录,默认值当前module的子目录tinker
    autoBackupApkDir = "${bakPath}"

    // 是否启用覆盖tinkerPatch配置功能,默认值false
    // 开启后tinkerPatch配置不生效,即无需添加tinkerPatch
    overrideTinkerPatchConfiguration = true

    // 编译补丁包时,必需指定基线版本的apk,默认值为空
    // 如果为空,则表示不是进行补丁包的编译
    // @{link tinkerPatch.oldApk }
    baseApk = "${bakPath}/${baseApkDir}/app-release.apk"

    // 对应tinker插件applyMapping
    baseApkProguardMapping = "${bakPath}/${baseApkDir}/app-release-mapping.txt"

    // 对应tinker插件applyResourceMapping
    baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-release-R.txt"

    // 构建基准包和补丁包都要指定不同的tinkerId,并且必须保证唯一性
    tinkerId = "base-1.0.1"

    // 构建多渠道补丁时使用
    // buildAllFlavorsDir = "${bakPath}/${baseApkDir}"

    // 是否启用加固模式,默认为false.(tinker-spport 1.0.7起支持)
    // isProtectedApp = true

    // 是否开启反射Application模式
    enableProxyApplication = false

    // 是否支持新增非export的Activity(注意:设置为true才能修改AndroidManifest文件)
    supportHotplugComponent = true

}

/**
 * 一般来说,我们无需对下面的参数做任何的修改
 * 对于各参数的详细介绍请参考:
 * https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
 */
tinkerPatch {
     
    //oldApk ="${bakPath}/${appName}/app-release.apk"
    ignoreWarning = false
    useSign = true
    dex {
     
        dexMode = "jar"
        pattern = ["classes*.dex"]
        loader = []
    }
    lib {
     
        pattern = ["lib/*/*.so"]
    }

    res {
     
        pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
        ignoreChange = []
        largeModSize = 100
    }

    packageConfig {
     
    }
    sevenZip {
     
        zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
//        path = "/usr/local/bin/7za"
    }
    buildConfig {
     
        keepDexApply = false
        //tinkerId = "1.0.1-base"
        //applyMapping = "${bakPath}/${appName}/app-release-mapping.txt" //  可选,设置mapping文件,建议保持旧apk的proguard混淆方式
        //applyResourceMapping = "${bakPath}/${appName}/app-release-R.txt" // 可选,设置R.txt文件,通过旧apk文件保持ResId的分配
    }
}


3.初始化SDK

enableProxyApplication = false 的情况

这是Tinker推荐的接入方式,一定程度上会增加接入成本,但具有更好的兼容性。

集成Bugly升级SDK之后,我们需要按照以下方式自定义ApplicationLike来实现Application的代码(以下是示例):

自定义Application
public class SampleApplication extends TinkerApplication {
     
    public SampleApplication() {
     
        super(ShareConstants.TINKER_ENABLE_ALL, "xxx.xxx.SampleApplicationLike",
                "com.tencent.tinker.loader.TinkerLoader", false);
    }
}

注意:这个类集成TinkerApplication类,这里面不做任何操作,所有Application的代码都会放到ApplicationLike继承类当中
参数解析
参数1:tinkerFlags 表示Tinker支持的类型 dex only、library only or all suuport,default: TINKER_ENABLE_ALL
参数2:delegateClassName Application代理类 这里填写你自定义的ApplicationLike
参数3:loaderClassName Tinker的加载器,使用默认即可
参数4:tinkerLoadVerifyFlag 加载dex或者lib是否验证md5,默认为false

将以前的Applicaton配置为继承TinkerApplication的类:
Android 热更新集成_第1张图片

自定义ApplicationLike
public class SampleApplicationLike extends DefaultApplicationLike {
     

    public 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);
    }


    @Override
    public void onCreate() {
     
        super.onCreate();
        // 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId
        // 调试时,将第三个参数改为true
        Bugly.init(getApplication(), "900029763", false);
    }


    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    @Override
    public void onBaseContextAttached(Context base) {
     
        super.onBaseContextAttached(base);
        // you must install multiDex whatever tinker is installed!
        MultiDex.install(base);

        // 安装tinker
        // TinkerManager.installTinker(this); 替换成下面Bugly提供的方法
        Beta.installTinker(this);
    }

    @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
    public void registerActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks) {
     
        getApplication().registerActivityLifecycleCallbacks(callbacks);
    }

}

注意:tinker需要你开启MultiDex,你需要在dependencies中进行配置compile "com.android.support:multidex:1.0.1"才可以使用MultiDex.install方法; SampleApplicationLike这个类是Application的代理类,以前所有在Application的实现必须要全部拷贝到这里,在onCreate方法调用SDK的初始化方法,在onBaseContextAttached中调用Beta.installTinker(this);。

enableProxyApplication = true 的情况

注:无须你改造Application,主要是为了降低接入成本,我们插件会动态替换AndroidMinifest文件中的Application为我们定义好用于反射真实Application的类(需要您接入SDK 1.2.2版本 和 插件版本 1.0.3以上)。

public class MyApplication extends Application {
     

    @Override
    public void onCreate() {
     
        super.onCreate();
        // 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId
        // 调试时,将第三个参数改为true
        Bugly.init(this, "900029763", false);
    }

    @Override
    protected void attachBaseContext(Context base) {
     
        super.attachBaseContext(base);
        // you must install multiDex whatever tinker is installed!
        MultiDex.install(base);


        // 安装tinker
        Beta.installTinker();
    }

}

4. AndroidManifest.xml配置

在AndroidMainfest.xml中进行以下配置:

1. 权限配置
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<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" />
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
2. Activity配置
<activity
    android:name="com.tencent.bugly.beta.ui.BetaActivity"
    android:configChanges="keyboardHidden|orientation|screenSize|locale"
    android:theme="@android:style/Theme.Translucent" />
3. 配置FileProvider

注意:如果您想兼容Android N或者以上的设备,必须要在AndroidManifest.xml文件中配置FileProvider来访问共享路径的文件。

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="${applicationId}.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths"/>
</provider>

如果使用的第三方库也配置了同样的FileProvider, 可以通过继承FileProvider类来解决合并冲突的问题,示例如下:

<provider
    android:name=".utils.BuglyFileProvider"
    android:authorities="${applicationId}.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true"
    tools:replace="name,authorities,exported,grantUriPermissions">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths"
        tools:replace="name,resource"/>
</provider>

FileProvider类是在support-v4包中的,检查你的工程是否引入该类库。

在res目录新建xml文件夹,创建provider_paths.xml文件如下:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- /storage/emulated/0/Download/${
     applicationId}/.beta/apk-->
    <external-path name="beta_external_path" path="Download/"/>
    <!--/storage/emulated/0/Android/data/${
     applicationId}/files/apk/-->
    <external-path name="beta_external_files_path" path="Android/data/"/>
</paths>

注意:这里配置的两个外部存储路径是升级SDK下载的文件可能存在的路径,一定要按照上面格式配置,不然可能会出现错误。
1.3.1及以上版本,可以不用进行以上配置,aar已经在AndroidManifest配置了,并且包含了对应的资源文件。

5.混淆配置

为了避免混淆SDK,在Proguard混淆文件中增加以下配置:

-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{
     *;}
# tinker混淆规则
-dontwarn com.tencent.tinker.**
-keep class com.tencent.tinker.** {
      *; }

如果使用了support-v4包,还需要配置以下混淆规则:


-keep class android.support.**{
     *;}

robust集成

1.添加插件依赖

1.在App的build.gradle,加入如下依赖:

apply plugin: 'com.android.application'
//制作补丁时将这个打开,auto-patch-plugin紧跟着com.android.application
//apply plugin: 'auto-patch-plugin'
apply plugin: 'robust'

compile 'com.meituan.robust:robust:0.4.99'
	

2.在整个项目的build.gradle加入classpath:

buildscript {
     
    repositories {
     
        jcenter()
    }
    dependencies {
     
         classpath 'com.meituan.robust:gradle-plugin:0.4.99'
         classpath 'com.meituan.robust:auto-patch-plugin:0.4.99'
   }
}

3.在工程项目的src同级目录下配置robust.xml文件

注意 gradle 3.6及以上版本默认启用R8,会将插入的ChangeQuickRedirect变量优化掉,需要在混淆文件proguard-rules.pro中加入以下代码:
-keepclassmembers class **{ public static com.meituan.robust.ChangeQuickRedirect *; }

Robust补丁自动化,为Robust自动生成补丁

使用者只需要提交修改完bug后的代码,运行和线上apk打包同样的gradle命令即可,会在项目的app/build/outputs/robust目录下生成补丁。

自动化补丁工具
gradle命令:./gradlew clean assembleRelease --stacktrace --no-daemon

2.修复使用

1.使用插件时,需要把auto-patch-plugin放置在com.android.application插件之后,其余插件之前。

apply plugin: 'com.android.application'
apply plugin: 'auto-patch-plugin'

2.将保存下来的mapping文件和methodsMap.robust文件放在app/robust/文件夹下。
3.修改代码,在改动的方法上面添加@Modify注解,对于Lambda表达式请在修改的方法里面调用RobustModify.modify()方法

@Modify
    protected void onCreate(Bundle savedInstanceState) {
     
        super.onCreate(savedInstanceState);
     }
     //或者是被修改的方法里面调用RobustModify.modify()方法
     protected void onCreate(Bundle savedInstanceState) {
     
        RobustModify.modify()
        super.onCreate(savedInstanceState);
     }
     

新增的方法和字段使用@Add注解

   //增加方法
    @Add
    public String getString() {
     
        return "Robust";
    }
    //增加类
    @Add
    public class NewAddCLass {
     
        public static String get() {
     
           return "robust";
         }
    }

4.运行和生成线上apk同样的命令,即可生成补丁,补丁目录app/build/outputs/robust/patch.jar
5.补丁制作成功后会停止构建apk,出现类似于如下的提示,表示补丁生成成功
Android 热更新集成_第2张图片

3.样例使用

  1. 生成样例apk,执行gradle命令:

    ./gradlew clean assembleRelease --stacktrace --no-daemon

  2. 安装样例apk。保存mapping.txt文件以及app/build/outputs/robust/methodsMap.robust文件

  3. 修改代码之后,加上**@Modify**注解或者调用RobustModify.modify()方法

  4. 把保存的mapping.txt和methodsMap.robust放到app/robust目录下

  5. 执行与生成样式apk相同的gradle命令:

    ./gradlew clean assembleRelease --stacktrace --no-daemon

  6. 补丁制作成功后会停止构建apk,出现类似于如下的提示,表示补丁生成成功
    Android 热更新集成_第3张图片

  7. 将补丁文件copy到手机目录/sdcard/robust下

    adb push ~/Desktop/code/robust/app/build/outputs/robust/patch.jar /sdcard/robust/patch.jar

    补丁的路径/sdcard/robust是PatchManipulateImp中指定的

4. 注意事项:

1.内部类的构造方法是private(private会生成一个匿名的构造函数)时,需要在制作补丁过程中手动修改构造方法的访问域为public
2.对于方法的返回值是this的情况现在支持不好,比如builder模式,但在制作补丁代码时,可以通过如下方式来解决,增加一个类来包装一下(如下面的B类),
method a(){ return this; }
改为
method a(){ return new B().setThis(this).getThis(); }
3. 字段增加能力内测中,不过暂时可以通过增加新类,把字段放到新类中的方式来实现字段增加能力
4.新增的类支持包括静态内部类和非内部类
5.对于只有字段访问的函数无法直接修复,可通过调用处间接修复

总结:

应用层面来说,阿里系的Sophix方案最优,但是该方案收费,美团的Robust不支持资源修改,所以有了很大限制,而且该技术官方已经很久不维护,并且集成对公司项目改动比较大,所以综合来说性价比最高的方案是腾讯系的Tinker,针对Tinker热更新,腾讯有Bugly针对tinker的集成,补丁文件有一站式的管理,而且还可以独立出Bugly,由公司自己的服务器去维护补丁文件的管理,更加灵活。

你可能感兴趣的:(安卓,android,安卓,热更新,tinker,robust)