设计思想

d_stack对混合界面间的操作做了一层数据抽象,用户直接操作的是抽象层,其行为主要有打开页面和关闭页面两类,对应的数据操作是添加节点数据和删除节点数据,根据添加或者删除的节点信息,去驱动native或者flutter具体页面的跳转行为,核心逻辑是是节点数据管理。在flutter侧的采用单engin复用,从而节约了内存。

设计思想如下图

技术要点

打开/关闭页面 VS 入栈/出栈

打开页面简单一些,无论是push 还是 present一次只会打开一个页面,对于关闭则情况稍微复杂一些,pop/dismiss只关闭当前页面,popTo/popToRoot/popSkip则可能会关闭多个页面或者关闭一个模块,页面是native或者flutter 类型都有可能,直接处理的话比较麻烦,为此进行了抽象,用数组来记录栈的FILO操作,这和页面的打开关闭顺序是一致的。用户根据提供的API进行节点数据的操作,比如打开页面对应的是入栈操作,将要打开的页面抽象为节点,然后节点加入到数组尾部。

核心点:页面操作对应节点信息,打开关闭页面对应节点的入栈出栈。

用户行为管理 VS 节点管理 VS 栈管理

节点的核心四个属性是

target:页面路由
pageType:页面类型
params:页面之间传参
actionType:对应事件类型

用户的行为触发节点管理,节点管理驱动页面跳转(即栈管理),考虑到Android和iOS实现的差异性,节点管理放在了native侧处理。

以下是我们准备的测试场景

以场景四举例,来说明混合栈的流程:F3 popToRoot到第一个native页面,其过程如下

1.用户调用API popToRoot,发送到节点管理。

2.节点管理根据popToRoot,知道将要关闭首页之后的所有页面,通过比对target信息,将将要关闭的页面节点信息取出。再生成两个数组,分别对应将要关闭的native页面和flutter页面。

3.根据这两个数组信息,native和flutter进行页面跳转:native将要关闭2个页面。flutter将要关闭3个页面。

单engin & 单navigator

接上面描述,native和flutter怎么关闭页面呐?d_stack在flutter侧采用的是单navigator。单navigator的前提是单engin,通过复用engin的方式,即节约了内存,又可以统一栈管理,只要处理好混合栈的切换,即UIViewControllerFlutterViewController的交互,单独考虑一侧栈管理是很好处理的。测试场景四中Flutter页面 F3返回到根控制器,需要native将要关闭2个页面。flutter将要关闭3个页面,根据处理好的数据,native的navigator 只需要调用原生popTo到根控制器,而不用管flutter侧怎么实现,同理flutter侧的navigator只需要pop3次即可。

具体怎么实现的单engin,iOS侧是先创建engin,flutter创建的时候关联这个engin,从而达到复用,Android侧是缓存engin,

下面是官方文档:

在 add-to-app 的场景中,例如通过在 Android 上使用 FlutterActivity.withCachedEngine() 方法构建的 Intent,调用 startActivity() 时,或者,在 iOS 上调用 initWithEngine: nibName: bundle:,展示实例化的 FlutterViewController,都会将 FlutterEngine 挂载到 UI 组件。

- (instancetype)init
{
    if(self = [super initWithEngine:[DStack sharedInstance].engine
                            nibName:nil
                             bundle:nil]) {
    }
    return self;
}

拦截 & 手势

d_stack采用抽象节点去管理栈,有一个核心的点是记录全节点信息,无论是ntive节点还是flutter节点。对于用户来说是有可能直接调用原生api的,只要有一个节点信息记录错误就有可能是是之后的页面跳转全乱套。因此我们才用监听系统api来收集节点信息,具体实现详见4 Android和 iOS侧的实现,目的只有一个就是正确的记录全节点信息。在flutter侧则是直接通过api进行记录。

这样就有另一个问题,当用户通过手势返回的时候,不会去调用api,框架的处理是监听gesture事件。

flutter页面动画自定义

当native打开flutter页面时,按照单navigator的设计,会打开一个Flutter容器来实现push效果,同时也会打开目的flutter页面,这就会造成有两个打开动画效果,关闭时也类似。框架层做的是自定义flutter动画,在收到native发过来打开flutter页面指令后,无动画打开页面,其余情况正常逻辑处理,flutter的页面动画是对称的,打开和关闭一致,并且只能存在router中,router又只能在打开新页面时处理,好在关闭当前容器最后一个页面时,会遇到和打开时一样的两个效果,因此只在打开时flutter侧处理一次即可。

路由使用

路由这块原生不用改变,flutter侧按照接入文档注册即可,注册路由和官方基本一致,甚至更简便,不用写传参信息,框架层已经做了处理。获取传参时,也和系统一样,包装在了map中。

