Bugly热修复集成

1.简述

Bugly采用Tinker作为热修复的解决方案,Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码,So库以及资源,让应用能够不需要重新安装的情况下实现更新。Tinker不能让补丁实时生效,它必须在打上补丁后重启App。在开始集成之前,我们有必要了解清楚,Tinker有哪些不足:
由于原理与系统限制,Tinker有以下已知问题:

  • Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件(1.9.0支持新增非export的Activity);
  • 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
  • 在Android N上,补丁对应用启动时间有轻微的影响;
  • 不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed";
  • 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。

2.添加插件依赖

项目的build.gradle:

buildscript {
    ext.kotlin_version = '1.3.30'
    repositories {
        google()
        jcenter()
        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'  //3.6.3不支持
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        // tinkersupport插件, 其中lastest.release指拉取最新版本,也可以指定明确版本号,例如1.0.4
        // 注意:自tinkersupport 1.0.3版本起无需再配tinker插件的classpath。
        classpath "com.tencent.bugly:tinker-support:1.1.5"

        // 多渠道插件(多渠道打包推荐使用)
        classpath 'com.meituan.android.walle:plugin:1.1.6'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

3.集成SDK

app的build.gradle:

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"

    defaultConfig {
        applicationId "com.geespace.hotfix"
        minSdkVersion 23
        targetSdkVersion 26
        versionCode 1
        versionName "1.1"

        // 开启multidex
        multiDexEnabled true

        ndk {
            //设置支持的SO库架构
            abiFilters 'armeabi-v7a' //, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
        }

    }

    dexOptions {
        // 支持大工程模式
        jumboMode = true
    }


    // 签名配置
    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")
        }
    }


    lintOptions {
        checkReleaseBuilds false
        abortOnError false
    }

    // 构建类型
    buildTypes {
        release {
            minifyEnabled false
            signingConfig signingConfigs.release
            multiDexKeepProguard file('multidex-config.pro')
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }

        debug {
            debuggable true
            minifyEnabled false
            signingConfig signingConfigs.debug
        }
    }

    // 多渠道配置
//    flavorDimensions "default"
//
//    productFlavors {
//        xiaomi {
//
//        }
//        yyb {
//
//        }
//        wdj {
//
//        }
//    }
}


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.3.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.multidex:multidex:2.0.1'
    //注释掉原有bugly的仓库
    //compile 'com.tencent.bugly:crashreport:latest.release'//其中latest.release指代最新版本号,也可以指定明确的版本号,例如1.3.4
    implementation 'com.tencent.bugly:crashreport_upgrade:latest.release'
    // 指定tinker依赖版本(注:应用升级1.3.5版本起,不再内置tinker)
    implementation 'com.tencent.tinker:tinker-android-lib:latest.release'
    //implementation 'com.tencent.bugly:nativecrashreport:latest.release' //其中latest.release指代最新版本号,也可以指定明确的版本号,例如2.2.0

    // walle 瓦力(多渠道使用)
    implementation 'com.meituan.android.walle:library:1.1.6'

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

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

// 多渠道使用walle示例(注:多渠道使用)
apply from: 'multiple-channel.gradle'


4.配置Tinker

在app的build.gradle文件同级目录下创建一个tinker-support.gradle文件.内容如下:

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

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

/**
 * 此处填写每次构建生成的基准包目录
 */
def baseApkDir = "app-0611-15-14-38"
//def myTinkerId="1.1-base"
def myTinkerId="1.1-patch"

/**
 * 对于插件各参数的详细解析请参考 如果没有特殊需求下面的参数都可以不用更改;
 * 如果apk需要加固等可以参考具体描述设置参数
 */
