Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: 连续 push 多个页面后返回,栈底页面会 rebuild,页面上的 Image 组件会闪烁一下重新加载图片,体验非常差。 #2034

Closed
lihanst opened this issue May 8, 2024 · 15 comments

Comments

@lihanst
Copy link

lihanst commented May 8, 2024

请描述遇到的问题,以及您所期望的正确的结果

问题:

使用 withContainer: true(这是我们项目的默认配置) 连续 push 多个页面,在返回时,除了倒数第二个页面,前方的页面都会 rebuild,Image 组件会闪烁。
我的 App 的首页切换成了 Flutter,当我们回到首页时,时常会触发这个问题,导致我们的用户体验非常差。

期望结果:

页面不会 rebuild,Image 组件不会重新加载。

请说明如何操作会遇到上述问题

在 example_new_for_ios 中把某个 Tab 换成一个只显示图片的页面,并且多 push 几次这种页面(图片保持不同)。

RPReplay_Final1715161073.-.Compressed.with.FlexClip.mp4

在下面填入关键复现代码

/// 新增这几个页面
 static Map<String, FlutterBoostRouteFactory> routerMap = {
   
    'image1': (settings, uniqueId) {
      return CupertinoPageRoute(
        settings: settings,
        builder: (_) => ImagePage(
          title: 'Image1',
          imageUrl: 'https://www.gran-turismo.com/images/c/i1W3k7FXLZd7AEE.jpg',
          onTap: () {
            BoostNavigator.instance.push('image2', withContainer: true);
          },
        ),
      );
    },
    'image2': (settings, uniqueId) {
      return CupertinoPageRoute(
        settings: settings,
        builder: (_) => ImagePage(
          title: 'Image2',
          imageUrl: 'https://www.gran-turismo.com/images/c/i118nykkHmrKZ8E.jpg',
          onTap: () {
            BoostNavigator.instance.push('image3', withContainer: true);
          },
        ),
      );
    },
    'image3': (settings, uniqueId) {
      return CupertinoPageRoute(
        settings: settings,
        builder: (_) => ImagePage(
          title: 'Image3',
          imageUrl: 'https://www.gran-turismo.com/images/c/i1x3VZnN7fMmEC.jpg',
          onTap: () {
            BoostNavigator.instance.push('image4', withContainer: true);
          },
        ),
      );
    },
    'image4': (settings, uniqueId) {
      return CupertinoPageRoute(
        settings: settings,
        builder: (_) => ImagePage(
          title: 'Image4',
          imageUrl: 'https://www.gran-turismo.com/images/c/i1WgjjhunGvTOO.jpg',
          onTap: () {
            BoostNavigator.instance.pop();
          },
        ),
      );
    },
 'image5': (settings, uniqueId) {
      return CupertinoPageRoute(
        settings: settings,
        builder: (_) => ImagePage(
          title: 'Image5',
          imageUrl: 'https://www.gran-turismo.com/images/c/i1R84xGCzARyJ8E.jpg',
          onTap: () {},
        ),
      );
  };

class ImagePage extends StatelessWidget {
  final String title;
  final String imageUrl;
  final VoidCallback onTap;

  const ImagePage(
      {super.key,
      required this.title,
      required this.imageUrl,
      required this.onTap});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GestureDetector(
          onTap: onTap,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              CachedNetworkImage(
                  imageUrl: imageUrl, width: 1920 / 6, height: 1080 / 6),
              Text(title),
            ],
          ),
        ),
      ),
    );
  }
}
        /// 往 Tab 中添加原生 VC 和 FlutterVC
        //native主页
        let homeViewController = HomeViewController()
        homeViewController.tabBarItem = UITabBarItem(title: "首页", image: nil, tag: 0)
         
        //下面是三个flutter vc
        let fvc1  = FBFlutterViewContainer()!
        fvc1.setName("image1", uniqueId: nil, params: nil, opaque: true)
        fvc1.tabBarItem = UITabBarItem(title: "flutter_image1", image: nil, tag: 1)

        let fvc2  = FBFlutterViewContainer()!
        fvc2.setName("image5", uniqueId: nil, params: nil, opaque: true)
        fvc2.tabBarItem = UITabBarItem(title: "flutter_image5", image: nil, tag: 2)

        let tabBarController = UITabBarController()
        tabBarController.setViewControllers([homeViewController,fvc1,fvc2], animated: false)
       
        let navigationViewController = UINavigationController(rootViewController: tabBarController)
        navigationViewController.navigationBar.isHidden = true
        self.window?.rootViewController = navigationViewController

