alibaba / flutter_boost

FlutterBoost is a Flutter plugin which enables hybrid integration of Flutter for your existing native apps with minimum efforts
https://github.com/alibaba/flutter_boost
MIT License
6.98k stars 1.23k forks source link

Android:FlutterBoostFragment 里面有PlatformView时候,切到另外一个Tab的FlutterBoostFragment 会crash #1755

Open liuyicheng3 opened 2 years ago

liuyicheng3 commented 2 years ago

Steps to Reproduce

1.在 flutterboost example工程 修改 “main dart”, 把tab_message修改调到example工程里面的NativeViewExample页面 `

  tab_message': (settings, uniqueId) {
    return PageRouteBuilder<dynamic>(

      settings: settings,

      pageBuilder: (_, __, ___) =>

          NativeViewExample());

}

`

  1. run example 工程起来 进入 “open flutter fragment page”
  2. 切换到 朋友Tab 必崩

Flutter Boost Version 4.2.0 Target Platform: Android Target OS version/browser: Android 10 Devices: 荣耀畅玩 9A

Logs

java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first. at android.view.ViewGroup.addViewInner(ViewGroup.java:5327) at android.view.ViewGroup.addView(ViewGroup.java:5156) at android.view.ViewGroup.addView(ViewGroup.java:5096) at android.view.ViewGroup.addView(ViewGroup.java:5069) at io.flutter.plugin.platform.PlatformViewsController.attachToView(PlatformViewsController.java:747) at io.flutter.embedding.android.FlutterView.attachToFlutterEngine(FlutterView.java:1215) at io.flutter.embedding.android.FlutterActivityAndFragmentDelegate.onCreateView(FlutterActivityAndFragmentDelegate.java:337) at io.flutter.embedding.android.FlutterFragment.onCreateView(FlutterFragment.java:806) at com.idlefish.flutterboost.containers.FlutterBoostFragment.onCreateView(FlutterBoostFragment.java:92) at com.idlefish.flutterboost.example.tab.FriendFlutterFragment.onCreateView(FriendFlutterFragment.java:19) at androidx.fragment.app.Fragment.performCreateView(Fragment.java:2600) at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:881) at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManagerImpl.java:1238) at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:434) at androidx.fragment.app.FragmentManagerImpl.executeOps(FragmentManagerImpl.java:2079) at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1869) at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1824) at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1727) at androidx.fragment.app.FragmentManagerImpl$2.run(FragmentManagerImpl.java:150) at android.os.Handler.handleCallback(Handler.java:900) at android.os.Handler.dispatchMessage(Handler.java:103) at android.os.Looper.loop(Looper.java:219) at android.app.ActivityThread.main(ActivityThread.java:8349) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)

    • Flutter version 3.3.3 on channel stable at /Users/liuyc/fvm/versions/3.3.3
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 18a827f393 (6 weeks ago), 2022-09-28 10:03:14 -0700
    • Engine revision 5c984c26eb
    • Dart version 2.18.2
    • DevTools version 2.15.0
liuyicheng3 commented 2 years ago

因为detachFromFlutterEngine是FlutterBoostFragment在onCreateView做的。

但是super.onCreateView(inflater, container, savedInstanceState) 里面最终会把当前engine的platformview attach上去的操作(也就导致了第二个tab 错误的把第一个tab的platformview再次attach,所以出问题了) flutterEngine.getPlatformViewsController().attachToView(this)

FlutterBoostFragment

  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            FlutterBoost.instance().getPlugin().onContainerCreated(this);
            View view = super.onCreateView(inflater, container, savedInstanceState);
            flutterView = FlutterBoostUtils.findFlutterView(view);
            // Detach FlutterView from engine before |onResume|.
            flutterView.detachFromFlutterEngine();
            if (DEBUG) Log.d(TAG, "#onCreateView: " + flutterView + ", " + this);
            if (view == flutterView) {
                // fix https://github.com/alibaba/flutter_boost/issues/1732
                FrameLayout frameLayout = new FrameLayout(view.getContext());
                frameLayout.addView(view);
                return frameLayout;
            }
            return view;
        }
joechan-cq commented 2 years ago

目前BoostFragment中的时序如下:

onCreateView(attachEngine -> detachEngine)  -> onResume或者onHiddenChanged (detachEngineIfNeed -> attachEngine)

这个时许中,会出现crash的其实有两个点:

  1. Frgament1 attachEngine后,切换Fragment2,onCreateView中又attachEngine
  2. onCreateView中detachEngine后,onResume中执行detachEngineIfNeed

上述问题的crash点出在1上,解决方法其实就是两次attachEngine中间添加一次detachEngine。但时机不好控制,所以基于目前没有PlatformView的情况下,能够正常运行,那么一个折中的方案就是,在Fragment2 attachEngine之前,把PlatformView从Fragment1的flutterView中移除掉。

