背景

我们是一个纯Flutter运用,开机屏运用了穿山甲的广告来增加收入。当集成一个公司内部的原生组件之后,发现iOS端显现开机屏广告之后直接黑屏。

因为我们一直是纯Flutter运用,所以与原生端的交互都是经过plugin的办法。所以在做开机屏广告的时分,也要求借调过来的同学把穿山甲sdk封装成独立的plugin

问题原因

因为在没有集成公司原生sdk的时分,app在显现广告之后是能正常展现的,所以第一时间查看了该sdk对app的改动,发现该组件将rootViewController修正为了UINavigatorController。代码如下:

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    let res = super.application(application, didFinishLaunchingWithOptions: launchOptions)
      let rootVc = self.window?.rootViewController
      if rootVc != nil && !(rootVc is UINavigationController) {
          let navController = UINavigationController(rootViewController: rootVc!)
          navController.navigationBar.isHidden = true
          self.window?.rootViewController = navController
      }
      return res
  }

之所以这么做是因为SDK的页面跳转办法是经过导航控制器的,可是在纯Flutter开发中,根控制器是FlutterViewController,其承继自UIViewController, 为了能正常跳转处置页面,这儿将根控制器手动改为了UINavigationController

那么之前是怎样显现开机屏广告以及怎样封闭广告的呢?看了下插件代码,大致如下

// 开屏广告
- (void) showSplashAd:(FlutterMethodCall*) call result:(FlutterResult) result{
    UIWindow *mainView =[[UIApplication sharedApplication] keyWindow];
    UIViewController *tmpRootVc = mainView.rootViewController;
    DJWelcomeViewController *vc = [[DJWelcomeViewController alloc] init];
    vc.view.frame = CGRectMake(0, 0, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height);
    mainView.rootViewController = vc;
    vc.tmpRootVc = tmpRootVc;
    result(@"");
}

便是将原来的rootVc保存一下,将window的根控制器设置为广告的控制器来显现开机屏。

下面是封闭广告代码:

UIWindow *mainView =[[UIApplication sharedApplication] keyWindow];
mainView.rootViewController = self.tmpRootVc;

看起来没有什么问题,切换根控制器也是iOS的一个常见操作,那为什么突然就不好使了呢?

首先我排查了下是不是根控制器的view没有设置背景色,在切换的时分将根控制器view又设置了背景色为白色,从头运转,仍然还是黑屏。阐明不是背景色的问题。

那另一种常见的黑屏原因可能是根控制器出了问题,直接Xcode运转项目,当黑屏时查看视图层级如下:

当纯Flutter开发遇到iOS的weak特点

发现window下根本就没有控制器!断点查看:

当纯Flutter开发遇到iOS的weak特点

发现rootVc已经为nil,所以导致app黑屏。查看了下rootVc的声明:

@property(nonatomic, weak) UIViewController *rootVc;

发现rootVc是由weak润饰。这儿就真相大白了,因为weak 表示的是一种非具有联系,当初次切换根控制器为DJWelcomeViewController 时,作为根控制器的UINavigatorController 的不再被window持有而开释,DJWelcomeViewController持有的rootVc特点也随之置为nil。当再次切换时window就不再有根视图而显现黑屏。

处理

知道了是什么原因,那处理起来就比较简单了。这儿有两种处理方案

方案一:

rootVc 改为strong润饰,让DJWelcomeViewController 强持有本来的根控制器,这样当封闭广告时能够顺利拿到根控制器

@property(nonatomic, strong) UIViewController *rootVc;

方案二:

还有一种方案便是不再经过切换rootViewController的办法来显现/封闭广告,而运用push或许present的办法路由到广告页面,