复现的平台

iOS

Flutter SDK版本

3.13.9

FlutterBoost版本

4.5.6

是否延迟初始化FlutterBoost

No

解决方案

无效的解决方案一

相关 issues: #1219

把 opaque 改为 false 后表现正常,但是会出现其他问题:

  1. FlutterBoost 是使用 overlay 来管理页面的,当显示新的 Flutter 页面后,被移动到下方的页面的状态依旧还是 active,如果下方页面有动图或者轮播图,那么这个页面依旧会触发重绘,从而影响最上方页面的性能。

无效的解决方案二

使用 BoostCacheWidget,经测试无效。

@joechan-cq
Copy link
Collaborator

iOS我并不熟悉,不过Image控件本身是有内存缓存的,如果有内存缓存,就算build应该也不会闪。可以试着增大下PaintingBinding.instance!.imageCache里的缓存设置试试。

另外我记得以前在iOS上,ViewController退出时,会触发Flutter层清理图片内存缓存,不知道这个问题还在不在,你可以关注下ImageCache.clear()方法是否被调用。

@lihanst
Copy link
Author

lihanst commented May 21, 2024

iOS我并不熟悉,不过Image控件本身是有内存缓存的,如果有内存缓存,就算build应该也不会闪。可以试着增大下PaintingBinding.instance!.imageCache里的缓存设置试试。

另外我记得以前在iOS上,ViewController退出时,会触发Flutter层清理图片内存缓存,不知道这个问题还在不在,你可以关注下ImageCache.clear()方法是否被调用。

测试下了,在 ViewController 创建时,ServicesBinding 会收到 systemMessage,type 是 memoryPressure,接着 flutter 侧触发 ImageCache.clear();

看了下 flutter_boost 的创建逻辑,每次创建新的 controller 的时候,flutter_boost 会手动清理 engine.viewController
https://github.com/alibaba/flutter_boost/blob/main/ios/Classes/container/FBFlutterViewContainer.m

而这个逻辑导致了 engine 发送 memoryPressure 通知,
https://github.com/flutter/engine/blob/d5ef1aeed4e295ea7bc06d8e4854b5edb74a1c59/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm#L445

@joechan-cq

@joechan-cq
Copy link
Collaborator

iOS我并不熟悉,不过Image控件本身是有内存缓存的,如果有内存缓存,就算build应该也不会闪。可以试着增大下PaintingBinding.instance!.imageCache里的缓存设置试试。
另外我记得以前在iOS上,ViewController退出时,会触发Flutter层清理图片内存缓存,不知道这个问题还在不在,你可以关注下ImageCache.clear()方法是否被调用。

测试下了,在 ViewController 创建时,ServicesBinding 会收到 systemMessage,type 是 memoryPressure,接着 flutter 侧触发 ImageCache.clear();

看了下 flutter_boost 的创建逻辑,每次创建新的 controller 的时候,flutter_boost 会手动清理 engine.viewController https://github.com/alibaba/flutter_boost/blob/main/ios/Classes/container/FBFlutterViewContainer.m

而这个逻辑导致了 engine 发送 memoryPressure 通知, https://github.com/flutter/engine/blob/d5ef1aeed4e295ea7bc06d8e4854b5edb74a1c59/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm#L445

@joechan-cq

