likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 6

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产物,崩溃日志信息就没有那么详细

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

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

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

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

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

[FlutterTextInputPlugin hostView]

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

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控制器

@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控制器对象

- (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

- (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

- (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

- (void)attatchFlutterEngine
{
    if(ENGINE.viewController != self){
        ENGINE.viewController=self;
    }
}

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

- (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

- (void)attatchFlutterEngine
{
    if(ENGINE.viewController != self){
        ENGINE.viewController=self;
    }
}

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

flutter engine源代码

github.com/flutter/eng…

- (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来编译:

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页面之间的跳转则不会有问题。

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

//push之前取消当前页面输入框的焦点
FocusManager.instance.primaryFocus?.unfocus();
//push新页面