// 开屏广告
- (void)showSplashAd:(FlutterMethodCall*)call result:(FlutterResult) result{
    UIWindow *window =[[UIApplication sharedApplication] keyWindow];
    UIViewController *rootVc = window.rootViewController;
    DJWelcomeViewController *vc = [[DJWelcomeViewController alloc] init];
  // 运用push或许present 跳转
    if([rootVc isKindOfClass:[UINavigationController class]]) {
        UINavigationController *nav = (UINavigationController *)rootVc;
        [nav pushViewController:vc animated:NO];
    } else {
        vc.modalPresentationStyle = UIModalPresentationFullScreen;
        [rootVc presentViewController:vc animated:NO completion:nil];
    }
    result(@"");
}

封闭广告

    // 返回flutter侧
    if(self.navigationController) {
        [self.navigationController popViewControllerAnimated:NO];
    } else {
        [self dismissViewControllerAnimated:NO completion:nil];
    }

能够看到,方案一的改动很小,只需要将weak改为strong, 其他都不用改就能处理问题。而方案二呢则是改变了展现广告的办法,显现、封闭广告的办法都要修正,看起来改动代码比较多。

可是因为我们一直是纯Flutter开发,所以希望能尽量少的对根控制器进行操作以防止一些可能的不知道问题,所以最后采用了方案二来处理。

思考

到这儿问题是已经处理了,但却还有一个疑问:

为什么之前运用weak润饰没有问题,将根控制器修正为UINavigationController 之后就有了问题?

其实在iOS原生开发中,在切换控制器时通常是从头创建一个新的vc来设置为rootViewController。假如想像上面那样持有原来的根控制器的话,就需要对其进行一个强持有,运用weak的话显现黑屏才是正常的。

所以问题能够改为

为什么FlutterViewControllerweak润饰,在切换了根控制器之后,FlutterViewController 为什么还没有被开释?

app启动后,iOS端会初始化AppDelegate,纯Flutter开发中,AppDelegate 承继自FlutterAppDelegate ,下面是该类的部分实现:

// 初始化
- (instancetype)init {
  if (self = [super init]) {
    _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
  }
  return self;
}
- (BOOL)application:(UIApplication*)application
    willFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
  return [_lifeCycleDelegate application:application willFinishLaunchingWithOptions:launchOptions];
}
- (BOOL)application:(UIApplication*)application
    didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
  return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}
// Returns the key window's rootViewController, if it's a FlutterViewController.
// Otherwise, returns nil.
- (FlutterViewController*)rootFlutterViewController {
  if (_rootFlutterViewControllerGetter != nil) {
    return _rootFlutterViewControllerGetter();
  }
  UIViewController* rootViewController = _window.rootViewController;
  if ([rootViewController isKindOfClass:[FlutterViewController class]]) {
    return (FlutterViewController*)rootViewController;
  }
  return nil;
}

能够看到,在初始化的时分创建了一个_lifeCycleDelegate,其主要作用是代理一些app的生命周期办法。

这儿我们能够看下rootFlutterViewController 办法,能够看到在获取rootViewController 时判断了下类型是不是FlutterViewController ,假如不是,直接返回nil。

明显,当我们将rootViewController设置为UINavigatorController时,rootFlutterViewController 会返回nil。这也就解释了上面的疑问。当将rootViewControllerFlutterViewController 切换走之后,FlutterViewController并不会开释,Flutter结构仍然持有着FlutterViewController

其他

这儿简单介绍一下FlutterViewController

FlutterViewController 依附于 FlutterEngine,给 Flutter 传递 UIKit 的输入事情,并展现被 FlutterEngine 渲染的每一帧画面。FlutterEngine 则充当 Dart VM 和 Flutter 运转时的主机。

具体的能够看下flutter.cn/docs/develo…

FlutterViewController供给了一个iOS端的容器,FlutterEngine 担任UI的绘制。

FlutterViewController有两个结构函数:

一:传递一个FlutterEngine作为参数