那可以参考下面代码,自己管理内存缓存的清理

class FixImageWidgetBinding extends WidgetsFlutterBinding {

  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      FixImageWidgetBinding();
    return WidgetsBinding.instance!;
  }

  @override
  ImageCache createImageCache() {
    return FixImageCache();
  }
}

class FixImageCache extends ImageCache {

  @override
  void clear() {
    if (Platform.isIOS) {
      //这里如果是iOS系统就不去做释放了
    } else {
      super.clear();
    }
  }

  void clearForIOS() {
    //调用父类的clear方法
    super.clear();
  }
}

main.dart的main函数里,执行一下

FixImageWidgetBinding();

@lihanst
Copy link
Author

lihanst commented May 21, 2024

iOS我并不熟悉,不过Image控件本身是有内存缓存的,如果有内存缓存,就算build应该也不会闪。可以试着增大下PaintingBinding.instance!.imageCache里的缓存设置试试。
另外我记得以前在iOS上,ViewController退出时,会触发Flutter层清理图片内存缓存,不知道这个问题还在不在,你可以关注下ImageCache.clear()方法是否被调用。

测试下了,在 ViewController 创建时,ServicesBinding 会收到 systemMessage,type 是 memoryPressure,接着 flutter 侧触发 ImageCache.clear();
看了下 flutter_boost 的创建逻辑,每次创建新的 controller 的时候,flutter_boost 会手动清理 engine.viewController https://github.com/alibaba/flutter_boost/blob/main/ios/Classes/container/FBFlutterViewContainer.m
而这个逻辑导致了 engine 发送 memoryPressure 通知, https://github.com/flutter/engine/blob/d5ef1aeed4e295ea7bc06d8e4854b5edb74a1c59/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm#L445
@joechan-cq

那可以参考下面代码,自己管理内存缓存的清理

class FixImageWidgetBinding extends WidgetsFlutterBinding {

  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      FixImageWidgetBinding();
    return WidgetsBinding.instance!;
  }

  @override
  ImageCache createImageCache() {
    return FixImageCache();
  }
}

class FixImageCache extends ImageCache {

  @override
  void clear() {
    if (Platform.isIOS) {
      //这里如果是iOS系统就不去做释放了
    } else {
      super.clear();
    }
  }

  void clearForIOS() {
    //调用父类的clear方法
    super.clear();
  }
}

main.dart的main函数里,执行一下

FixImageWidgetBinding();

那真正遇到内存压力的时候,图片反而得不到清理。或者我要写一些逻辑来判断这个 memoryPressure 是不是跟页面的 push 有关。

所以我的疑问是 flutter_boost 每次都清理 engine.viewController 是不是必须的?

@joechan-cq
Copy link
Collaborator

我记得没错的话,iOS触发清理内存缓存这个逻辑,并不是FlutterBoost的本意,而是Flutter iOS SDK的内部设计。FlutterBoost切换ViewController时的逻辑在我看来是没有问题的,只是因为Flutter iOS SDK内部的逻辑,导致执行了内存清理。

@lihanst
Copy link
Author

lihanst commented May 21, 2024

我记得没错的话,iOS触发清理内存缓存这个逻辑,并不是FlutterBoost的本意,而是Flutter iOS SDK的内部设计。FlutterBoost切换ViewController时的逻辑在我看来是没有问题的,只是因为Flutter iOS SDK内部的逻辑,导致执行了内存清理。

在 Flutter 的单引擎设计下, 置空 viewController 就清理内存也许是合理的。
我疑惑的是 FlutterBoost 置空 viewController 的设计目的是什么?不能直接设置新的 viewController 上去么?在多 Tab 场景下,就是直接设置新的 viewController。

@joechan-cq
Copy link
Collaborator

可能是为了防止内存泄漏吧。直接设置看着也没啥问题。

@lipopli
Copy link

lipopli commented May 22, 2024