tinkerSupport {

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

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

    //建议设置true,用户就不用再自己管理tinkerId的命名,插件会为每一次构建的base包自动生成唯一的tinkerId,
    // 默认命名规则是versionname.versioncode_时间戳
    //具体参考https://github.com/BuglyDevTeam/Bugly-Android-Demo/wiki/Tinker-ID%E8%AF%A5%E6%80%8E%E4%B9%88%E8%AE%BE%E7%BD%AE
   // autoGenerateTinkerId = true

    // 构建基准包和补丁包都要指定不同的tinkerId,并且必须保证唯一性
    // 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。
    // 这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等
    //tinkerId = "1.1-base"
    tinkerId = "${myTinkerId}"

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

    // 编译补丁包时,必需指定基线版本的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"


    // 构建多渠道补丁时使用
     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}/${baseApkDir}/app-release.apk"
    ignoreWarning = false
    useSign = true
    dex {
        dexMode = "jar"
        pattern = ["classes*.dex"]
        loader = []
    }
    lib {
        pattern = ["lib/*/*.so", "src/main/jniLibs/*/*.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 = "${myTinkerId}"
        //applyMapping = "${bakPath}/${appName}/app-release-mapping.txt" //  可选,设置mapping文件,建议保持旧apk的proguard混淆方式
        applyResourceMapping = "${bakPath}/${baseApkDir}/app-release-R.txt" // 可选,设置R.txt文件,通过旧apk文件保持ResId的分配
    }
}
1.overrideTinkerPatchConfiguration

当overrideTinkerPatchConfiguration=true时,tinkerPatch可以省略不写,Bugly会加载默认的Tinker配置,但请注意,如果你的so文件不是存放在libs目录下(与src目录同级),又或者资源文件的存放在你自定义的目录中,那么这时候你要小心了,这些文件在制作补丁时不会被检测,也就是说这些so文件和资源文件将不会被热修复,这种情况下就需要将
overrideTinkerPatchConfiguration=false,并设置tinkerPatch的lib和res属性。

2.baseApkDir

baseApkDir时基准包(也称基线包)的目录,在生成补丁时需要根据基准包在bakApk下具体文件夹名字修改。


image.png
3.tinkerId

tinkerId时Bugly热修复方案最重要的一个因素,一般来说我们可以使用git版本号、versionName等等,它会将补丁包与基准包产生对应关系,假设基准包的tinkerId为1.1-base,则生成的补丁包中的YAPATCH.MF文件关系如下:

image.png

对于同一个基准包,可以发布多个补丁包。
Q:如果我打了多个补丁包,比如先上传了补丁A已经下发到了设备中并且修复,如果我又打了一个补丁,这种情况会怎样?
A:如果你的基于基线版本打了多个补丁包,并且上传了多个,我们会以你最后上传的补丁为准,就是说后面的补丁会覆盖前面的补丁

5.初始化SDK

Bugly的初始化工作需要在Applicaiton中完成,但是对原生Tinker来说,默认的Applicaiton是无法完成热修复的,Tinker针对Applicaiton无法热修复的问题,给予了两个选择:

  • enableProxyApplication = false的情况

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

package com.geespace.hotfix.application;

import com.tencent.tinker.loader.app.TinkerApplication;
import com.tencent.tinker.loader.shareutil.ShareConstants;

/**
 * Created by maozonghong
 * on 2020/6/10
 */