final Map args = ModalRoute.of(context).settings.arguments;

详细技术实现

iOS侧原理介绍

iOS侧Controller的节点生成:

要想实现打开一个controller时同步生成一个Node节点,并且不影响外界的正常push和present,我们得拦截controller系统的push、present、pop、dismiss等相关的所有函数。在这些函数里面实现节点的生成和移除,我们使用runtime去hook相应的方法。

SEL initRootViewController = @selector(initWithRootViewController:);
    SEL newInitRootViewController = @selector(d_StackInitWithRootViewController:);
    dStackSelectorSwizzling([self class], initRootViewController, newInitRootViewController);

    SEL push = @selector(pushViewController:animated:);
    SEL newPush = @selector(d_stackPushViewController:animated:);
    dStackSelectorSwizzling([self class], push, newPush);

    SEL pop = @selector(popViewControllerAnimated:);
    SEL newPop = @selector(d_stackPopViewControllerAnimated:);
    dStackSelectorSwizzling([self class], pop, newPop);

    SEL popTo = @selector(popToViewController:animated:);
    SEL newPopTo = @selector(d_stackPopToViewController:animated:);
    dStackSelectorSwizzling([self class], popTo, newPopTo);

    SEL popRoot = @selector(popToRootViewControllerAnimated:);
    SEL newPopRoot = @selector(d_stackPopToRootViewControllerAnimated:);
    dStackSelectorSwizzling([self class], popRoot, newPopRoot);
Flutter侧page节点的生成:

通过channel发消息至Native,Native收到消息,打开一个页面时生成节点,加入到栈管理。

- (void)handleSendNodeToNativeMessage:(FlutterMethodCall*)call result:(FlutterResult)result
{
    DNodePageType pageType = [DNode pageTypeWithString:call.arguments[@"pageType"]];
    DNodeActionType actionType = [DNode actionTypeWithString:call.arguments[@"actionType"]];
    DNode *node = [[DNodeManager sharedInstance] nextPageScheme:call.arguments[@"target"]
                                                       pageType:pageType
                                                         action:actionType
                                                         params:call.arguments[@"params"]];
    node.fromFlutter = YES;
    node.storyboard = call.arguments[@"storyboard"];
    node.identifier = call.arguments[@"identifier"];
    [[DNodeManager sharedInstance] checkNode:node];
    result(@"节点操作完成");
}
拦截手势:

在iOS侧,拦截掉系统的返回手势,在手势响应时,判断当前显示的页面是否为FlutterViewController,从我们的栈管理里面取出当前页面的Node节点,再看前一个节点。如果当前节点是FlutterViewController时, 前一个节点是Native页面的节点,那么是需要响应Native的手势返回的,否则就不响应,把手势的响应留给Flutter侧去处理。其实就是判断当前节点是不是Native和Flutter的临界点。

  适配开源库FDFullScreenGesture,在好多项目中使用了这个三方库实现手势的返回,所以在拦截手势时,也特意的兼容了一下这个框架。思路就是:判断项目中是否引入了FDFullScreenGesture,如果引入了,则hook它实现的gestureRecognizerShouldBegin,如果没有引入FDFullScreenGesture,则我们自身实现手势返回,并监听gestureRecognizerShouldBegin。这里自身实现的手势返回参考的就是FDFullScreenGesture,代码实现和FDFullScreenGesture保持一致。

- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
    // 判断是否需要手势拦截
    UINavigationController *navigationContoller = self.navigationController;
    UIViewController *topViewController = navigationContoller.viewControllers.lastObject;
    CGPoint touchPoint = [gestureRecognizer locationInView:gestureRecognizer.view];
    if (touchPoint.x > gestureRecognizer.view.frame.size.width / 3.0) {
        // 默认不是全屏滑动返回,更接近原生体验
        return NO;
    }
    
    if (self.navigationController.viewControllers.count <= 1) {
        return NO;
    }

    if (topViewController.dStack_interactivePopDisabled) {
        return NO;
    }
    
    if ([[self.navigationController valueForKey:@"_isTransitioning"] boolValue]) {
        return NO;
    }
    CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view];
    if (translation.x <= 0) {
        return NO;
    }
    
    if (navigationContoller == [DActionManager rootController]) {
        UIViewController *rootViewController = navigationContoller.viewControllers.firstObject;
        if (topViewController == rootViewController) {
            if (topViewController.isFlutterViewController) {
                return NO;
            }
        }
    }
    BOOL shouldBegin = YES;
    if (topViewController.isFlutterViewController) {
        // 如果节点列表是空,说明已经在第一页了并且是Flutter的页面,则直接绕过
        shouldBegin = [[DNodeManager sharedInstance] nativePopGestureCanReponse];
    }
    if (shouldBegin) {
        topViewController.isGesturePoped = YES;
    }
    return shouldBegin;
}