iOS我并不熟悉,不过Image控件本身是有内存缓存的,如果有内存缓存,就算build应该也不会闪。可以试着增大下PaintingBinding.instance!.imageCache里的缓存设置试试。
另外我记得以前在iOS上,ViewController退出时,会触发Flutter层清理图片内存缓存,不知道这个问题还在不在,你可以关注下ImageCache.clear()方法是否被调用。

测试下了,在 ViewController 创建时,ServicesBinding 会收到 systemMessage,type 是 memoryPressure,接着 flutter 侧触发 ImageCache.clear();
看了下 flutter_boost 的创建逻辑,每次创建新的 controller 的时候,flutter_boost 会手动清理 engine.viewController https://github.com/alibaba/flutter_boost/blob/main/ios/Classes/container/FBFlutterViewContainer.m
而这个逻辑导致了 engine 发送 memoryPressure 通知, https://github.com/flutter/engine/blob/d5ef1aeed4e295ea7bc06d8e4854b5edb74a1c59/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm#L445
@joechan-cq

那可以参考下面代码,自己管理内存缓存的清理

class FixImageWidgetBinding extends WidgetsFlutterBinding {

  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      FixImageWidgetBinding();
    return WidgetsBinding.instance!;
  }

  @override
  ImageCache createImageCache() {
    return FixImageCache();
  }
}

class FixImageCache extends ImageCache {

  @override
  void clear() {
    if (Platform.isIOS) {
      //这里如果是iOS系统就不去做释放了
    } else {
      super.clear();
    }
  }

  void clearForIOS() {
    //调用父类的clear方法
    super.clear();
  }
}

main.dart的main函数里,执行一下

FixImageWidgetBinding();

那真正遇到内存压力的时候,图片反而得不到清理。或者我要写一些逻辑来判断这个 memoryPressure 是不是跟页面的 push 有关。

所以我的疑问是 flutter_boost 每次都清理 engine.viewController 是不是必须的?

目前我也遇到这个问题,用这个方法可以解决图片闪烁的问题,这样写会不会导致内存占用越来越大?

@joechan-cq
Copy link
Collaborator

内存缓存上限还是会受ImageCache中的策略控制,只是原本iOS在内存不够时会通知Flutter清理内存缓存的逻辑没了。这个可以自己在iOS Native层实现后另外定义消息通知到Flutter,然后调用clearForIOS进行清理。

@lipopli
Copy link

lipopli commented May 23, 2024

iOS在内存不够时

func applicationDidReceiveMemoryWarning(_ application: UIApplication) 在iOS原生AppDelegate里的这个回调发个通知给fluuter调用clearForIOS吗?

@joechan-cq
Copy link
Collaborator

应该是吧,我并不是iOS开发,所以不清楚iOS Native上的接口。

@joechan-cq
Copy link
Collaborator

joechan-cq commented May 24, 2024

image

Flutter iOS SDK中的FlutterViewController.viewDidDisappear中也会通知Flutter层notifyLowMemory

@lihanst
Copy link
Author

lihanst commented May 24, 2024

@lipopli 我现在尝试把 flutter boost 中所有置空 engine.viewController 的代码去掉了,目前还没发现什么问题。你可以试试

@lipopli
Copy link

lipopli commented May 27, 2024

@lipopli 我现在尝试把 flutter boost 中所有置空 engine.viewController 的代码去掉了,目前还没发现什么问题。你可以试试

这个能提交到flutter_boost吗?还是你把代码放到本地了? 我目前是按照 @joechan-cq 的方案解决的

@lihanst
Copy link
Author

lihanst commented May 27, 2024

@lipopli 我现在尝试把 flutter boost 中所有置空 engine.viewController 的代码去掉了,目前还没发现什么问题。你可以试试

这个能提交到flutter_boost吗?还是你把代码放到本地了? 我目前是按照 @joechan-cq 的方案解决的

你自己 fork 一份代码去改吧。他们都直接把这个 issues 关了,看起来都不想改这问题。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants