解决flutter崩溃问题The host view controller is (null)

bugly后台收到这种崩溃的bug:

#0 Thread NSInternalInconsistencyException The application must have a host view since the keyboard client must be part of the responder chain to function. The host view controller is (null)

flutter release产物,崩溃日志信息就没有那么详细

通过bugly 的跟踪数据模块, 发现崩溃发生时,总是最后出现一个相同的viewcontroller的viewwillappear方法调用。

这样通过这几个页面的导航切换和操作,最后复现出bug。

在连机开发模式下,使用flutter debug版本库,得到了更加具体的崩溃日志:

FlutterTextInputPlugin hostView

css 复制代码
0 CoreFoundation ___exceptionPreprocess + 220
1 libobjc.A.dylib _objc_exception_throw + 60 
2 Foundation __userInfoForFileAndLine
3 Flutter -[FlutterTextInputPlugin hostView] + 144
4 Flutter -[FlutterTextInputPlugin addToInputParentViewIfNeeded:] + 84
5 Flutter -[FlutterTextInputPlugin getOrCreateAutofillableView:isPasswordAutofill:] + 140
6 Flutter -[FlutterTextInputPlugin updateAndShowAutofillViews:focusedField:isPasswordRelated:] + 124
7 Flutter -[FlutterTextInputPlugin setTextInputClient:withConfiguration:] + 192
8 Flutter  -[FlutterTextInputPlugin handleMethodCall:result:] + 292 
9 Flutter __45-[FlutterMethodChannel setMethodCallHandler:]_block_invoke + 112
10 Flutter __ZN7flutter25PlatformMessageHandlerIos21HandlePlatformMessageENSt3__110unique_ptrINS_15Platf

创建FlutterViewContorller控制器

objectivec 复制代码
@interface FBFlutterViewContainer : FlutterViewController<FBFlutterContainer>

- (instancetype)init
{
    ENGINE.viewController = nil;
    if(self = [super initWithEngine:ENGINE
                            nibName:_flbNibName
                             bundle:_flbNibBundle]){
       //这个FlutterViewContoller的初始化会将engine的 viewController 赋值为当前新创建的控制器
                             
        //NOTES:在present页面时,默认是全屏,如此可以触发底层VC的页面事件。否则不会触发而导致异常
        self.modalPresentationStyle = UIModalPresentationFullScreen;
        [self _setup];
    }
    return self;
}

修改engine的viewController

分别在viewDidLoad,viewWillAppear、viewDidAppear3个方法都会调用attatchFlutterEngine重新保存viewContoller控制器对象

ini 复制代码
- (void)attatchFlutterEngine
{
    if(ENGINE.viewController != self){//vc1 push vc2,vc2初始化时候,engine已经替换为vc2,ENGINE.viewController==self是相等的
     //如果是UITabBarController容器下面的 控制器,从tab1下的vc1切换到tab2下的vc2, ENGINE.viewController != self是不等的
        ENGINE.viewController=self;
    }
}

detachFlutterEngineIfNeeded

objectivec 复制代码
- (void)detachFlutterEngineIfNeeded
{
    if (self.engine.viewController == self) {
        //need to call [surfaceUpdated:NO] to detach the view controller's ref from
        //interal engine platformViewController,or dealloc will not be called after controller close.
        //detail:https://github.com/flutter/engine/blob/07e2520d5d8f837da439317adab4ecd7bff2f72d/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm#L529
        [self surfaceUpdated:NO];

        if(ENGINE.viewController != nil) {
            ENGINE.viewController = nil;
        }
    }
}

在 控制器移除时调用 detachFlutterEngineIfNeeded

objectivec 复制代码
- (void)didMoveToParentViewController:(UIViewController *)parent {
    if (!parent) {
        //当VC被移出parent时,就通知flutter层销毁page
        [self detachFlutterEngineIfNeeded];
        [self notifyWillDealloc];
    }
    [super didMoveToParentViewController:parent];
}

- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {

    [super dismissViewControllerAnimated:flag completion:^(){
        if (completion) {
            completion();
        }
        //当VC被dismiss时,就通知flutter层销毁page
        [self detachFlutterEngineIfNeeded];
        [self notifyWillDealloc];
    }];
}

什么情况下 self.engine.viewController == self

flutter页面跳转flutter页面

如果是flutter页面1 跳转flutter 页面2,再从flutter页面2,pop回到flutter 页面1, 因为页面1的viewWillAppear会调用attatchFlutterEngine

ini 复制代码
- (void)attatchFlutterEngine
{
    if(ENGINE.viewController != self){
        ENGINE.viewController=self;
    }
}

所以 页面2 关闭时detachFlutterEngineIfNeeded 中的self.engine.viewController == self是不会成立的。

objectivec 复制代码
- (void)detachFlutterEngineIfNeeded
{
    if (self.engine.viewController == self) {
        //need to call [surfaceUpdated:NO] to detach the view controller's ref from
        //interal engine platformViewController,or dealloc will not be called after controller close.
        //detail:https://github.com/flutter/engine/blob/07e2520d5d8f837da439317adab4ecd7bff2f72d/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm#L529
        [self surfaceUpdated:NO];

        if(ENGINE.viewController != nil) {
            ENGINE.viewController = nil;
        }
    }
}

当flutter页面1 跳转native页面2,native页面2 跳转flutter页面3

从页面3,pop关闭页面3回到native页面2,