Android侧原理介绍

android侧节点管理

a. android侧节点生成和删除

和iOS一样,Android侧节点生成也有两种:

1.是收到flutter侧发送过来的节点信息

private static void handleSendNodeToNative(Map<String, Object> args) {
    if (args == null) {
        return;
    }
    String actionType = (String) args.get("actionType");
    String target = (String) args.get("target");
    String pageType = (String) args.get("pageType");
    Map<String, Object> params = (Map<String, Object>) args.get("params");
    //创建Node节点信息
    DNode node = DNodeManager.getInstance().createNode(
            target,
            DStackActivityManager.getInstance().generateUniqueId(),
            pageType,
            actionType,
            params,
            true);
    DNodeManager.getInstance().checkNode(node);
}

2.native的节点信息,native节点采用监听生命周期回调ActivityLifecycleCallbacks来处理。

节点生成

@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
    activeActivity = activity;
    Activity bottomActivity = DStackActivityManager.getInstance().getBottomActivity();
    appCreate = bottomActivity == null;
    DStackActivityManager.getInstance().addActivity(activity);
    String uniqueId = DStackActivityManager.getInstance().generateUniqueId();
    if (DStackActivityManager.getInstance().isFlutterActivity(activity)) {
        //如果是flutterActivity,那么不记录节点
        //把当前节点的activity赋值
        DNode currentNode = DNodeManager.getInstance().getCurrentNode();
        if (currentNode != null &&
                currentNode.getPageType().equals(DNodePageType.DNodePageTypeFlutter)) {
            currentNode.setActivity(activity);
        }
    } else {
        //当前activity是nativeActivity,记录节点,并且赋值节点的activity
        DNode node = DNodeManager.getInstance().createNode(
                activity.getClass().getName(),
                uniqueId,
                DNodePageType.DNodePageTypeNative,
                DNodeActionType.DNodeActionTypePush,
                null,
                false);
        DNodeManager.getInstance().checkNode(node);
        DNode currentNode = DNodeManager.getInstance().getCurrentNode();
        if (currentNode != null) {
            currentNode.setActivity(activity);
        }
    }
    if (appCreate) {
        PageLifecycleManager.appCreate();
    }
}

节点删除

@Override
public void onActivityDestroyed(@NonNull Activity activity) {
    boolean isPopTo = DStackActivityManager.getInstance().isPopTo();
    DStackActivityManager.getInstance().removeActivity(activity);
    if (DStackActivityManager.getInstance().isFlutterActivity(activity)) {
        return;
    }
    DNode node = DNodeManager.getInstance().createNode(
            activity.getClass().getName(),
            DStackActivityManager.getInstance().generateUniqueId(),
            DNodePageType.DNodePageTypeNative,
            DNodeActionType.DNodeActionTypePop,
            null,
            false);
    node.setPopTo(isPopTo);
    DNodeManager.getInstance().checkNode(node);
}

b. 节点去重

产⽣场景:

Native A — > Flutter B —> Flutter C —> Flutter D —> Native E。

按照这个流程,我们需要⽣生成5个Node节点,Flutter D — >Native E这个流程其 实就是push NativeActivity,因为我们hook了onActivityCreate()方法,这时候我们会在这个⼊口生成节点,有因为我们是从flutter侧收来的节点消息,所以这时候其实会有两个Node节点对应⼀个Native页面,显然是有问题的。所以在DNodeManager里做一个判断,当从flutter侧传来的push节点信息,看被push的页面是否属于NativeActivity,如果是NativeActivity,则不进行入栈管理。

/**
 * 如果node信息来自flutter并且页面类型是native,那么不记录节点,由页面拦截触发
 */
private boolean repeatNode(DNode node) {
    return node.isFromFlutter() &&     node.getPageType().equals(DNodePageType.DNodePageTypeNative);
}

4.2.2 核心类介绍

a. DNodeManager

管理所有节点信息的类,用来负责处理节点的数据处理。

1.入栈

