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

Flutter程式設計開發發表於2019-11-23

對於上線的應用,資料統計方面最基本的要求就是異常和錯誤資訊的上報,flutter 端也有專門的工具和平臺來做這些事情,但是對於國內應用來說應該用的不是很多,用的比較多的應該還是 bugly,同時整合 bugly 還能實現熱修復功能。

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

一、Bugly 整合

1、新增依賴

Android Studio 開啟 flutter 裡面的 android 原生工程,在 project 的 build.gradle 裡面新增 bugly 和 tinker 的外掛。

    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 裡面新增依賴

    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'
複製程式碼

2、打包配置

新增完成依賴之後,配置一下打包需要的資訊。 首先將自己的簽名檔案放到 app\keystore 下。

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

接著在 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 包了。

注意: 如果配置了使用混淆檔案,則需要在 progrard-rules.pro 檔案裡面新增對應的混淆規則,比如 bugly 的就是:


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

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

複製程式碼

如果在原生端還有其他的需要新增混淆規則的,都加上,以免打出的 release 包出現異常。

3、新建 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 一下下載程式碼等。

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

複製程式碼

5、Application 配置

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

    // 是否開啟反射Application模式
    enableProxyApplication = false
複製程式碼
1、新建 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 。

2、新建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);
    }

}


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

二、flutter 異常上報

bugly 是不能收集到 flutter 的崩潰資訊的,因此需要我們自己手動上報,flutter 提供了手動上報的功能:

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

因此我們只要把 flutter 端的錯誤,通過原生手動上報給 bugly 就行了。

1、手動呼叫

可以手動上報一條錯誤資訊,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));
                }

複製程式碼

2、自動捕獲異常上報

通過 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。

注意: 這樣捕獲到的錯誤不是崩潰資訊,需要在錯誤分析中檢視

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

三、通過 bugly 進行熱修復

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

1、打 release 包做為基準包

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

    // 構建基準包和補丁包都要指定不同的tinkerId,並且必須保證唯一性
    tinkerId = "base-20.0-test"
複製程式碼

Gradle Task 皮膚執行 assembleRelease 的 task,也可以執行 assemble ,只不過這個也會打 debug 包,或者直接在 android 專案目錄下執行 命令:

gradlew assembleRelease 
複製程式碼

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

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

2、修改程式碼(bug)通過 tinker 打差分包

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

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

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

  // 構建基準包和補丁包都要指定不同的tinkerId,並且必須保證唯一性
    tinkerId = "patch-20.0-test"
複製程式碼

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

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

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

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

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

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

Android Studio 直接開啟這個檔案,看一下

Flutter 使用 bugly 進行異常上報與熱修復
如果有上面的如 Created-Time、From、To 等配置資訊,基本就沒有問題了。

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

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

在修復之前:

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

如果收到了更新補丁,並且 tinker 配置了提示使用者重啟:

      // 設定是否提示使用者重啟,預設為false
        Beta.canNotifyUserRestart = true;
複製程式碼

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

重啟之後完成修復:

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

四、注意事項

在使用 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 程式設計開發」微信公眾號 。

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

相關文章