背景介绍
_gpus_ReturnNotPermittedKillClient
可知,App是因为在后台访问了GPU导致了Crash,或许有些同学不太明白,为什么App在后台访问GPU会导致Crash呢?这其实是和iOS系统的策略有关。iOS系统是禁止后台的App访问GPU的,主要是为了保证前台正在运行的App的性能体验。因为GPU在系统看来是非常宝贵且有限的资源,如果App退到后台之后还继续疯狂使用GPU的话,那么前台App的性能可能就无法得到保障了。那么就有同学问了,如果App并没有遵循这个规范,在退到后台之后,继续使用Metal或OpenGLES访问GPU,会发生什么事情呢?答案很简单,会直接Crash。官方的修复方案
UIApplicationDidEnterBackgroundNotification
这个通知后,不要再执行任何可能会访问到GPU的操作。但是这个通知是在主线程收到的,而真正去访问GPU的则是Raster线程或IO线程,那么该如何通知它们呢?为此,Google软件工程师Aaron Clarke(github名为gaaclarke)设计了一个新的同步机制: SyncSwitch。SyncSwitch简单来说就是可以在一个线程去设置一个类型为bool的value,另一个线程的代码分为两个分支,根据value的值来确定具体走哪个分支。我们先来看看SyncSwitch是如何设计与实现的,以下是SyncSwitch的构造函数和两个API:SetSwitch
来设置value来表示GPU是否可用。而逻辑需要根据iOS在前台或者在后台走不同分支时,则调用Execute
方法来走对应的逻辑。Execute
方法参数的结构体Handlers
的代码:SetSwitch
和Execude
时加锁,然后根据value
值去调用true_handler
或者false_handler
。ImageDecoder::UploadRasterImage
导致的GPU后台Crash,具体代码如下:问题的进一步解决
MultipleFrameCodec::getNextFrame场景的Crash
MultipleFrameCodec::getNextFrame
引起的占比是最高的,因此我决定先从这个问题下手。我们先来看一下问题的堆栈信息,来分析一下Crash具体是如何发生的。SkImage::MakeCrossContextFromPixmap
来生成一个基于texture的SkImage
,该方法与问题相关的逻辑如下:SkImage
之前,会先调用了GrGpu::prepareTextureForCrossContextUsage
来获取一个GrSemaphore
,那么这个方法具体是什么用的呢,我们先来看看官方的文档注释:GrSemaphore
用于同步。接下来看看使用OpenGLES的情况下这个方法是如何实现的吧。GrGLSync
,并且会调用一次flush
来确保GrGLSync
对象已经创建并且发送到了gpu。这个flush
方法会去调用OpenGLES的APIglFlush
,如果此时应用正处于后台,那么调用glFlush
会导致应用直接崩溃。MultipleFrameCodec::getNextFrame
方法与之相关的逻辑,逻辑还是比较清晰的,如果有resourceContext
,则使用SkImage::MakeCrossContextFromPixmap
来生成SkImage
,否则则使用SkImage::MakeFromBitmap
来生成。gpu_disable_sync_switch
来确保只有在GPU可用时才会调用SkImage::MakeCrossContextFromPixmap
生成SkImage
,而如果GPU不可用,则回退到调用SkImage::MakeFromBitmap
生成SkImage
。EncodeImage场景的Crash
image_encoding.cc
中的EncodeImage
方法未使用is_gpu_disabled_sync_switch
导致的Crash,具体代码如下:is_gpu_disabled_sync_switch
的逻辑,这部分代码比较简单,就不贴了。定位问题和修改问题可以说都很顺利,但是如何去写单元测试则让我犯了难。我修改的ConvertToRasterUsingResourceContext
是一个内部方法,写单元测试时访问不到,另外即使将这个方法暴露出来,我们也没有办法传入一个flutter::SyncSwitch
来用于测试,原因是flutter::SyncSwitch
内部并没有属性来判断它自己是否被访问过。由于写不出单元测试,所以我只好向flutter官方的同学求助。ConvertToRasterUsingResourceContext
放到头文件,并改成模板,这样单元测试里不用传入flutter::SyncSwitch
,只需要传入另一个Mock的其它类型的SyncSwitch
就行。FLUTTER_RELEASE
这个宏来做条件编译,在非release模式下为SyncSwitch
增加逻辑使得其可以知道它是否被调用过,这样可以尽量少改动具体实现来做单元测试。但是这个方案最终没有被gaaclarke采纳,他觉得条件编译使得维护变得复杂,并不是一个好方案。image_encoding.h
中,gaaclarke给了我一个建议,可以增加一个image_encoding_impl.h
来解决这个问题,这的确是个好主意。Rasterizer::DrawToSurface
方法不要在后台访问GPU。但是这个场景和之前场景却有着比较大的区别,之前的场景如果我们无法访问GPU,那么我们可以使用CPU来做兜底逻辑。但是在Rasterizer::DrawToSurface
时无法访问GPU,那么应该怎么处理呢。Rasterizer::DrawToSurface
时,也使用is_gpu_disabled_sync_switch
。那么如果当前无法访问GPU,该怎么做呢,我突然想到,DrawToSurface
是为了让这一帧上屏,让用户能够看见。那么如果此时应用在后台,用户本来就看不见这一帧,那么我们为什么不直接将这一帧丢弃掉呢?这一帧丢掉会有问题吗,我仔细分析了一下,应该没有问题,因为当用户从后台回到前台时,Animator::Start
会被调用,然后会调用RequestFrame
去确保最新的一帧上屏。Rasterizer::DrawToSurface
这么顶层的地方使用is_gpu_disabled_sync_switch
。他觉得或许这个问题应该从Skia层解决更为合适。总结
References
[1]
#13908 Made a way to turn off the OpenGL operations on the IO thread for backgrounded apps: https://github.com/flutter/engine/pull/13908[2]
#28159 Prevent app from accessing the GPU in the background in MultiFrameCodec: https://github.com/flutter/engine/pull/28159[3]
#28369 Prevent app from accessing the GPU in the background in EncodeImage: https://github.com/flutter/engine/pull/28369[4]
Crash in Metal from MTLReleaseAssertionFailure => https://github.com/flutter/flutter/issues/89171[5]
分析过程: https://github.com/flutter/flutter/issues/89171#issuecomment-908871405[6]
#28383 Started providing the GPU sync switch to Rasterizer.DrawToSurface() => https://github.com/flutter/engine/pull/28383