版本號0.1.54
看源碼以前,我先去看下官方文檔,對於其源碼的設計說明,文中所說的原生都是指android前端
看完官方文檔的說明,我有如下幾個疑問java
第一個:容器是怎麼設計的?android
第二個:native和flutter的channel的通道是如何設計的?ios
第三個:Flutter是適配層到底再作些什麼?segmentfault
單獨拎出來說講,這個類比較簡單,就是集合各個模塊並讓其初始化,同時也是該插件入口處,無論原生和flutter都同樣,看源碼也是從這裏開始看起,但原生和flutter的初始化流程稍微有少量區別,主要仍是由於原生是做爲容器,flutter的容器是依賴於原生容器。api
入口:/flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/MyApplication.javaapp
FlutterBoost.init從這裏開始進入框架
FlutterBoost.init(new Platform() { @Override public Application getApplication() { return MyApplication.this; } @Override public boolean isDebug() { return true; } @Override public void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode, Map<String, Object> exts) { PageRouter.openPageByUrl(context, url, urlParams, requestCode); } @Override public IFlutterEngineProvider engineProvider() { //注意這裏 覆寫了createEngine return new BoostEngineProvider() { @Override public BoostFlutterEngine createEngine(Context context) { return new BoostFlutterEngine(context, new DartExecutor.DartEntrypoint( context.getResources().getAssets(), FlutterMain.findAppBundlePath(context), "main"), "/"); } }; } @Override public int whenEngineStart() { return ANY_ACTIVITY_CREATED; } }); BoostChannel.addActionAfterRegistered(new BoostChannel.ActionAfterRegistered() { @Override public void onChannelRegistered(BoostChannel channel) { //platform view register should use FlutterPluginRegistry instread of BoostPluginRegistry TextPlatformViewPlugin.register(FlutterBoost.singleton().engineProvider().tryGetEngine().getPluginRegistry()); } }); }
上面大部分方法,作過android也知道是幹嗎的,這裏重點講講IFlutterEngineProvider這個接口,這裏有3個方法,以下less
/** * a flutter engine provider */ public interface IFlutterEngineProvider { /** * create flutter engine, we just hold a single instance now * @param context * @return */ BoostFlutterEngine createEngine(Context context); /** * provide a flutter engine * @param context * @return */ BoostFlutterEngine provideEngine(Context context); /** * may return null * @return */ BoostFlutterEngine tryGetEngine(); }
抽象成接口,根據項目的實際狀況,開發者能夠本身實現flutter引擎,或採用官方源碼裏本身的實現類即BoostEngineProvider,但想不明白 createEngine 和provideEngine 到底有何區別,到底爲什麼弄成兩個方法,不就是個提供個flutter引擎實例嗎?異步
看了下createEngine的實現,主要加載實例BoostFlutterEngine,這個實例看名字也清楚是進行flutter引擎的初始化,設置了dart默認入口點即main,設置了路由起點及插件的聲明註冊一類
而後去看provideEngine方法的實現,代碼較少,以下
@Override public BoostFlutterEngine provideEngine(Context context) { Utils.assertCallOnMainThread(); if (mEngine == null) { FlutterShellArgs flutterShellArgs = new FlutterShellArgs(new String[0]); FlutterMain.ensureInitializationComplete( context.getApplicationContext(), flutterShellArgs.toArray()); mEngine = createEngine(context.getApplicationContext()); final IStateListener stateListener = FlutterBoost.sInstance.mStateListener; if(stateListener != null) { stateListener.onEngineCreated(mEngine); } } return mEngine; }
初始化flutter參數及增長一個回調,沒什麼特別之處,而後去翻了下flutter.jar的FlutterActivity源碼,它的flutter引擎初始化最後是追蹤到FlutterFragment,關鍵代碼以下
public void onAttach(Context context) { super.onAttach(context); //這裏初始化flutter參數 this.initializeFlutter(this.getContextCompat()); if (this.flutterEngine == null) { //這裏是初始化flutter引擎 this.setupFlutterEngine(); } this.platformPlugin = new PlatformPlugin(this.getActivity(), this.flutterEngine.getPlatformChannel()); }
這裏是連在一塊兒的,flutter源碼沒有翻來覆去全看一遍,閒魚進行這樣的接口設計應該是有必定的緣由
這裏再單獨講下插件的註冊,咱們知道native是做爲插件庫須要原生項目依賴,在初始化中,注意一下插件的註冊,是用反射實現的,以下
路徑:flutter_boost/android/src/main/java/com/idlefish/flutterboost/BoostFlutterView.java 中的init方法,代碼以下
private void init() { ... mFlutterEngine.startRun((Activity)getContext()); ... }
跟隨startRun方法深刻,就會找到FlutterBoost.singleton().platform().registerPlugins(mBoostPluginRegistry),跟着下去,會發現使用反射方式來實現插件註冊 以下代碼
@Override public void registerPlugins(PluginRegistry registry) { try { Class clz = Class.forName("io.flutter.plugins.GeneratedPluginRegistrant"); Method method = clz.getDeclaredMethod("registerWith",PluginRegistry.class); method.invoke(null,registry); }catch (Throwable t){ throw new RuntimeException(t); } }
畢竟引擎初始化框架從新編寫了,因此在插件的註冊上也改變了,init的原生部分就講解到此
入口:/flutterProject/flutter_boost/example/lib/main.dart
flutter的源碼查看前,你們務必先去看看flutter的初始化流程,Navigator源碼解析及Route源碼解析,由於不曉得相關初始化流程及Navigator的設計原理,裏面的關鍵調用 你們均可能看不明白,我這邊可能也是直接就過了,這裏給個連接你們能夠去看看
@override void initState() { super.initState(); print('_MyAppState initState'); ///路由註冊,原生經過MethodChannel通道來啓動對應的flutter頁面 FlutterBoost.singleton.registerPageBuilders({ 'first': (pageName, params, _) => FirstRouteWidget(), 'second': (pageName, params, _) => SecondRouteWidget(), 'tab': (pageName, params, _) => TabRouteWidget(), 'flutterFragment': (pageName, params, _) => FragmentRouteWidget(params), ///能夠在native層經過 getContainerParams 來傳遞參數 'flutterPage': (pageName, params, _) { print("flutterPage params:$params"); return FlutterRouteWidget(); }, }); } @override Widget build(BuildContext context) { print('_MyAppState build'); return MaterialApp( title: 'Flutter Boost example', builder: FlutterBoost.init(postPush: _onRoutePushed), home: Container()); } ///flutter 路由push 監聽,每啓動一個新的flutter頁面 就回調該方法 void _onRoutePushed( String pageName, String uniqueId, Map params, Route route, Future _) { print('pageName'+pageName+"\n"); }
先跟隨FlutterBoost.singleton進去看看,其構造函數以下
FlutterBoost(){ Logger.log('FlutterBoost 構造函數'); ContainerCoordinator(_boostChannel); }
跟隨着ContainerCoordinator去看看,ContainerCoordinator和BoostChannel如何一塊兒來維護通訊通道,ContainerCoordinator構造函數以下
ContainerCoordinator(BoostChannel channel) { assert(_instance == null); _instance = this; channel.addEventListener("lifecycle", (String name, Map arguments) => _onChannelEvent(arguments)); channel.addMethodHandler((MethodCall call) => _onMethodCall(call)); }
增長native生命週期監聽,增長方法監聽,再去看看_onChannelEvent和_onMethodCall方法就應該清楚ContainerCoordinator其實就是翻譯員,將與原生通訊的協議進行解釋翻譯,根據傳過來的事件名,方法名 逐一進行須要的框架處理,BoostChannel實際上是聲明通道,將接收和發送功能進行封裝,接收natvie傳來的信息,將從flutter的信息發送到native,固然也作了一部分的框架業務處理,將event和method事件進行區分
分發,我的以爲將該功能直接丟至ContainerCoordinator處理可能更好點,應該是出於爲了區分event和method特地在BoostChannel進行處理
接下來看看ContainerCoordinator對於native傳過來的通訊數據處理,代碼以下,就分爲以前說的event和method兩類,代碼註釋也寫了
/// 對native 整個應用的 生命週期 進行抽象出的幾個行爲事件,讓flutter作相應的處理 /// android端 基本上除了有回退事件的處理,剩餘的生命週期 僅僅是作了監聽沒作任何處理 /// 分別是回退處理 android纔有 /// foreground 本應用是處於前臺 /// background 本應用是處於後臺 /// scheduleFrame 觸發一幀的繪製,但ios和android 都沒找到發送該事件的代碼,老版本遺留代碼? Future<dynamic> _onChannelEvent(dynamic event) { ... } /// 對native view生命週期(在android 就是activity)進行抽象出的 幾個行爲事件, /// 讓flutter作相應的框架處理 Future<dynamic> _onMethodCall(MethodCall call) { ... }
這裏就不講Method的處理邏輯,後面會結合容器部分重點講
接下來再回到main.dart文件,跟隨FlutterBoost.init方法進去看一下,就是初始化BoostContainerManager,再也不深刻,後面會結合起來一塊兒講BoostContainerManager
這模塊代碼比較少,先從這模塊開始講起
咱們知道原生和flutter之間的通訊就是經過MethodChannel這個類實現的(原生和flutter的類名同樣),前面有講flutter的boost_channel.dart的做用,native的BoostChannel其實也同樣,將接收和發送功能進行封裝,接收flutter傳來的信息,將從native的信息發送到flutter
前面原生初始化 講到插件的註冊是經過反射實現的,GeneratedPluginRegistrant.java當中的registerWith方法咱們接下去看一下,註冊的時候作了哪些事,路徑lutter_boost/android/src/main/java/com/idlefish/flutterboost/BoostChannel.java
public static void registerWith(PluginRegistry.Registrar registrar) { sInstance = new BoostChannel(registrar); //通道註冊後,處理flutter的method 調用處理 for(ActionAfterRegistered a : sActions) { a.onChannelRegistered(sInstance); } //狀態監聽 回調 if(FlutterBoost.sInstance != null) { final IStateListener stateListener = FlutterBoost.sInstance.mStateListener; if (stateListener != null) { stateListener.onChannelRegistered(registrar, sInstance); } } sActions.clear(); }
看到了吧,BoostChannel的實例化是在插件註冊的時候進行的,繼續深刻,以下代碼
private BoostChannel(PluginRegistry.Registrar registrar){ mMethodChannel = new MethodChannel(registrar.messenger(), "flutter_boost"); mMethodChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() { @Override public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { if (methodCall.method.equals("__event__")) { String name = methodCall.argument("name"); Map args = methodCall.argument("arguments"); Object[] listeners = null; synchronized (mEventListeners) { Set<EventListener> set = mEventListeners.get(name); if (set != null) { listeners = set.toArray(); } } if(listeners != null) { for(Object o:listeners) { ((EventListener)o).onEvent(name,args); } } }else{ Object[] handlers; synchronized (mMethodCallHandlers) { handlers = mMethodCallHandlers.toArray(); } for(Object o:handlers) { ((MethodChannel.MethodCallHandler)o).onMethodCall(methodCall,result); } } } }); }
對於通道上的數據分爲兩類event和method,都是和flutter一一對應的,前面flutter初始化中,也講過,在原生這邊event 沒有框架上的業務處理,但提供了回調,根據本身的業務是否須要增長監聽
method的處理,去查看BoostMethodHandler,FlutterBoost.java做爲內部類存在,以下
class BoostMethodHandler implements MethodChannel.MethodCallHandler { @Override public void onMethodCall(MethodCall methodCall, final MethodChannel.Result result) { switch (methodCall.method) { case "pageOnStart": ... break; case "openPage": ... break; case "closePage": ... break; case "onShownContainerChanged": ... break; default: { result.notImplemented(); } } } }
看這些switch的case處理,大概也猜出來是幹嗎的了,是flutter通知native的頁面行爲事件,例openPage,closePage等等 ,在初始化中,講了挺多flutter的chanell,因此這裏就不在講了,可是講講兩邊的設計
兩邊都有個channel類,主要都是用來接收和發送消息的
flutter專門有一個類ContainerCoordinator.dart,中文翻譯過來就是集裝箱協調員,用於通訊事件的統一處理,就是將從原生接收到的信息進行處理,可是在原生那邊並無相似的類,而是將這個工做放在FlutterBoost.java這個內部類中,我的以爲爲了保持統一能夠專門抽象出個類,將該功能放置該類中,放在FlutterBoost.java不能保持高度統一且不雅觀吧
講到這裏,其實通道的設計你們應該理解得差很少了(解決開頭提出的問題)
native和flutter的channel的通道是如何設計的?
閒魚的棧管理方案,是將棧的管理都放置原生,因此在原生必須暴露棧的管理,讓項目接入方能在原有棧的解決方案上 融合進閒魚的棧管理方案,因此頁面的打開就是入口處,從該入口處去查看容器的設計,先從demo中的PageRouter.java看起,以下代碼
public class PageRouter { public static final String NATIVE_PAGE_URL = "sample://nativePage"; public static final String FLUTTER_PAGE_URL = "sample://flutterPage"; public static final String FLUTTER_FRAGMENT_PAGE_URL = "sample://flutterFragmentPage"; public static boolean openPageByUrl(Context context, String url,Map params) { return openPageByUrl(context, url,params, 0); } public static boolean openPageByUrl(Context context, String url, Map params, int requestCode) { try { if (url.startsWith(FLUTTER_PAGE_URL)) { context.startActivity(new Intent(context, FlutterPageActivity.class)); return true; } else if (url.startsWith(FLUTTER_FRAGMENT_PAGE_URL)) { context.startActivity(new Intent(context, FlutterFragmentPageActivity.class)); return true; } else if (url.startsWith(NATIVE_PAGE_URL)) { context.startActivity(new Intent(context, NativePageActivity.class)); return true; } else { return false; } } catch (Throwable t) { return false; } } }
若是你們用過阿里的Aroute路由框架,就會以爲很親切,將每一個View配置一個路由,仍是前端的思想借鑑過來,一個統一的界面打開處,根據路由路徑,判斷是原生view仍是FlutterView,分別打開不一樣的Activity
接入方,在這裏能夠根據自身的原生棧管理再進行抽象封裝就ok了
接下來看看哪裏調用了openPageByUrl(注意是下面那個)方法,發現正是咱們一開始框架初始化的時候在調用,以下,文件路徑flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/MyApplication.java
@Override public void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode, Map<String, Object> exts) { PageRouter.openPageByUrl(context, url, urlParams, requestCode); }
再查看是哪裏調用了該方法,一直追蹤到FlutterBoost.java,關鍵代碼以下
case "openPage": { try { Map<String,Object> params = methodCall.argument("urlParams"); Map<String,Object> exts = methodCall.argument("exts"); String url = methodCall.argument("url"); mManager.openContainer(url, params, exts, new FlutterViewContainerManager.OnResult() { @Override public void onResult(Map<String, Object> rlt) { if (result != null) { result.success(rlt); } } }); }catch (Throwable t){ result.error("open page error",t.getMessage(),t); } }
Flutter 經過channel通道 通知原生 要打開一個新的頁面,而後原生將自身的生命週期經過通道告知flutter,flutter再進行相應的頁面處理,雖然短短一句話,但其中的邏輯及代碼量仍是不少的...
前面已經講了通道部分,這裏再貼點關鍵代碼,原生將自身的生命週期經過通道告知flutter,關鍵代碼以下
private class MethodChannelProxy { private int mState = STATE_UNKNOW; private void create() { ... } private void appear() { ... } private void disappear() { ... } } private void destroy() { .. } public void invokeChannel(String method, String url, Map params, String uniqueId) { ... } public void invokeChannelUnsafe(String method, String url, Map params, String uniqueId) { .. } } public static String genUniqueId(Object obj) { return System.currentTimeMillis() + "-" + obj.hashCode(); } }
ok,如今已經找到了MethodChannelProxy類,那咱們就繼續回找(注意我這裏講解都是從冰山一角再慢慢往上查,最終再將冰山一塊兒探索完畢)MethodChannelProxy是做爲ContainerRecord.java的內部類存在。接下來咱們來看ContainerRecord類,其實現了IContainerRecord接口,再繼續深究找到IOperateSyncer接口,代碼以下
public interface IOperateSyncer { void onCreate(); void onAppear(); void onDisappear(); void onDestroy(); void onBackPressed(); void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults); void onNewIntent(Intent intent); void onActivityResult(int requestCode, int resultCode, Intent data); void onContainerResult(int requestCode, int resultCode, Map<String,Object> result); void onUserLeaveHint(); void onTrimMemory(int level); void onLowMemory(); }
該接口是經過對原生的生命週期再結合flutter的生命週期特點及android自身的特性(有回退物理鍵)抽象出來的,繼續回到接下來咱們來看ContainerRecord類,發現MethodChannelProxy類其實就是作個代理功能,看名字也清楚,在接口方法被調用的時候,通知flutter
接下來看看IContainerRecord的方法被調用處,追蹤到BoostFlutterActivity.java和BoostFlutterFragment.java,這裏咱們只看Activity,Fragment基本差很少。咱們看到BoostFlutterActivity在走onCreate生命週期方法時,建立了mSyncer,關鍵代碼以下
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); configureWindowForTransparency(); mSyncer = FlutterBoost.singleton().containerManager().generateSyncer(this); mFlutterEngine = createFlutterEngine(); mFlutterView = createFlutterView(mFlutterEngine); setContentView(mFlutterView); mSyncer.onCreate(); configureStatusBarForFullscreenFlutterExperience(); }
FlutterBoost.singleton().containerManager().generateSyncer() 繼續深刻,追蹤到FlutterViewContainerManager類,關鍵代碼以下
@Override public IOperateSyncer generateSyncer(IFlutterViewContainer container) { Utils.assertCallOnMainThread(); //建立容器記錄實例 ContainerRecord record = new ContainerRecord(this, container); if (mRecordMap.put(container, record) != null) { Debuger.exception("container:" + container.getContainerUrl() + " already exists!"); } mRefs.add(new ContainerRef(record.uniqueId(),container)); //講接口引用返回 return record; }
ContainerRecord實例在這裏建立,同時將接口引用返給Activity,這樣就和原生View的生命週期關聯起來了
注意到這裏咱們已經追蹤到FlutterViewContainerManager.java類,已經能夠從上帝視角去看了。
剛剛從容器打開出入一直追蹤到FlutterViewContainerManager.java類,該類看名字就清楚就是容器的管理者,容器建立、打開、關閉、銷燬、彈出、移除等等工做都是在這兒,這裏最關鍵的generateSyncer方法剛剛追蹤的時候已經講過。這裏再重點講講該類的setContainerResult方法,以下
void setContainerResult(IContainerRecord record,int requestCode, int resultCode, Map<String,Object> result) { IFlutterViewContainer target = findContainerById(record.uniqueId()); if(target == null) { Debuger.exception("setContainerResult error, url="+record.getContainer().getContainerUrl()); } if (result == null) { result = new HashMap<>(); } result.put("_requestCode__",requestCode); result.put("_resultCode__",resultCode); final OnResult onResult = mOnResults.remove(record.uniqueId()); if(onResult != null) { onResult.onResult(result); } }
單獨拎出來說,主要是本人好奇 目標頁 向 起始面 如何傳輸數據的,
在純ntive就是靠着onActivityResult回調拿到目標頁傳回的數據,該方法就是處理目標頁傳回來後的處理
在混合棧中 就分爲3種狀況
1.native-flutter
2.flutter-native
3.flutter-flutter
native和ntive就不用說了,都用不到該框架
第一種狀況:native-flutter
demo自身當中並無相關的演示代碼,因而我按照原生是如何接受傳回來的數據去進行更改,改了兩處以下,類路徑flutterProject/flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/MainActivity.java
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
data.getSerializableExtra(IFlutterViewContainer.RESULT_KEY);
Debuger.log("MainActivityResult"+data.getSerializableExtra(IFlutterViewContainer.RESULT_KEY).toString());
}
還有一處,類路徑flutterProject/flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/PageRouter.java
以下
public static boolean openPageByUrl(Context context, String url, Map params, int requestCode) { try { if (url.startsWith(FLUTTER_PAGE_URL)) { //接受目標頁的回傳必須經過startActivityForResult進行打開 ((Activity)context).startActivityForResult(new Intent(context, FlutterPageActivity.class),requestCode); return true; } else if (url.startsWith(FLUTTER_FRAGMENT_PAGE_URL)) { context.startActivity(new Intent(context, FlutterFragmentPageActivity.class)); return true; } else if (url.startsWith(NATIVE_PAGE_URL)) { context.startActivity(new Intent(context, NativePageActivity.class)); return true; } else { // context.startActivity(new Intent(context, FlutterTwoPageActivity.class)); return false; } } catch (Throwable t) { return false; } }
還有記得修改調起的Flutter頁面是'second',由於demo中只有它纔有傳回數據,實現原理這裏我簡單描述,不詳細講了,就是flutter在關閉頁面的時候,傳回數據,以下
class SecondRouteWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("Second Route"), ), body: Center( child: RaisedButton( onPressed: () { // Navigate back to first route when tapped. BoostContainerSettings settings = BoostContainer.of(context).settings; FlutterBoost.singleton.close(settings.uniqueId, result: {"result": "data from second"}); }, child: Text('Go back with result!'), ), ), ); } }
跟蹤關閉代碼邏輯,經過以前創建的通訊通道,傳過去相關方法,即closePage,原生接收到以後的邏輯代碼處理以下(類文件路徑flutterProject/flutter_boost/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java):
case "closePage": { try { String uniqueId = methodCall.argument("uniqueId"); Map<String,Object> resultData = methodCall.argument("result"); Map<String,Object> exts = methodCall.argument("exts"); mManager.closeContainer(uniqueId, resultData,exts); result.success(true); }catch (Throwable t){ result.error("close page error",t.getMessage(),t); } }
追蹤closeContainer,一直追蹤到BoostFlutterActivity.java的finishContainer方法,以下
@Override public void finishContainer(Map<String,Object> result) { if(result != null) { FlutterBoost.setBoostResult(this,new HashMap<>(result)); finish(); }else{ finish(); } }
再跟蹤下去,邏輯很明朗了就不詳細講了
第二種狀況:flutter-native
閒魚的混合棧方案裏,每一個flutter都有本身的獨立原生宿主View,因此回調也得依賴原生
原生咱們知道生命週期裏就有回調方法,即onActivityResult方法,可是Flutter並無該方法,閒魚的混合框架裏也並無專門把這個生命週期給抽出來,本人更傾向於把這個給抽出來,這樣框架也比較清晰。不過如今不少原生業務都已經不多用這種方式進行頁面傳值,由於業務複雜起來,用這種方式反而更麻煩,因此原生就出現了不少eventBus相似的通訊框架,因此設計混合棧框架的時候,就直接忽略,而直接用自帶的flutter api來實現該功能,怎麼實現的?繼續看
先看下invokeMethod這個方法,原生和flutter都會有個回調函數,flutter頁面拿到目標頁的數據傳回就是採用該方法,接下來我們去看flutter頁面打開native頁面開始看起,類路徑flutterProject/flutter_boost/example/lib/simple_page_widgets.dart,關鍵代碼以下:
InkWell( child: Container( padding: const EdgeInsets.all(8.0), margin: const EdgeInsets.all(8.0), color: Colors.yellow, child: Text( 'open native page', style: TextStyle(fontSize: 22.0, color: Colors.black), )), ///後面的參數會在native的IPlatform.startActivity方法回調中拼接到url的query部分。 ///例如:sample://nativePage?aaa=bbb onTap: () => FlutterBoost.singleton.open("sample://nativePage", urlParams: { "query": {"aaa": "bbb"} }).then((Map value) { print( "call me when page is finished. did recieve second route result $value"); }), )
FlutterBoost.singleton.open 跟蹤下去,發現最終調用的就是invokeMethod,
以下
Future<Map<dynamic,dynamic>> open(String url,{Map<dynamic,dynamic> urlParams,Map<dynamic,dynamic> exts}){ Map<dynamic, dynamic> properties = new Map<dynamic, dynamic>(); properties["url"] = url; properties["urlParams"] = urlParams; properties["exts"] = exts; return channel.invokeMethod<Map<dynamic,dynamic>>( 'openPage', properties); }
而後返回的Future,異步的回調函數,拿到原生頁面的回傳數據。這裏的邏輯很簡單,重點是原生那邊怎麼保存該回調,而後在關閉容器的時候進行調用 回調函數以此將數據傳給Flutter
接下來看原生對於openPage的處理,以前在講通道的時候提過,類路徑
flutterProject/flutter_boost/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java,代碼以下
case "openPage": { try { Map<String,Object> params = methodCall.argument("urlParams"); Map<String,Object> exts = methodCall.argument("exts"); String url = methodCall.argument("url"); mManager.openContainer(url, params, exts, new FlutterViewContainerManager.OnResult() { @Override public void onResult(Map<String, Object> rlt) { if (result != null) { result.success(rlt); } } }); }catch (Throwable t){ result.error("open page error",t.getMessage(),t); } } break;
重點看 mManager.openContainer方法,傳入了一個回調函數,最後調用
result.success(rlt);,數據就傳回flutter頁面,接下來咱們跟蹤去看看如何去保存該回調並 最終調用回調
繼續跟蹤openContainer方法,代碼以下:
void openContainer(String url, Map<String, Object> urlParams, Map<String, Object> exts,OnResult onResult) { Context context = FlutterBoost.singleton().currentActivity(); if(context == null) { context = FlutterBoost.singleton().platform().getApplication(); } if(urlParams == null) { urlParams = new HashMap<>(); } int requestCode = 0; final Object v = urlParams.remove("requestCode"); if(v != null) { requestCode = Integer.valueOf(String.valueOf(v)); } final String uniqueId = ContainerRecord.genUniqueId(url); urlParams.put(IContainerRecord.UNIQ_KEY,uniqueId); if(onResult != null) { mOnResults.put(uniqueId,onResult); } FlutterBoost.singleton().platform().openContainer(context,url,urlParams,requestCode,exts); }
注意 這裏有個mOnResults的Map類型參數,就是保存回調函數,經過每一個Flutter頁面容器的uniqueId作key(只有Flutter頁面會創建容器),但前提是該起始容器打開的時候必須傳入Key,否則就沒法回調,由於找不到該回調了。這就會出現一個問題,就是咱們第一個打開的Flutter頁面並非經過onePage打開的,而是直接經過Context.startActivity方法打開,那麼就不會保存該回調,也就沒法將目標頁的數據傳回起始頁了,已經反饋給閒魚官方了,本人想過幾種方式,爲了這個簡單的功能,就破壞總體框架得不償失,等閒魚官方更優雅的解決方式吧
繼續這個mOnResults這個參數,驗證咱們的猜測,看看哪裏在使用,剛剛只是寫入回調函數,就找到setContainerResult這個方法,就回到剛剛說要重點講的方法那了,關鍵代碼以下:
void setContainerResult(IContainerRecord record,int requestCode, int resultCode, Map<String,Object> result) { .... final OnResult onResult = mOnResults.remove(record.uniqueId()); Debuger.log("setContainerResult uniqueId "+record.uniqueId()); if(onResult != null) { Debuger.log("onResult has result"); onResult.onResult(result); } }
看看哪裏有調用這個方法,發現有兩處,一處就是原生的生命週期onDestroy的時候,代碼以下:
@Override public void onDestroy() { ... mManager.setContainerResult(this,-1,-1,null); ... }
這個是當前頁面銷燬的時候,但並無數據傳回,明顯不是
ok,繼續看另外追蹤後的一處關鍵代碼,
代碼以下:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { ... mSyncer.onContainerResult(requestCode,resultCode,result); }
ok,Flutter起始頁拿到native傳回的數據
第三種狀況:flutter-flutter
這裏不細講了,由於就是第一種和第二種的邏輯區分,無非目標頁不太同樣,傳值就是第一種狀況的邏輯,拿值就是第二種狀況的邏輯
原生關於容器部分,再講下IFlutterViewContainer.java和IContainerRecord.java 這兩個類,由於容器部分主要就是圍繞 這兩個抽象出來的接口進行一系列的框架實現,這裏面作了至關多的抽象,IContainerRecord.java比較好理解,對原生View 生命週期的部分方法抽象,例:onCreate方法,通知flutter頁面能夠作一些初始化工做(這裏面就涉及到flutter容器部分了),還有引擎部分的部分方法抽象等
IFlutterViewContainer.java這個類主要是用於業務代碼使用的,你能夠看它的實現類都是在demo當中,而後抽象出的方法都是傳參,傳路由路徑,容器關閉時的參數回傳等等
好了原生容器的講解就到此,應該仍是遺漏了很多細節的地方,本人以爲好理解就直接過去了
講這一部分以前,咱們得先了解個flutter的一個widget 叫作Overlay!
瞭解這玩意,就能弄清楚混合棧是如何作flutter頁面的棧,這個組件最大的特色就是提供了動態的在Flutter的渲染樹上插入佈局的特性。那豈不是很適合Toast這樣的場景? 是的去Google下,發現的全是用Overlay來作Toast功能
基於Overlay的特性,就能夠用全屏非透明的Overlay,每增長一個flutter頁面就增長一個包含自定義的Widget的OverlayEntry,而後覆蓋在上一個OverlayEntry上,用戶反正看到的只是覆蓋在最頂層的OverlayEntry,若是還不能理解能夠看看這篇文章
ok,背景交代完畢,如今要去看閒魚如何設計的這個容器及頁面棧,咱們就從打開第一個flutter頁面做爲入口開始看起。類路徑:flutterProject/flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/PageRouter.java,第一個打開的Flutter頁面是FlutterPageActivity.java,前面在講通道設計的時候,講到過原生生命週期和Flutter生命週期的綁定,提到過一個抽象出來的接口IOperateSyncer.java,先從onCreate方法開始看起,通過生命週期綁定調用,生成原生容器,提取定義好的通道參數,而後通過通道通訊,最後追蹤到flutter的didInitPageContainer的方法處理
(類路徑flutterProject/flutter_boost/lib/container/container_coordinator.dart)
繼續跟蹤,中間會通過生命週期的監聽調用,最終調用_createContainerSettings方法
(類路徑flutterProject/flutter_boost/lib/container/container_coordinator.dart),代碼以下
BoostContainerSettings _createContainerSettings( String name, Map params, String pageId) { Widget page; final BoostContainerSettings routeSettings = BoostContainerSettings( uniqueId: pageId, name: name, params: params, builder: (BuildContext ctx) { //Try to build a page using keyed builder. if (_pageBuilders[name] != null) { page = _pageBuilders[name](name, params, pageId); } //Build a page using default builder. if (page == null && _defaultPageBuilder != null) { page = _defaultPageBuilder(name, params, pageId); } assert(page != null); Logger.log('build widget:$page for page:$name($pageId)'); return page; }); return routeSettings; }
根據方法,再過一遍代碼,就是flutter頁面容器的參數配置,同時找到一開始註冊好的page頁面
接下來跟蹤原生的appear方法,一樣的一陣信號傳輸...,最終進入flutter的ContainerCoordinator.dart類中的didShowPageContainer方法,繼續跟蹤,追蹤到flutter_boost/lib/container/container_coordinator.dart的showContainer方法
注意的是 flutter容器初始化的過程當中作了不少兼容工做,兼容ios兼容android,畢竟兩個平臺的生命週期是有所差異,但最終要抽象成同樣的生命週期,因此要作很多的兼容工做,例如連續2次(didInitPageContainer和didShowPageContainer)進行初始化flutter容器參數
繼續看showContainer方法,代碼以下
void showContainer(BoostContainerSettings settings) { if (settings.uniqueId == _onstage.settings.uniqueId) { _onShownContainerChanged(null, settings.uniqueId); return; } final int index = _offstage.indexWhere((BoostContainer container) => container.settings.uniqueId == settings.uniqueId); //頁面的從新顯示 if (index > -1) { _offstage.add(_onstage); _onstage = _offstage.removeAt(index); setState(() {}); for (BoostContainerObserver observer in FlutterBoost .singleton.observersHolder .observersOf<BoostContainerObserver>()) { observer(ContainerOperation.Onstage, _onstage.settings); } Logger.log('ContainerObserver#2 didOnstage'); } else { //push flutter棧 pushContainer(settings); } }
這裏的邏輯很簡單,重點看下pushContainer方法,代碼以下
void pushContainer(BoostContainerSettings settings) { assert(settings.uniqueId != _onstage.settings.uniqueId); assert(_offstage.every((BoostContainer container) => container.settings.uniqueId != settings.uniqueId)); //將當前頁面的add _offstage.add(_onstage); //須要push的頁面容器建立 _onstage = BoostContainer.obtain(widget.initNavigator, settings); setState(() {}); //觀察者回調 for (BoostContainerObserver observer in FlutterBoost .singleton.observersHolder .observersOf<BoostContainerObserver>()) { observer(ContainerOperation.Push, _onstage.settings); } Logger.log('ContainerObserver#2 didPush'); }
flutter的容器的建立,調用setState方法,跟隨進去,發現一個東西,通常flutter頁面開發都用不着,就是SchedulerBinding,這裏有個文章介紹,這裏我簡單講解下,咱們能夠想一想flutter的啓動流程中,確定是有個調度節點,例如:Widget何時處理build,何時處理動畫計算等,就是調度。咱們若是要寫框架,確定是要對flutter的調度 得清楚,這樣才能寫出閒魚這樣的混合棧方案,代碼以下
@override void setState(VoidCallback fn) { Logger.log('BoostContainerManager setState'); if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) { //主要在下一幀以前,作一些清理工做或者準備工做 SchedulerBinding.instance.addPostFrameCallback((Duration duration) { Logger.log('BoostContainerManager persistentCallbacks'); _refreshOverlayEntries(); }); } else { Logger.log('BoostContainerManager '+SchedulerBinding.instance.schedulerPhase.toString()); _refreshOverlayEntries(); } fn(); //return super.setState(fn); }
若是當前調度的是SchedulerPhase.persistentCallbacks,那麼就加一個回調,在persistent以後進行調用_refreshOverlayEntries方法,
SchedulerPhase.persistentCallbacks 是處理build/layout/paint工做
能夠這麼理解,SchedulerPhase.persistentCallbacks就是在搭建舞臺,舞臺搭建好了,那麼表演者就能夠上臺表演了 即調用_refreshOverlayEntries方法
繼續查看_refreshOverlayEntries方法,代碼以下
void _refreshOverlayEntries() { final OverlayState overlayState = _overlayKey.currentState; if (overlayState == null) { return; } if (_leastEntries != null && _leastEntries.isNotEmpty) { for (_ContainerOverlayEntry entry in _leastEntries) { entry.remove(); } } final List<BoostContainer> containers = <BoostContainer>[]; containers.addAll(_offstage); assert(_onstage != null, 'Should have a least one BoostContainer'); containers.add(_onstage); //一層層的entry覆蓋上去 _leastEntries = containers .map<_ContainerOverlayEntry>( (BoostContainer container) => _ContainerOverlayEntry(container)) .toList(growable: false); overlayState.insertAll(_leastEntries); SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) { final String now = _onstage.settings.uniqueId; if (_lastShownContainer != now) { final String old = _lastShownContainer; _lastShownContainer = now; _onShownContainerChanged(old, now); } //將焦點切換至當前的BoostContainerState updateFocuse(); }); }
調用OverlayState的insertAll方法,將_leastEntries 覆蓋上去,push flutter頁面的講解就到這人,pop其實也同樣,將當前的頁面棧彈出,固然也有特殊的業務處理,例如非當前的棧彈出,而是某個flutter頁彈出,這裏就不細講,邏輯仍是比較清晰好理解
其實本人在看完flutter的源碼以後,對於BoostContainer.dart比較有疑問,實際上是對其背後的對於Navigator和Overlay有疑問,BoostContainer要繼承的是Navigator,這明明是個導航控制器,其實剛剛給出的文章裏面講得很是通俗易懂了。我本身疑問的緣由主要是認爲一個flutter app應該就只有一個Navigator,其實主要是flutter業務開發作多了而進去的誤區。閒魚的混合棧中的flutter頁面棧管理就跟日常的flutter頁面棧很不同。其實最好的理解方式,本身寫一個最簡單的相似的flutter頁面管理,而後再看那篇文章,就豁然開朗了。
容器講解就到此了,解決疑問中的第一個問題
第一個:容器是怎麼設計的?
適配層 只有原生才須要作相應的工做,看以前,想一想若是要作適配層,要作哪些適配?
作過flutter業務開發,確定在軟鍵盤上面花過很多心思去作相應的界面適配工做~
的確,看原生代碼裏就有個XInputConnectionAdaptor.java的類,其實要看適配層,要花很多精力的,要弄清楚flutter的啓動流程,而後重寫FlutterView即XFlutterView,其實跟官方提供的FlutterView改動並非不少
遺留問題:
由於 存在第一個打開的Flutter頁面沒法將數據傳回起始頁問題,
後來又去翻了下通道的相關代碼,發現有這麼一個flutter向原生的pageOnStart方法,類路徑flutterProject/flutter_boost/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java,代碼以下
case "pageOnStart": { Map<String, Object> pageInfo = new HashMap<>(); try { IContainerRecord record = mManager.getCurrentTopRecord(); if (record == null) { record = mManager.getLastGenerateRecord(); } if(record != null) { pageInfo.put("name", record.getContainer().getContainerUrl()); pageInfo.put("params", record.getContainer().getContainerUrlParams()); pageInfo.put("uniqueId", record.uniqueId()); } result.success(pageInfo); } catch (Throwable t) { result.error("no flutter page found!",t.getMessage(),t); } } break;
看了下代碼,應該就是第一個flutter頁面的打開邏輯,可是在flutter的demo中並沒發現,多是之前版本留下的