Flutter 使用 bugly 進行異常上報與熱修復

對於上線的應用,數據統計方面最基本的要求就是異常和錯誤信息的上報,flutter 端也有專門的工具和平臺來作這些事情,可是對於國內應用來講應該用的不是不少,用的比較多的應該仍是 bugly,同時集成 bugly 還能實現熱修復功能。android

對於集成、異常上報和熱修復,最主要的仍是以官網爲準。接下來演示一下如何在 flutter 應用上集成 bugly 來進行異常上報和熱修復。git

1、Bugly 集成

一、添加依賴

Android Studio 打開 flutter 裏面的 android 原生工程,在 project 的 build.gradle 裏面添加 bugly 和 tinker 的插件。github

dependencies {
        classpath 'com.android.tools.build:gradle:3.4.1'
        // tinkersupport插件(1.0.3以上無須再配置tinker插件)
        classpath "com.tencent.bugly:tinker-support:1.1.5"
        classpath 'com.tencent.bugly:symtabfileuploader:2.2.1'

    }
複製代碼

而後在 app 的 build.gardle 裏面引入插件編程

//騰訊bug管理插件
apply plugin: 'bugly'

複製代碼

接着在 app 的 build.gradle 裏面添加依賴bash

implementation "com.android.support:multidex:1.0.1" // 多dex配置
    //implementation 'com.tencent.bugly:crashreport_upgrade:1.3.4'// 遠程倉庫集成方式(推薦)
    //implementation("com.tencent.tinker:tinker-android-lib:1.9.1") { changing = true }

    implementation 'com.tencent.bugly:crashreport_upgrade:1.3.5'
    // 指定tinker依賴版本(注:應用升級1.3.5版本起,再也不內置tinker)
    implementation 'com.tencent.tinker:tinker-android-lib:1.9.6'
    implementation 'com.tencent.bugly:nativecrashreport:latest.release' //其中latest.release指代最新版本號,也能夠指定明確的版本號,例如2.2.0

    
    implementation 'com.android.support:support-v4:28.0.0'
    implementation 'com.android.support:appcompat-v7:28.0.0'
複製代碼

二、打包配置

添加完成依賴以後,配置一下打包須要的信息。 首先將本身的簽名文件放到 app\keystore 下。微信

接着在 app 的 build.gardle 裏面設置配置信息。架構

signingConfigs {
        release {
            try {
                storeFile file("./keystore/hcc.jks")
                storePassword "hcc007"
                keyAlias "hcc"
                keyPassword "hcc007"
            } 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')

            // 混淆開關
            minifyEnabled false
            // 是否zip對齊
            zipAlignEnabled true
            // 移除無用的resource文件
            shrinkResources false
            // 是否打開debuggable開關
            debuggable false
            // 是否打開jniDebuggable開關
            jniDebuggable false
            // 混淆配置文件
            proguardFile 'proguard-rules.pro'
            //
//
            ndk {
                abiFilters 'armeabi-v7a' //, 'armeabi-v7a', 'x86_64', 'arm64-v8a', 'mips', 'mips64'
            }
        }
        debug {
            debuggable true
            minifyEnabled false
            signingConfig signingConfigs.release

            ndk {
                abiFilters 'armeabi' , 'armeabi-v7a', 'x86_64', 'arm64-v8a', 'mips', 'mips64'
            }
        }
    }

複製代碼

這樣就能夠打 release 包了。app

注意: 若是配置了使用混淆文件,則須要在 progrard-rules.pro 文件裏面添加對應的混淆規則,好比 bugly 的就是:ide

# Bugly混淆規則
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}

# 避免影響升級功能,須要keep住support包的類
-keep class android.support.**{*;}

複製代碼

若是在原生端還有其餘的須要添加混淆規則的,都加上,以避免打出的 release 包出現異常。工具

三、新建 tinker-support.gradle 文件