但一般无法直接获取到Fragment1中的flutterView,所以这里可以通过继承PlatformViewsController的方法来实现:

public class FlutterBoostPlatformViewsController extends PlatformViewsController {

    public FlutterView mCurrentFlutterView;

    @Override
    public void attachToView(@NonNull FlutterView newFlutterView) {
        super.attachToView(newFlutterView);
        mCurrentFlutterView = newFlutterView;
    }

    @Override
    public void detachFromView() {
        if (mCurrentFlutterView == null) {
            return;
        }
        super.detachFromView();
        mCurrentFlutterView = null;
    }

    public void removePlatformWrapperOrParents() {
        if (mCurrentFlutterView != null) {
            List<View> needRemoveViews = new ArrayList<>();
            int childCount = mCurrentFlutterView.getChildCount();
            for (int i = 0; i < childCount; i++) {
                View view = mCurrentFlutterView.getChildAt(i);
                if (view.getClass().getName().contains("PlatformViewWrapper") || view instanceof FlutterMutatorView) {
                    needRemoveViews.add(view);
                }
            }
            if (!needRemoveViews.isEmpty()) {
                for (View needRemoveView : needRemoveViews) {
                    mCurrentFlutterView.removeView(needRemoveView);
                }
            }
        }
    }
}

然后在构建FlutterEngine的地方使用FlutterBoostPlatformViewsController代替PlatformViewsController

public FlutterEngine provideFlutterEngine(@NonNull Context context) {
    return new FlutterEngine(
            context,
            null,
            null,
            new FlutterBoostPlatformViewsController(),
            null,
            true,
            false);
}

最后在FlutterBoostFragmentonCreateView方法开始调用removePlatformWrapperOrParentsPlatformView给移掉。

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        FlutterEngine flutterEngine = getFlutterEngine();
        if (flutterEngine != null) {
            PlatformViewsController platformViewsController =
                    flutterEngine.getPlatformViewsController();
            if (platformViewsController instanceof FlutterBoostPlatformViewsController) {
                ((FlutterBoostPlatformViewsController) platformViewsController).removePlatformWrapperOrParents();
            }
        }
        ......
    }

这种方法只能解决attachEngine后又attachEngine的crash,运行后就会出现detachEngine后detachEngineIfNeed的crash点,就是个NPE,解决方法也简单,上面的FlutterBoostPlatformViewsController中已经做了保护,detachFromView中加了空判定。

liuyicheng3 commented 2 years ago

为什么不能在FlutterBoostFragment里面的 didFragmentHide把 performDetach加回来?

 protected void didFragmentHide() {
    FlutterBoost.instance().getPlugin().onContainerDisappeared(this);
    // We defer |performDetach| call to new Flutter container's |onResume|;
    // performDetach();
    if (DEBUG) Log.d(TAG, "#didFragmentHide: " + this + ", isOpaque=" + isOpaque());
}
joechan-cq commented 2 years ago

大概是,无法保证多个Fragment的情况下,可见Fragment的onResume方法和需要hide的fragment的didFragmentHide的方法的调用顺序把。

liuyicheng3 commented 2 years ago

确实有这方面的问题。不过如果是在一个FragmentTransaction 顺序 操作多个fragment 就没问题了

marchlqq10 commented 1 year ago
needRemoveViews

这个我试过,可以解决单个fragment的跳转问题。 但是,如果是左右2个fragment,右边去加载新的 flutter,会让左边加载的flutter,view remove掉,导致左边不能滚动

joechan-cq commented 1 year ago
needRemoveViews

这个我试过,可以解决单个fragment的跳转问题。 但是,如果是左右2个fragment,右边去加载新的 flutter,会让左边加载的flutter,view remove掉,导致左边不能滚动

FlutterBoost的目标场景中,应该不包含这种界面上同时显示两个Fragment的场景

crh2017 commented 1 year ago

这样会导致,返回Frgament1 时,platformView不能的触摸响应的

crh2017 commented 1 year ago

补充下,是从Frgament2返回Frgament1

joechan-cq commented 1 year ago

目前FlutterBoost对PlatformView的支持在生命周期上有比较大的问题,最好还是不要用PlatfomrView吧。

crh2017 commented 1 year ago