因为页面2是一个native页面,他的viewWillAppear不会有 attatchFlutterEngine相关的调用。 页面3关闭时候,调用 detachFlutterEngineIfNeeded,就会使self.engine.viewController == self条件成立。将engine的viewController设置为nil了

这时,在关闭native页面,回到flutter页面1时,flutter页面1的viewWillAppear执行attatchFlutterEngine

ini 复制代码
- (void)attatchFlutterEngine
{
    if(ENGINE.viewController != self){
        ENGINE.viewController=self;
    }
}

将engine的viewController重新设置为当前flutter vc控制器。

flutter engine源代码

github.com/flutter/eng...

objectivec 复制代码
- (UIView*)hostView {
  UIView* host = _viewController.view;
  NSAssert(host != nullptr,
           @"The application must have a host view since the keyboard client "
           @"must be part of the responder chain to function. The host view controller is %@",
           _viewController);
  return host;
}

关于断言:

在 release 环境下,断言通常被用于开发和调试阶段,以便在发现代码中的错误或者不合理条件时,能够更容易地定位问题。然而,在发布版本中,这些断言通常会被编译器优化掉,以避免不必要的运行时开销和可能的崩溃。

NSAssert 在 release 环境下不会导致崩溃,但是你需要确保在生产环境中没有留下不合理的断言条件,以避免潜在的问题。

我们在写代码的时候会写一些断言来发现调试阶段的一些异常情况,但是这些异常情况上线后是不应该展示给用户或者让用户感知到的。

通常使用 NSAssert 或基于它的宏。Xcode 4.2 之后在 release 模式下会自动将所有的。NSAssert 优化掉,也就是说,release 模式下,NSAssert 不会被编译到二进制文件中去。

那么这里为什么还crash了呢。搜索了一下engine的源码也没有其他地方有"must be part of the responder chain to function. The host view controller is"之类的代码。

这是因为我们用的flutter engine使用的flutter官方编译好的产物,并没有通过xcode来编译:

ini 复制代码
Pod::Spec.new do |s|
  s.name                  = 'Flutter'
  s.version               = '3.0.500' # 3.0.5
  s.summary               = 'A UI toolkit for beautiful and fast apps.'
  s.description           = <<-DESC
Flutter is Google's UI toolkit for building beautiful, fast apps for mobile, web, desktop, and embedded devices from a single codebase.
This pod vends the iOS Flutter engine framework. It is compatible with application frameworks created with this version of the engine and tools.
The pod version matches Flutter version major.minor.(patch * 100) + hotfix.
DESC
  s.homepage              = 'https://flutter.dev'
  s.license               = { :type => 'BSD', :text => <<-LICENSE
Copyright 2014 The Flutter Authors. All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimer in the documentation and/or other materials provided
      with the distribution.
    * Neither the name of Google Inc. nor the names of its
      contributors may be used to endorse or promote products derived
      from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

LICENSE
  }
  s.author                = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
  s.source                = { :http => 'https://storage.flutter-io.cn/flutter_infra_release/flutter/e85ea0e79c6d894c120cda4ee8ee10fe6745e187/ios/artifacts.zip' }
  s.documentation_url     = 'https://flutter.dev/docs'
  s.platform              = :ios, '9.0'
  s.vendored_frameworks   = 'Flutter.xcframework'
end

总结

flutter页面1有一个输入框,将输入框获取焦点变成输入状态,push到native页面2, native页面2 push到flutter页面3,关闭flutter页面3,回到native页面2, 再关闭native页面2,此时就会产生崩溃。因为在关闭页面3时,页面3调用detachFlutterEngineIfNeeded会将engine的viewController置空。关闭native页面2时,页面1viewWillappear之前engine的viewController已经是空的,触发键盘hostView中的断言导致崩溃。

如果是3个flutter页面之间的跳转则不会有问题。

解决方法是当跳转新页面时,将当前页面的输入框的焦点先取消再跳转。

scss 复制代码
//push之前取消当前页面输入框的焦点
FocusManager.instance.primaryFocus?.unfocus();
//push新页面
相关推荐
GitLqr1 小时前
玩转 Flutter 项目依赖:从 pubspec.yaml 到实战包管理
flutter·面试·全栈
程序员老刘8 小时前
Qwen 3.8 max干了20分钟没干完,免费模型3分17秒搞定,问题出在哪?
flutter·ai编程
飞凌嵌入式10 小时前
告别多端重复开发:Buildroot+Flutter实现全平台UI风格统一
flutter·ui
FungLeo11 小时前
Flutter 带 TTL 的多级缓存设计:内存+磁盘+网络三层实战
网络·flutter·缓存·性能优化
FungLeo12 小时前
Flutter 可复用公共组件库设计与落地:AppDialog/BottomSheet 等实战
flutter
Fungleo1069971 天前
Flutter/Android Release 包连不上网?AndroidManifest INTERNET 权限排查实录
flutter
GitLqr1 天前
Flutter 实战:为你的 App 增加桌面快捷方式 (Quick Actions)
flutter·app·全栈
FungLeo1 天前
Flutter 把第三方 UI 库渐进迁回 Material 3:组件映射总表 + 四批次替换实战
flutter·ui
GitLqr2 天前
iOS 27 强制要求 UISceneDelegate:UIKit 和 Flutter 开发者该如何应对?
flutter·ios·全栈
蜡台2 天前
Flutter HTTP 请求完整详解
网络协议·flutter·http·dart