- (instancetype)initWithEngine:(FlutterEngine*)engine
                       nibName:(nullable NSString*)nibName
                        bundle:(nullable NSBundle*)nibBundle {
  NSAssert(engine != nil, @"Engine is required");
  self = [super initWithNibName:nibName bundle:nibBundle];
  if (self) {
    _viewOpaque = YES;
    if (engine.viewController) {
      FML_LOG(ERROR) << "The supplied FlutterEngine " << [[engine description] UTF8String]
                     << " is already used with FlutterViewController instance "
                     << [[engine.viewController description] UTF8String]
                     << ". One instance of the FlutterEngine can only be attached to one "
                        "FlutterViewController at a time. Set FlutterEngine.viewController "
                        "to nil before attaching it to another FlutterViewController.";
    }
    _engine.reset([engine retain]);
    _engineNeedsLaunch = NO;
    _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]);
    _weakFactory = std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(self);
    _ongoingTouches.reset([[NSMutableSet alloc] init]);
    [self performCommonViewControllerInitialization];
    [engine setViewController:self];
  }
  return self;
}

结构函数二:内部会创建一个FlutterEngine

- (instancetype)initWithProject:(FlutterDartProject*)project
                   initialRoute:(NSString*)initialRoute
                        nibName:(NSString*)nibName
                         bundle:(NSBundle*)nibBundle {
  self = [super initWithNibName:nibName bundle:nibBundle];
  if (self) {
    [self sharedSetupWithProject:project initialRoute:initialRoute];
  }
  return self;
}
- (void)sharedSetupWithProject:(nullable FlutterDartProject*)project
                  initialRoute:(nullable NSString*)initialRoute {
  // Need the project to get settings for the view. Initializing it here means
  // the Engine class won't initialize it later.
  if (!project) {
    project = [[[FlutterDartProject alloc] init] autorelease];
  }
  FlutterView.forceSoftwareRendering = project.settings.enable_software_rendering;
  auto engine = fml::scoped_nsobject<FlutterEngine>{[[FlutterEngine alloc]
                initWithName:@"io.flutter"
                     project:project
      allowHeadlessExecution:self.engineAllowHeadlessExecution
          restorationEnabled:[self restorationIdentifier] != nil]};
  if (!engine) {
    return;
  }
  _viewOpaque = YES;
  _weakFactory = std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(self);
  _engine = std::move(engine);
  _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]);
  [_engine.get() createShell:nil libraryURI:nil initialRoute:initialRoute];
  _engineNeedsLaunch = YES;
  _ongoingTouches.reset([[NSMutableSet alloc] init]);
  [self loadDefaultSplashScreenView];
  [self performCommonViewControllerInitialization];
}

FlutterEngine 会经过setViewController办法指定一个FlutterViewController :

- (void)setViewController:(FlutterViewController*)viewController {
  FML_DCHECK(self.iosPlatformView);
  _viewController =
      viewController ? [viewController getWeakPtr] : fml::WeakPtr<FlutterViewController>();
  self.iosPlatformView->SetOwnerViewController(_viewController);
  [self maybeSetupPlatformViewChannels];
  if (viewController) {
    __block FlutterEngine* blockSelf = self;
    self.flutterViewControllerWillDeallocObserver =
        [[NSNotificationCenter defaultCenter] addObserverForName:FlutterViewControllerWillDealloc
                                                          object:viewController
                                                           queue:[NSOperationQueue mainQueue]
                                                      usingBlock:^(NSNotification* note) {
                                                        [blockSelf notifyViewControllerDeallocated];
                                                      }];
  } else {
    self.flutterViewControllerWillDeallocObserver = nil;
    [self notifyLowMemory];
  }
}

PS: 这儿仅仅剖析了纯Flutter的运用,对于add-to-app方式可能会有些不同的情况。

另:其实我觉得sdk应该暴露api给调用方,让调用方自己决定怎么跳转到处置页面,而不是要求调用方必须有UINavigatorController,但这是集团内部的sdk,也没办法要求人家更改,只能自己处理了‍

因为是个人的剖析,难免会有些遗漏或许过错之处,还望各位大佬能多多指教