public class SampleApplication extends TinkerApplication {
    public SampleApplication() {
        super(ShareConstants.TINKER_ENABLE_ALL,
                "com.geespace.hotfix.application.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
}
package com.geespace.hotfix.application;

import android.annotation.TargetApi;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
import android.widget.Toast;

import androidx.multidex.MultiDex;

import com.meituan.android.walle.WalleChannelReader;
import com.tencent.bugly.Bugly;
import com.tencent.bugly.beta.Beta;
import com.tencent.bugly.beta.interfaces.BetaPatchListener;
import com.tencent.tinker.entry.DefaultApplicationLike;

import java.util.Locale;

/**
 * Created by maozonghong
 * on 2020/6/10
 */

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

public class SampleApplicationLike extends DefaultApplicationLike{

    public static final String TAG = "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();

        Beta.autoCheckUpgrade=true;
        // 设置是否开启热更新能力,默认为true
        Beta.enableHotfix = true;
        // 设置是否自动下载补丁,默认为true
        Beta.canAutoDownloadPatch = true;
        // 设置是否自动合成补丁,默认为true
        Beta.canAutoPatch = true;
        // 设置是否提示用户重启,默认为false
        Beta.canNotifyUserRestart = true;
        // 补丁回调接口
        Beta.betaPatchListener = new BetaPatchListener() {
            @Override
            public void onPatchReceived(String patchFile) {
                Log.e(TAG,"onPatchReceived:"+patchFile);
                Toast.makeText(getApplication(), "补丁下载地址" + patchFile, Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onDownloadReceived(long savedLength, long totalLength) {
                Log.e(TAG,"onDownloadReceived: savedLength"+savedLength+",totalLength:"+totalLength);
                Toast.makeText(getApplication(),
                        String.format(Locale.getDefault(), "%s %d%%",
                                Beta.strNotificationDownloading,
                                (int) (totalLength == 0 ? 0 : savedLength * 100 / totalLength)),
                        Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onDownloadSuccess(String msg) {
                Log.e(TAG,"onDownloadSuccess:"+msg);
                Toast.makeText(getApplication(), "补丁下载成功", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onDownloadFailure(String msg) {
                Log.e(TAG,"onDownloadFailure:"+msg);
                Toast.makeText(getApplication(), "补丁下载失败", Toast.LENGTH_SHORT).show();

            }

            @Override
            public void onApplySuccess(String msg) {
                Log.e(TAG,"onApplySuccess:"+msg);
                Toast.makeText(getApplication(), "补丁应用成功", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onApplyFailure(String msg) {
                Log.e(TAG,"onApplyFailure:"+msg);
                Toast.makeText(getApplication(), "补丁应用失败", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onPatchRollback() {

            }
        };

        // 设置开发设备,默认为false,上传补丁如果下发范围指定为“开发设备”,需要调用此接口来标识开发设备
        Bugly.setIsDevelopmentDevice(getApplication(), true);
        // 多渠道需求塞入
         String channel = WalleChannelReader.getChannel(getApplication());
         Bugly.setAppChannel(getApplication(), channel);


        // 这里实现SDK初始化,appId替换成你的在Bugly平台申请的appId
        // 调试时,将第三个参数改为true
        Bugly.init(getApplication(), "23a0cc94f2", true);
    }


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

    @Override
    public void onTerminate() {
        super.onTerminate();
        Beta.unInit();
    }

}
  • enableProxyApplication = true 的情况
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();
    }

}

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

6.AndroidManifest.xml配置

1.权限配置

    
    
    
    
    
    
2.Activity配置
 
3.FileProvider配置
 
            
        

     

7.制作基准包

在app编码完成并测试通过后,就是打包上线了,上线前打的包就是基准包。

  • 打开app下的tinker.support.gradle文件
  • 配置基准包的tinkerId
  • 双击运行build下的assembleRelease


    image.png

    AS在执行assembleRelease指令时,就是在编译基准包了,当编译完成时,app的build目录下会自动生成基准包文件夹,以时间戳来命名的(也就是说,每次执行assembleRelease指令都会在build目录下创建不同的基准包文件夹)。见下图:


    image.png

    这两个文件对之后制作补丁包来说是非常重要的,你需要做的就是将这两个文件保存好,可以保存到云盘,硬盘,git服务器上,但是不能就让它这样放着,因为当你执行clean project时,app的build目录会被删除,这样基准包与R文件都会丢失。

8.制作补丁包

  • 修改baseApkDir的值为基准包所有文件夹的名字。
  • 修改补丁包的 tinkerId
  • 打开侧边的Gradle标签,找到项目的主Module,双击tinker-support下的buildTinkerPatchRelease指令,生成补丁包。


    image.png

当编译完成后,在app的build/outputs/patch目录下会在"patch_singed_7zip.apk"文件,它就是补丁包,双击打开它,可以看到其中有一个YAPATCH.MF,里面记录了基准包与补丁包的tinkerId,Created-Time,VersionName等.


image.png

制作好的补丁,可以上传到bugly后台,进行下发。


image.png

Q. 我该上传哪个补丁?patch目录跟tinkerPatch目录下的补丁有什么区别吗?
A:你必须要上传build/outputs/patch目录下的补丁包,

9.多渠道打包

这里推荐使用walle来打多渠道包,新一代多渠道打包神器。

配置示例:

// 多渠道使用walle示例(注:多渠道使用)

apply from: 'multiple-channel.gradle'

创建multiple-channel.gradle,内容如下:
apply plugin: 'walle'
walle {
    // 指定渠道包的输出路径
    apkOutputFolder = new File("${project.buildDir}/outputs/channels");
    // 定制渠道包的APK的文件名称
    apkFileNameFormat = '${appName}-${packageName}-${channel}-${buildType}-v${versionName}-${versionCode}-${buildTime}.apk';
    // 渠道配置文件
    channelFile = new File("${project.getProjectDir()}/channel")
}
创建channel配置:
image.png
命令行打多渠道包:

执行 assembleReleaseChannels Task

image.png

ok,到此已经实现快速打多渠道包了。

如何获取渠道信息?
dependencies {
      ........
   // walle 瓦力(多渠道使用)
    implementation 'com.meituan.android.walle:library:1.1.6'
    ...............
}

在代码中获取渠道信息:

String channel = WalleChannelReader.getChannel(this.getApplicationContext());
Bugly.setAppChannel(getApplication(), channel);

集成当中遇到的问题:

1.Could not find method getAaptOptions() for arguments [] on task的问题
应该是gralde太新了,目前不支持gralde 5.0.我使用gradle版本5.6.4,修改了资源文件,进行补丁文件打包的时候 一直报这个错。

2.java.io.FileNotFoundException: E:...\build\intermediates\tinker_intermediates\values_backup
解决方法:
1.基准文件备份下
2.clean项目
3.重新打补丁包

3.org.gradle.internal.event.ListenerNotificationException: Failed to notify project evaluation listener.
我一开始使用gradle插件版本3.6.3,apply from: 'tinker-support.gradle'的时候 一直报这个错,后面的得知时gradle插件版本太高了,改成3.6.3以下的插件版本就ok了。

4.Sample Application: com.tencent.tinker.loader.a: Tinker Exception:createDelegate failed
我使用的是enableProxyApplication = false模式,运行一直报创建代理Applicaiton失败.我是打release包,因为开启了 minifyEnabled true,
可能是混淆了,把minfiyEnable设置为false 就好了。

5.Caused by: com.tencent.tinker.android.dex.DexException: Unexpected magic: [100, 101, 120, 10, 48, 51, 55, 0]
打补丁包的过程中一直出行这个错误,github上看到有人说是tinker不支持java1.8,这特么就尴尬了,我项目中用到lambda,难道只能放弃?

后面偶然网上说可能出现的问题

  • 基线版本问题
    基线版本最好采用gradle assemble、gradle assembleDebug、gradle assembleRelease等生成
    直接点击运行图标生成的apk貌似有问题。
  • 确认Patch包与基线包之间是否更改过下列相关信息,如不一致则报错。'

minSdkVersion 16
targetSdkVersion 29
versionCode 1
versionName "1.0.0"

  • minSdkVersion >=26 会出现这个问题

以上就是本人集成过程中碰到的一些问题,大部分都是跟gradle和插件版本有关,所以,升级到最新的gradle版本会碰到很多问题。

最后贴下Demo链接:

https://github.com/maozonghong/BuglyTinkerDemo

你可能感兴趣的:(Bugly热修复集成)