调试发现返回Frgament1 时,Frgament2对应FlutterBoostActivity在执行onDestroy时,PlatformViewsController又执行一次detach方法,导致platformViewsChannel被置为空,触摸事件没法传递过来 /**

crh2017 commented 1 year ago

这边的临时解决方案 背景: flutter 的activity1 frament1 跳转activity2 frament2,frament1 包含platformView,按照上面老哥解决崩溃问题的基础上,出现activity2 frament2返回activity1 frament1时,platformView触摸事件没有响应。 问题流程:activity2 frament2返回时,在activity1 frament1 已经在PlatformViewsController已经attach,activity2的onDestroy触发flutterboostfragment的onDetach,最终调用了PlatformViewsController的detach,导致platformViewsChannel销毁了,中断了触摸事件的传递。 解决: 把platformViewsChannel重新attach,恢复platformViewsChannel,能让activity1 frament1 的platformView重新获取触摸事件。 实现: activity2的onDestroy时,异步去调用 flutterEngine.getPlatformViewsController().attach(getContextActivity(), flutterEngine.getRenderer(), flutterEngine.getDartExecutor()); 前提是判断frament1处于可见状态,且处于detach状态 兼容: PlatformViewsController在attach时有个判断context是否为空,如下所示: public void attach( @Nullable Context context, @NonNull TextureRegistry textureRegistry, @NonNull DartExecutor dartExecutor) { if (this.context != null) { throw new AssertionError( "A PlatformViewsController can only be attached to a single output target.\n"

说明: 项目比较急,没时间再一一跟进为什么ondestroy为什么调用PlatformViewsController的detach(),把刚刚attach的东西detach掉。 当前测试看起来正常,platformView能正常拿到触摸事件,暂没发现别的问题,如果有发现其他新问题,大家讨论下解决方法。 后续等解决了,再替换官方的方法

ChinaZeng commented 7 months ago

这个官方有后续的安排吗?

wanghuasheng commented 6 months ago

官方应该不跟进了吧,因为咸鱼没这种场景。

OnClickListener2048 commented 6 months ago

FlutterBoostActivity也有类似的问题

joechan-cq commented 6 months ago

这个我一直想改,但没找到通用合适的方法。Flutter SDK版本太多,对PlatformView的修改也是频繁,又都是私有方法和私有变量,所以改起来非常麻烦。

OnClickListener2048 commented 6 months ago

目前我们线上收到了很多FlutterBoostActivity的问题

image

这个问题目前能查到的线索是FlutterBoostActivity使用了translucent模式也就是背景是transparent导致了flutterview渲染模式使用了texture,在activity在后台被杀死重启后 就会出现该问题

joechan-cq commented 6 months ago

结合之前 @crh2017 这位兄弟的办法,我觉得,可以不用去调用PlatformViewsController.detach方法,这样能够使内部的channelHandler继续响应,解决 #1834 里的内存泄漏问题,同时也能使触摸事件正常响应。

public class FBPlatformViewsController extends PlatformViewsController {

    private Context appCtx;

    /**
     * 记录PlatformViewsController绑定使用的FlutterView
     */
    private FlutterView curFlutterView = null;

    /**
     * 占位FlutterView,用于防止不执行完整detach后,内部channelHandler继续响应时,出现空指针异常。
     */
    private FlutterView dummyFlutterView = null;

    public FBPlatformViewsController() {
        super();
    }

    @Override
    public void attach(@Nullable Context context, @NonNull TextureRegistry textureRegistry,
                       @NonNull DartExecutor dartExecutor) {
        if (appCtx == null && context != null) {
            appCtx = context.getApplicationContext();
            dummyFlutterView = new FlutterView(appCtx);
        }
        super.attach(context, textureRegistry, dartExecutor);
    }

    @Override
    public void detach() {
        // 不执行完整的detach,这样就使内部channelHandler正确响应,同时避免platformView触摸事件无法响应
        // super.detach();
        // 使用反射将内部context变量设置为null,一方面解决重新attach时的异常,另一方面解决内存泄漏
        try {
            Field contextF = getClass().getSuperclass().getDeclaredField("context");
            contextF.setAccessible(true);
            contextF.set(this, null);
        } catch (Exception ignore) {
        }
    }

    @Override
    public void attachToView(@NonNull FlutterView newFlutterView) {
        if (curFlutterView == null) {
            super.attachToView(newFlutterView);
            curFlutterView = newFlutterView;
        } else if (newFlutterView != curFlutterView) {
            removePlatformWrapperOrParents();
            super.attachToView(newFlutterView);
            curFlutterView = newFlutterView;
        }
    }

    @Override
    public void detachFromView() {
        if (curFlutterView != null) {
            super.detachFromView();
            curFlutterView = null;
            //将占位FlutterView绑定上去
            attachToView(dummyFlutterView);
        }
    }

    public void removePlatformWrapperOrParents() {
        if (curFlutterView != null) {
            List<View> needRemoveViews = new ArrayList<>();
            int childCount = curFlutterView.getChildCount();
            for (int i = 0; i < childCount; i++) {
                View view = curFlutterView.getChildAt(i);
                if (view.getClass().getName().contains("PlatformViewWrapper") || view instanceof FlutterMutatorView) {
                    needRemoveViews.add(view);
                }
            }
            if (!needRemoveViews.isEmpty()) {
                for (View needRemoveView : needRemoveViews) {
                    curFlutterView.removeView(needRemoveView);
                }
            }
        }
    }
}