要使用 tinker 熱修復的功能,首先須要新建一個 tinker 相關的腳本文件,這個文件就是官網示例的那個文件,須要在 app 裏面的 build.gradle 的同級目錄下新建這個文件。 文件內容以下:

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

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

/**
 * 此處填寫每次構建生成的基準包目錄
 */
def baseApkDir = "app-1123-18-38-49"

/**
 * 對於插件各參數的詳細解析請參考
 */
tinkerSupport {

    // 開啓tinker-support插件,默認值true
    enable = true
    tinkerEnable = 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 = "patch-20.0-test"

    // 構建多渠道補丁時用
    // buildAllFlavorsDir = "${bakPath}/${baseApkDir}"

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

    // 是否開啓反射Application模式
    enableProxyApplication = false


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

}

/**
 * 通常來講,咱們無需對下面的參數作任何的修改
 * 對於各參數的詳細介紹請參考:
 * 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的分配
    }
}
複製代碼

而後在 app 的 build.gradle 中引入這個腳本文件。

//騰訊bug管理插件
apply plugin: 'bugly'
apply from: 'tinker-support.gradle'
複製代碼

這樣就可使用 tinker 提供的打差分包的功能了。

注意:
腳本都修改完成以後,須要 Sync 一下下載代碼等。

四、AndroidManifest.xml 配置

按照 bugly 官網的要求,還須要在 AndroidManifest.xml 進行相關的配置。

配置權限
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE">
    </uses-permission>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.READ_LOGS"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
複製代碼
配置 provider 和 activity
<activity
            android:name="com.tencent.bugly.beta.ui.BetaActivity"
            android:configChanges="keyboardHidden|orientation|screenSize|locale"
            android:theme="@android:style/Theme.Translucent"/>

        <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>

複製代碼
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>

複製代碼

五、Application 配置

按照官方文檔建議的,不採用 Application 反射的模式:

// 是否開啓反射Application模式
    enableProxyApplication = false
複製代碼
一、新建 MyApplication
public class  MyApplication    extends TinkerApplication {
    public MyApplication() {

        super(ShareConstants.TINKER_ENABLE_ALL, "com.hc.flutter_hotfix_notkt.SampleApplicationLike",
                "com.tencent.tinker.loader.TinkerLoader", false);
    }
}

複製代碼

這裏會直接回調 SampleApplicationLike 。

二、新建SampleApplicationLike

這裏是真正的 Applicaion 實現類,這裏進行熱修復和 Tinker 等初始化配置

public class SampleApplicationLike extends DefaultApplicationLike {

    public static final String TAG = "hcc";
    private Application mContext;

    @SuppressLint("LongLogTag")
    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();
        mContext = getApplication();

        load_library_hack();
        if(BuildConfig.DEBUG){
            FlutterMain.startInitialization(mContext);
        }else {
            MyFlutterMain.startInitialization(mContext);
        }

        configTinker();
      //

    }

    // 使用Hack的方式(測試成功),flutter 加載,也是經過這種方式成功的。
    public void load_library_hack( ) {
        Log.i(TAG, "load_library_hack: ");
        String CPU_ABI = Build.CPU_ABI;
        // 將tinker library中的 CPU_ABI架構的so 註冊到系統的library path中。
        try {
            ///
            Toast.makeText(mContext,"開始加載 so,abi:" + CPU_ABI,Toast.LENGTH_SHORT).show();
            //  TinkerLoadLibrary.installNavitveLibraryABI(this, CPU_ABI);
            //這個路徑寫死試一下,也就是,獲取的 CPU_ABI,不許。
            TinkerLoadLibrary.installNavitveLibraryABI(mContext, "armeabi-v7a");
            //    TinkerLoadLibrary.loadLibraryFromTinker(MainActivity.this, "lib/armeabi", "app");
            Toast.makeText(mContext,"加載 so 完成",Toast.LENGTH_SHORT).show();
            ///data/data/${package_name}/tinker/lib
            Tinker tinker = Tinker.with(mContext);
            TinkerLoadResult loadResult = tinker.getTinkerLoadResultIfPresent();
            if (loadResult.libs == null) {
                return;
            }
            File soDir = new File(loadResult.libraryDirectory, "lib/" + "armeabi-v7a/libapp.so");
            if (soDir.exists()){
                if(!BuildConfig.DEBUG){
                    Log.i(TAG, "load_library_hack: 開始設置 tinker 路徑");
                }
            }else {
                Log.i("hcc", "load_library_hack: so 庫文件路徑不存在。。。 ");
            }
        }catch (Exception e){
            Toast.makeText(mContext,e.toString(),Toast.LENGTH_SHORT).show();
        }

    }

    private void configTinker() {
        Log.i(TAG, "configTinker: ");
        // 設置是否開啓熱更新能力,默認爲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) {
                Toast.makeText(mContext, "補丁下載地址" + patchFile, Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onDownloadReceived(long savedLength, long totalLength) {
                Toast.makeText(mContext,
                        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) {
                Toast.makeText(mContext, "補丁下載成功", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onDownloadFailure(String msg) {
                Toast.makeText(mContext, "補丁下載失敗", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onApplySuccess(String msg) {
                Toast.makeText(mContext, "補丁應用成功", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onApplyFailure(String msg) {
                Toast.makeText(mContext, "補丁應用失敗", Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onPatchRollback() {

            }
        };

        // 設置開發設備,默認爲false,上傳補丁若是下發範圍指定爲「開發設備」,須要調用此接口來標識開發設備
        Bugly.setIsDevelopmentDevice(mContext, false);
        Bugly.init(mContext, "你本身的appid", true);
        // 多渠道需求塞入
        // String channel = WalleChannelReader.getChannel(getApplication());
        // Bugly.setAppChannel(getApplication(), channel);
        // 這裏實現SDK初始化,appId替換成你的在Bugly平臺申請的appId

    }

    @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
        Beta.installTinker(this);
    }

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

}


複製代碼
三、Manifest 中指定application
<application
        android:name=".MyApplication"
        android:label="flutter_hotfix_notkt"
        android:icon="@mipmap/ic_launcher">
        ...
    </application>
複製代碼

2、flutter 異常上報

bugly 是不能收集到 flutter 的崩潰信息的,所以須要咱們本身手動上報,flutter 提供了手動上報的功能:

CrashReport.postCatchedException(new Throwable(msg));
複製代碼

所以咱們只要把 flutter 端的錯誤,經過原生手動上報給 bugly 就好了。

一、手動調用

能夠手動上報一條錯誤信息,flutter 端:

RaisedButton(
              child: Text("手動上報"),
              onPressed: (){
                platform.invokeMethod('report',"手動上報錯誤");
              },
            ),
複製代碼

原生經過 methodChannel 接收到消息以後上報給 bugly:

if(call.method.equals( "report")){
                    String msg = call.arguments.toString();
                  CrashReport.postCatchedException(new Throwable(msg));
                }

複製代碼

二、自動捕獲異常上報

經過 runZoned 來捕獲和上報錯誤,至關於 try/catch:

const platform = const MethodChannel('com.hc.flutter');


void collectLog(String line){
  //收集日誌
}
void reportErrorAndLog(FlutterErrorDetails details){
 //上報錯誤和日誌邏輯
  platform.invokeMethod('report',details.toString());
}

FlutterErrorDetails makeDetails(Object obj, StackTrace stack){
 // 構建錯誤信息
}

void main() {
  FlutterError.onError = (FlutterErrorDetails details) {
    reportErrorAndLog(details);
  };

  runZoned(
        () => runApp(MyApp()),
    zoneSpecification: ZoneSpecification(

      print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
        collectLog(line); //手機日誌
      },

    ),
    onError: (Object obj, StackTrace stack) {
      var details = makeDetails(obj, stack);
      reportErrorAndLog(details);
    },
  );
}

複製代碼

這樣就能夠捕獲到 flutter 這邊也異常上報給 bugly。

注意: 這樣捕獲到的錯誤不是崩潰信息,須要在錯誤分析中查看

3、經過 bugly 進行熱修復

關於熱修復原理和代碼相關的,參考上一篇文章Flutter Android 端熱修復(熱更新)實踐

一、打 release 包作爲基準包

打 release 包以前,須要配置在 tinker-support.gradle 裏面的 tinkerId,這個是這個 release 包的一個標識。

// 構建基準包和補丁包都要指定不一樣的tinkerId,而且必須保證惟一性
    tinkerId = "base-20.0-test"
複製代碼

Gradle Task 面板執行 assembleRelease 的 task,也能夠執行 assemble ,只不過這個也會打 debug 包,或者直接在 android 項目目錄下運行 命令:

gradlew assembleRelease 
複製代碼

拿到 release 安裝包以後,安裝並在聯網狀態下運行一次,只有在聯網時才能上報信息給 bugly, 這樣才能熱修復動態下發補丁包。

二、修改代碼(bug)經過 tinker 打差分包

打差分包確定須要有一個對照,對照的就是剛纔打的基準包,在 tinker-support.gradle 中指定這個基準包。

同時別忘了修改 tinkerId,bugly 會把這個修復包的 tinkerId 和 打的基準包對應上,這樣才能匹配到具體的版本。

// 構建基準包和補丁包都要指定不一樣的tinkerId,而且必須保證惟一性
    tinkerId = "patch-20.0-test"
複製代碼

都修改完成以後,執行 buildTinkerPatchRelease 打差分包

注意: 是 tinker-support 下的,不是 tinker 下的 task.

執行完命令以後,能夠在 build/app/outputs/patch/release 下找到已經壓縮好的差分包。

注意: 必須是 build/app/outpus/patch/release 下的纔是正確的差分包,不是在 app/outputs/apk/rinkerPatch 下的那個。

Android Studio 直接打開這個文件,看一下

若是有上面的如 Created-Time、From、To 等配置信息,基本就沒有問題了。

三、bugly 上傳補丁進行熱修復

在 bugly 的後臺上,經過 應用升級——熱更新來發布新補丁,同時能夠選擇全量設備仍是開發設備。若是不出意外的話,上傳成功以後,手機端就能夠等待熱修復效果了,可能須要幾分鐘的時間。

在修復以前:

若是收到了更新補丁,而且 tinker 配置了提示用戶重啓:

// 設置是否提示用戶重啓,默認爲false
        Beta.canNotifyUserRestart = true;
複製代碼

重啓以後完成修復:

4、注意事項

在使用 bugly 熱修復的時候有幾個注意事項:

  1. manifest 中不要忘記權限,同時
  2. 若是是啓用了混淆,對應的混淆規則注意添加
  3. tinkerId 和打差分包的 tinkerId 每次須要惟一,不要重複打包上傳
  4. tinker 熱修復 so 的時候,CPU_ABI 不要指定錯了,由於如今大部分 app 都是使用了 armeabi-v7a 一種,若是須要其餘的,都相應的在 build.gradle 和 代碼中修改便可。
ndk {
                abiFilters 'armeabi-v7a' //, 'armeabi-v7a', 'x86_64', 'arm64-v8a', 'mips', 'mips64'
            }
複製代碼
  1. 因爲 flutter 支持 debug 熱重載和 release AOT 兩種模式,代碼不同,能夠經過以下方式指定運行的代碼,這樣既能夠 debug 調試也能夠打 release 包
if(BuildConfig.DEBUG){
            FlutterMain.startInitialization(mContext);
        }else {
            MyFlutterMain.startInitialization(mContext);
        }
複製代碼
  1. 若是使用 bugly 熱修復失敗或者只想體驗 tinker 熱修復我還寫了一個本地的集成 tinker 熱修復 的 demo,參考這個 github,這個沒有問題以後,bugly 也應該沒有問題。

更詳細的代碼請參考 github

歡迎關注「Flutter 編程開發」微信公衆號 。

相關文章
相關標籤/搜索