導讀
Deferred Components,官方實作的Flutter代碼動態下發的方案,本文主要介紹官方方案的實作細節,探索在國內環境下使用Deferred Components,并且實作了最小驗證demo,讀罷本文,你就可以實作Dart檔案級別代碼的動態下發,
一、引言
Deferred Components是Flutter2.2推出的功能,依賴于Dart2.13新增的對Split AOT編譯支持,將可以在運行時每一個可單獨下載的Dart庫、assets資源包稱之為延遲加載組件,即Deferred Components,Flutter代碼編譯后,所有的業務邏輯都會打包在libapp.so一個檔案里,但如果使用了延遲加載,便可以分拆為多個so檔案,甚至一個Dart檔案也可以編譯成一個單獨的so檔案,
這樣帶來的好處是顯而易見的,可以將一些不常用功能放到單獨的so檔案中,當用戶使用時再去下載,可以大大降低安裝包的大小,提高應用的下載轉換率,另外,因為Flutter具備了運行時動態下發的能力,這讓大家看到了實作Flutter熱修復的另一種可能,截止目前來講,官方的實作方案必須依賴Google Play,雖然也針對中國的開發者給出了不依賴Google Play的自定義方案,但是并沒有給出實作細節,市面上也沒有自定義實作的文章,本文會先簡單介紹官方實作方案,并探究其細節,尋找自定義實作的思路,最侄訓實作一個最小Demo供大家參考,
二、官方實作方案探究
2.1 基本步驟
2.1.1.引入play core依賴,
dependencies {
implementation "com.google.android.play:core:1.8.0"
}
2.1.2.修改Application類的onCreate方法和attachBaseContext方法,
@Override
protected void onCreate(){
super.onCreate()
// 負責deferred components的下載與安裝
PlayStoreDeferredComponentManager deferredComponentManager = new
PlayStoreDeferredComponentManager(this, null);
FlutterInjector.setInstance(new FlutterInjector.Builder()
.setDeferredComponentManager(deferredComponentManager).build());
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// Emulates installation of future on demand modules using SplitCompat.
SplitCompat.install(this);
}
2.1.3.修改pubspec.yaml檔案,
flutter:
deferred-components:
2.1.4.在flutter工程里新增box.dart和some_widgets.dart兩個檔案,DeferredBox就是要延遲加載的控制元件,本例中box.dart被稱為一個加載單元,即loading_unit,每一個loading_unit對應唯一的id,一個deferred component可以包含多個加載單元,記得這個概念,后續會用到,
// box.dart
import 'package:flutter/widgets.dart';
/// A simple blue 30x30 box.
class DeferredBox extends StatelessWidget {
DeferredBox() {}
@override
Widget build(BuildContext context) {
return Container(
height: 30,
width: 30,
color: Colors.blue,
);
}
}
import 'box.dart' deferred as box;
class SomeWidget extends StatefulWidget {
@override
_SomeWidgetState createState() => _SomeWidgetState();
}
class _SomeWidgetState extends State<SomeWidget> {
Future<void> _libraryFuture;
@override
void initState() {
//只有呼叫了loadLibrary方法,才會去真正下載并安裝deferred components.
_libraryFuture = box.loadLibrary();
super.initState();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: _libraryFuture,
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return box.DeferredBox();
}
return CircularProgressIndicator();
},
);
}
}
2.1.5.然后在main.dart里面新增一個跳轉到SomeWidget頁面的按鈕,
Navigator.push(context, MaterialPageRoute(
builder: (context) {
return const SomeWidget();
},
));
2.1.6.terminal里運行 flutter build appbundle 命令,此時,gen_snapshot不會立即去編譯app,而是先運行一個驗證程式,目的是驗證此工程是否符合動態下發dart代碼的格式,第一次構建時肯定不會成功,你只需要按照編譯提示去修改即可,當全部修改完畢后,會得到最終的.aab型別的安裝包,
以上便是官方實作方案的基本步驟,更多細節可以參考官方檔案
https://docs.flutter.dev/perf/deferred-components
2.2 本地驗證
在將生成的aab安裝包上傳到Google Play上之前,最好先本地驗證一下,
首先你需要下載bundletool,然后依次運行下列命令就可以將aab安裝包裝在手機上進行最終的驗證了,
java -jar bundletool.jar build-apks --bundle=<your_app_project_dir>/build/app/outputs/bundle/release/app-release.aab --output=<your_temp_dir>/app.apks --local-testing
java -jar bundletool.jar install-apks --apks=<your_temp_dir>/app.apks
2.3 loadLibrary()方法呼叫的生命周期
圖1 官方實作方案介紹圖
(來源:https://github.com/flutter/flutter/wiki/Deferred-Components)
從官方的實作方案中可以知道,只有呼叫了loadLibrary方法后,才會去真正執行deferred components的下載與安裝作業,現在著重看下此方法的生命周期,
呼叫完loadLibrary方法后,dart會在內部查詢此加載單元的id,并將其一直向下傳遞,當到達jni層時,jni負責將此加載單元對應的deferred component的名字以及此加載單元id一塊傳遞給
PlayStoreDynamicFeatureManager,此類負責從Google Play Store服務器下載對應的Deferred Components并負責安裝,安裝完成后會逐層通知,最終告訴dart層,在下一幀渲染時展示動態下發的控制元件,
三、自定義實作
3.1 思路
梳理了loadLibrary方法呼叫的生命周期后,只需要自己實作一個類來代替
PlayStoreDynamicFeatureManager的功能即可,在官方方案中具體負責完成PlayStoreDynamicFeatureManager功能的物體類是io.flutter.embedding.engine.deferredcomponents.PlayStoreDeferredComponentManager,其繼承自DeferredComponentManager,分析原始碼得知,它最重要的兩個方法是installDeferredComponent和loadDartLibrary,
- installDeferredComponent:這個方法主要負責component的下載與安裝,下載安裝完成后會呼叫loadLibrary方法,如果是asset-only component,那么也需要呼叫DeferredComponentChannel.completeInstallSuccess或者DeferredComponentChannel.completeInstallError方法,
- loadDartLibrary:主要是負責找到so檔案的位置,并呼叫FlutterJNI dlopen命令打開so檔案,你可以直接傳入apk的位置,flutterJNI會直接去apk里加載so,避免處理解壓apk的邏輯,
那基本思路就有了,自己實作一個物體類,繼承DeferredComponentManager,實作這兩個方法即可,
3.2 代碼實作
本例只是最小demo實作,cpu架構采用arm64,且暫不考慮asset-only型別的component,
3.2.1.新增
CustomDeferredComponentsManager類,繼承DeferredComponentManager,
3.2.2.實作installDeferredComponent方法,將so檔案放到外部SdCard存盤里,代碼負責將其拷貝到應用的私有存盤中,以此來模擬網路下載程序,代碼如下:
@Override
public void installDeferredComponent(int loadingUnitId, String componentName) {
String resolvedComponentName = componentName != null ? componentName : loadingUnitIdToComponentNames.get(loadingUnitId);
if (resolvedComponentName == null) {
Log.e(TAG, "Deferred component name was null and could not be resolved from loading unit id.");
return;
}
// Handle a loading unit that is included in the base module that does not need download.
if (resolvedComponentName.equals("") && loadingUnitId > 0) {
// No need to load assets as base assets are already loaded.
loadDartLibrary(loadingUnitId, resolvedComponentName);
return;
}
//耗時操作,模擬網路請求去下載android module
new Thread(
() -> {
//將so檔案從外部存盤移動到內部私有存盤中
boolean result = moveSoToPrivateDir();
if (result) {
//模擬網路下載,添加2秒網路延遲
new Handler(Looper.getMainLooper()).postDelayed(
() -> {
loadAssets(loadingUnitId, resolvedComponentName);
loadDartLibrary(loadingUnitId, resolvedComponentName);
if (channel != null) {
channel.completeInstallSuccess(resolvedComponentName);
}
}
, 2000);
} else {
new Handler(Looper.getMainLooper()).post(
() -> {
Toast.makeText(context, "未在sd卡中找到so檔案", Toast.LENGTH_LONG).show();
if (channel != null) {
channel.completeInstallError(resolvedComponentName, "未在sd卡中找到so檔案");
}
if (flutterJNI != null) {
flutterJNI.deferredComponentInstallFailure(loadingUnitId, "未在sd卡中找到so檔案", true);
}
}
);
}
}
).start();
}
3.2.3.實作loadDartLibrary方法,可以直接拷貝
PlayStoreDeferredComponentManager類中的此方法,注釋已加,其主要作用就是在內部私有存盤中找到so檔案,并呼叫FlutterJNI dlopen命令打開so檔案,
@Override
public void loadDartLibrary(int loadingUnitId, String componentName) {
if (!verifyJNI()) {
return;
}
// Loading unit must be specified and valid to load a dart library.
//asset-only的component的unit id為-1,不需要加載so檔案
if (loadingUnitId < 0) {
return;
}
//拿到so的檔案名字
String aotSharedLibraryName = loadingUnitIdToSharedLibraryNames.get(loadingUnitId);
if (aotSharedLibraryName == null) {
// If the filename is not specified, we use dart's loading unit naming convention.
aotSharedLibraryName = flutterApplicationInfo.aotSharedLibraryName + "-" + loadingUnitId + ".part.so";
}
//拿到支持的abi格式--arm64_v8a
// Possible values: armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, mips, mips64
String abi;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
abi = Build.SUPPORTED_ABIS[0];
} else {
abi = Build.CPU_ABI;
}
String pathAbi = abi.replace("-", "_"); // abis are represented with underscores in paths.
// TODO(garyq): Optimize this apk/file discovery process to use less i/o and be more
// performant and robust.
// Search directly in APKs first
List<String> apkPaths = new ArrayList<>();
// If not found in APKs, we check in extracted native libs for the lib directly.
List<String> soPaths = new ArrayList<>();
Queue<File> searchFiles = new LinkedList<>();
// Downloaded modules are stored here--下載的 modules 存盤位置
searchFiles.add(context.getFilesDir());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//第一次通過appbundle形式安裝的split apks位置
// The initial installed apks are provided by `sourceDirs` in ApplicationInfo.
// The jniLibs we want are in the splits not the baseDir. These
// APKs are only searched as a fallback, as base libs generally do not need
// to be fully path referenced.
for (String path : context.getApplicationInfo().splitSourceDirs) {
searchFiles.add(new File(path));
}
}
//查找apk和so檔案
while (!searchFiles.isEmpty()) {
File file = searchFiles.remove();
if (file != null && file.isDirectory() && file.listFiles() != null) {
for (File f : file.listFiles()) {
searchFiles.add(f);
}
continue;
}
String name = file.getName();
// Special case for "split_config" since android base module non-master apks are
// initially installed with the "split_config" prefix/name.
if (name.endsWith(".apk")
&& (name.startsWith(componentName) || name.startsWith("split_config"))
&& name.contains(pathAbi)) {
apkPaths.add(file.getAbsolutePath());
continue;
}
if (name.equals(aotSharedLibraryName)) {
soPaths.add(file.getAbsolutePath());
}
}
List<String> searchPaths = new ArrayList<>();
// Add the bare filename as the first search path. In some devices, the so
// file can be dlopen-ed with just the file name.
searchPaths.add(aotSharedLibraryName);
for (String path : apkPaths) {
searchPaths.add(path + "!lib/" + abi + "/" + aotSharedLibraryName);
}
for (String path : soPaths) {
searchPaths.add(path);
}
//打開so檔案
flutterJNI.loadDartDeferredLibrary(loadingUnitId, searchPaths.toArray(new String[searchPaths.size()]));
}
3.2.4.修改Application的代碼并洗掉
com.google.android.play:core的依賴,
override fun onCreate() {
super.onCreate()
val deferredComponentManager = CustomDeferredComponentsManager(this, null)
val injector = FlutterInjector.Builder().setDeferredComponentManager(deferredComponentManager).build()
FlutterInjector.setInstance(injector)
至此,核心代碼全部實作完畢,其他細節代碼可以見
https://coding.jd.com/jd_logistic/deferred_component_demo/,需要加權限的聯系shenmingliang1即可,
3.3 本地驗證
- 運行 flutter build appbundle --release --target-platform android-arm64 命令生成app-release.aab檔案,
- .運行下列命令將app-release.aab決議出本地可以安裝的apks檔案:java -jar bundletool.jar build-apks --bundle=app-release.aab --output=app.apks --local-testing
- 解壓上一步生成的app.apks檔案,在加壓后的app檔案夾下找到splits/scoreComponent-arm64_v8a_2.apk,繼續解壓此apk檔案,在生成的scoreComponent-arm64_v8a_2檔案夾里找到lib/arm64-v8a/libapp.so-2.part.so 檔案,
- 執行 java -jar bundletool.jar install-apks --apks=app.apks命令安裝app.apks,此時打開安裝后的app,點擊首頁右下角的按鈕跳轉到DeferredPage頁面,此時頁面不會成功加載,并且會提示你“未在sd卡中找到so檔案”,
- 將第3步找到的lipase.so-2.part.so push到指定檔案夾下,命令如下 adb push libapp.so-2.part.so /storage/emulated/0/Android/data/com.example.deferred_official_demo/files,重啟app行程,并重新打開DeferredPage界面即可,
四、 總結
官方實作方案對國內的使用來講,最大的限制無疑是Google Play,本文實作了一個脫離Google Play限制的最小demo,驗證了deferred components在國內使用的可行性,
參考:
- https://docs.flutter.dev/perf/deferred-components
- https://github.com/flutter/flutter/wiki/Deferred-Components
作者:京東物流 沈明亮
內容來源:京東云開發者社區
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/553175.html
標籤:Html/Css
上一篇:React筆記-Hooks(九)
下一篇:返回列表