switch (actionType) {
    case DNodeActionType.DNodeActionTypePush:
    case DNodeActionType.DNodeActionTypePresent:
        //打开新页面
        //入栈管理
        //去重逻辑
        DLog.logD("----------push方法开始----------");
        handlePush(node);
        updateNodes();
        setCurrentNodeContainer();
        DActionManager.push(node);
        PageLifecycleManager.pageAppear(node);
        DLog.logD("----------push方法结束----------");
        break;

2.出栈

case DNodeActionType.DNodeActionTypePop:
case DNodeActionType.DNodeActionTypeDissmiss:
    //返回上一个页面
    //出栈管理
    //移除最后一个节点即可
    DLog.logD("----------pop方法开始----------");
    DLog.logD("node出栈,target:" + node.getTarget());
    if (node.isFromFlutter()) {
        if (currentNode != null) {
            //此处是flutter侧点击左上角返回键的逻辑
            //flutter页面触发的pop有可能不带target信息,需要手动添加
            //所有flutter侧页面关闭删除节点的逻辑都在handleNeedRemoveNode实现
            node.setTarget(currentNode.getTarget());
            node.setPageType(currentNode.getPageType());
        }
        DActionManager.pop(node);
    } else {
        //此处是关闭native页面清除节点逻辑
        handleNeedRemoveNativeNode(node);
    }
    DLog.logD("----------pop方法结束----------");
    break;

3.popTo

popTo方法是特殊的出栈方法,当popTo时,在栈管理⾥面,从后往前逆序查到第⼀个和目的页相同的Node,从目的之后的Node,全部出栈。这时需要整理需要出栈的NodeList,把NodeList移交给DActionManager去处理跳转管理。

case DNodeActionType.DNodeActionTypePopTo:
    //返回指定页面
    DLog.logD("----------popTo方法开始----------");
    needRemoveNodes.clear();
    needRemoveNodesIndex.clear();
    needRemoveNodes = needRemoveNodes(node);
    DNode popToNode = getCurrentNode();
    deleteNodes();
    updateNodes();
    DActionManager.popTo(node, needRemoveNodes);
    PageLifecycleManager.pageDisappear(popToNode);
    DLog.logD("----------popTo方法结束----------");
    break;

b. DActionManager

public class DActionManager {

    /**
     * 打开页面
     */
    public static void push(DNode node) {
        enterPageWithNode(node);
    }

    /**
     * 返回当前页面
     */
    public static void pop(DNode node) {
        closePageWithNode(node);
    }

    /**
     * 返回指定页面
     */
    public static void popTo(DNode node, List<DNode> removeNodes) {
        closePageWithNodes(node, removeNodes);
    }

    /**
     * 返回指定模块页面
     */
    public static void popSkip(DNode node, List<DNode> removeNodes) {
        closePageWithNodes(node, removeNodes);
    }

Flutter侧原理介绍

相比于native侧的全节点管理,flutter侧的实现原理较为简单,核心是flutter的节点记录及上报节点到native侧,采用单navigator进行管理,不用考虑flutter页面之间隔着几个native页面,只需要在收到用户触发事件是做相应的flutter页面跳转即可,混合栈页面效果有flutter容器结合route动画实现。

主要有以下几点:

注册PageBuilder

PageBuilder是路由及对应的WidgetBuilder组成,承担了routes的功能,之后flutter间的页面跳转,从注册的PageBuilder Map中取出对应的WidgetBuilder,从而生成相应的页面,不需要在PageBuilder中设置页面传参,在动态生成route的过程中会将参数设置到route的 RouteSettings中去。

navigator封装

扩展了navigator的api,在封装的函数里,增加了上报的数据处理,将flutter节点信息传到native侧进行节点管理。

navigator的跳转分两类,一类是flutter页面触发,这类事件产生的页面跳转由flutter侧进行处理,当然节点也需要进行上报。

在native侧也需要拿到节点信息做一些处理,可以参考native侧收到flutter节点的处理。

另一类是收到native侧的事件,这类事件是由native触发,比如native1想要打开或关闭一个flutter页面,native发送将要操作的节点信息到flutter侧,flutter收到信息后,解析出将要操作的页面信息,做相应的处理即可,代码参见handleActionToFlutter

flutter侧手势管理

flutter侧的手势主要是返回事件,能够监听大手势的地方是MaterialApp的navigatorObservers,相应的手势处理也在这里面,flutter侧手势会触发flutter页面返回,如果是FlutterViewContrller容器的第一个页面,则相应的native也需要处理对问题,比如手势flutter和native手势冲突,flutter容器返回等,详细参见native侧处理。

flutter侧注意点

框架目前没有实现类似native的全局拦截,以及考虑到和native的节点上报时机问题,需要在flutter页面切换的时候,除了手势事件之外的所有页面跳转操作调用DStack的api,api里做了节点上报和route导航功能,使用简便,有丰富的api供使用,可以取代路由框架的功能。

到这里基本d_stack框架介绍完成,如果遇到什么问题或者建议,欢迎一起交流。