作者:闲鱼技术——皓黯
1. 背景介绍
众所周知,在众多跨渠道计划中,Flutter的烘托一致性一直是它的一大亮点,可谓是真实的完成了像素等级的控制。这首要归功于Flutter的架构规划,它依据Skia来完成烘托,而后者则以OpenGLES、Metal或Vulkan作为后端,这在最大程度上保证了不同渠道的烘托一致性。Flutter的这个架构规划十分先进,当然,同其他项目一样,Flutter也不可避免的存在一些bug。今日我想和大家聊的,便是一个Flutter在iOS后台时拜访GPU导致Crash的问题。本文将先对GPU后台Crash产生的原因进行说明,再介绍官方对此问题的修正计划,终究分享闲鱼在此基础上怎么在其他三个场景处理该问题。
闲鱼App在运用Flutter开发项目的进程中,发现了一个与Flutter相关的iOS Crash,这个Crash的详细仓库如下:
依据仓库中的_gpus_ReturnNotPermittedKillClient
可知,App是由于在后台拜访了GPU导致了Crash,或许有些同学不太理解,为什么App在后台拜访GPU会导致Crash呢?这其实是和iOS体系的策略有关。iOS体系是制止后台的App拜访GPU的,首要是为了保证前台正在运行的App的功用体验。由于GPU在体系看来是十分宝贵且有限的资源,假如App退到后台之后还持续疯狂运用GPU的话,那么前台App的功用或许就无法得到保证了。那么就有同学问了,假如App并没有遵循这个标准,在退到后台之后,持续运用Metal或OpenGLES拜访GPU,会产生什么事情呢?答案很简单,会直接Crash。
由于Flutter运用了Skia作为烘托引擎,而后者在iOS则以Metal或OpenGLES作为后端,因而免不了要和GPU打交道,而在LayerTree光栅化上屏或许图片解码上传纹理时,都会运用到GPU,因而假如没有做好相应的保护措施的话,App就有或许Crash。
2. 官方的修正计划
Flutter运用日益增多,开发者们慢慢发现了这个问题,并向官方提了相应的Issue。陆陆续续有开发者向Flutter官方反应GPU后台Crash的问题,这引起了官方的留意,官方决议跟踪和处理这个问题。那么这个问题该怎么处理呢?处理这个问题的关键,便是在收到UIApplicationDidEnterBackgroundNotification
这个告诉后,不要再履行任何或许会拜访到GPU的操作。可是这个告诉是在主线程收到的,而真实去拜访GPU的则是Raster线程或IO线程,那么该怎么告诉它们呢?为此,Google软件工程师Aaron Clarke(github名为gaaclarke)规划了一个新的同步机制: SyncSwitch。SyncSwitch简单来说便是能够在一个线程去设置一个类型为bool的value,另一个线程的代码分为两个分支,依据value的值来确认详细走哪个分支。咱们先来看看SyncSwitch是怎么规划与完成的,以下是SyncSwitch的构造函数和两个API:
当iOS的前后台状况产生改动时,能够经过SetSwitch
来设置value来表明GPU是否可用。而逻辑需求依据iOS在前台或许在后台走不同分支时,则调用Execute
办法来走对应的逻辑。
以下是作为Execute
办法参数的结构体Handlers
的代码:
以下是上述办法的详细完成,咱们能够看到逻辑比较简单,首要便是在SetSwitch
和Execude
时加锁,然后依据value
值去调用true_handler
或许false_handler
。
终究官方经过这个计划,成功修正了ImageDecoder::UploadRasterImage
导致的GPU后台Crash,详细代码如下:
这是官方关于用于修正这个问题的PR:
#13908 Made a way to turn off the OpenGL operations on the IO thread for backgrounded apps[1]
当然,这个进程也不是一帆风顺的,在这个进程中,也遇到了一些问题,可是gaaclarke都顺畅处理了。
3. 问题的进一步处理
闲鱼将Flutter引擎晋级并将官方最新的修正Patch打上以后,发现依然存在GPU后台Crash,这说明GPU后台Crash的问题并没有完全处理,难道是官方的处理计划还存在什么疏漏吗?我细心剖析了闲鱼产生GPU后台Crash的仓库后,确认问题一共散布在3个当地,MultipleFrameCodec、EncodeImage以及DrawToSurface,而之前大家反应的ImageDecoder则并未呈现。所以能够确认的是,官方的处理计划并没有问题,仅仅并没有覆盖全面。而闲鱼由于事务体量大,场景复杂,再加上大规模运用Flutter,所以这些问题都都被一一露出了出来。已然问题原因现已确认,那么让咱们来看下怎么修正吧。
3.1 MultipleFrameCodec::getNextFrame场景的Crash
在闲鱼遇到的3个GPU后台Crash中,MultipleFrameCodec::getNextFrame
引起的占比是最高的,因而我决议先从这个问题下手。咱们先来看一下问题的仓库信息,来剖析一下Crash详细是怎么产生的。
依据仓库可知,在产生Crash时,Flutter调用了SkImage::MakeCrossContextFromPixmap
来生成一个依据texture的SkImage
,该办法与问题相关的逻辑如下:
咱们看到,在生成SkImage
之前,会先调用了GrGpu::prepareTextureForCrossContextUsage
来获取一个GrSemaphore
,那么这个办法详细是什么用的呢,咱们先来看看官方的文档注释:
依据文档注释能够看到,这个办法首要是为了让保证texture能够在多个context下安全运用。依据详细的后端完成,这个办法或许会回来一个GrSemaphore
用于同步。接下来看看运用OpenGLES的情况下这个办法是怎么完成的吧。
咱们留意到,这个办法会创立一个GrGLSync
,而且会调用一次flush
来保证GrGLSync
对象现已创立而且发送到了gpu。这个flush
办法会去调用OpenGLES的APIglFlush
,假如此刻运用正处于后台,那么调用glFlush
会导致运用直接崩溃。
上面咱们剖析了OpenGLES的完成,那么在Metal下是否也存在GPU后台Crash呢?答案是必定的,Metal也有这方面的限制,咱们在flutter issue里找到了一个与上面类似的仓库。
已然现已找到问题的原因了,那么咱们来看看怎么修正吧。先来看一下MultipleFrameCodec::getNextFrame
办法与之相关的逻辑,逻辑仍是比较明晰的,假如有resourceContext
,则运用SkImage::MakeCrossContextFromPixmap
来生成SkImage
,不然则运用SkImage::MakeFromBitmap
来生成。
那么该怎么修正这个问题呢,相信细心的读者或许现已想到了处理办法,能够运用gpu_disable_sync_switch
来保证只要在GPU可用时才会调用SkImage::MakeCrossContextFromPixmap
生成SkImage
,而假如GPU不可用,则回退到调用SkImage::MakeFromBitmap
生成SkImage
。
有了这个计划后,那么只需求稍加修正代码,功用也就完成了。当然,为了保证功用正确以及后续不会由于其他改动而导致不可用,咱们还需求写一个单元测验。终究的PR如下:
#28159 Prevent app from accessing the GPU in the background in MultiFrameCodec[2]
gaaclarke在review了这个PR之后给予了必定,现在这个PR现已成功合入到了master。
3.2 EncodeImage场景的Crash
第二个产生Crash的场景是在EncodeImage的时分,详细仓库如下
依据这个仓库,我很快就定位到了场景,这是在image_encoding.cc
中的EncodeImage
办法未运用is_gpu_disabled_sync_switch
导致的Crash,详细代码如下:
有了上一次的经验,我很快在这个基础上加上is_gpu_disabled_sync_switch
的逻辑,这部分代码比较简单,就不贴了。定位问题和修正问题能够说都很顺畅,可是怎么去写单元测验则让我犯了难。我修正的ConvertToRasterUsingResourceContext
是一个内部办法,写单元测验时拜访不到,别的即便将这个办法露出出来,咱们也没有办法传入一个flutter::SyncSwitch
来用于测验,原因是flutter::SyncSwitch
内部并没有特点来判断它自己是否被拜访过。由于写不出单元测验,所以我只好向flutter官方的同学求助。
gaaclarke十分热心地给了我一个计划,让我将ConvertToRasterUsingResourceContext
放到头文件,并改成模板,这样单元测验里不必传入flutter::SyncSwitch
,只需求传入另一个Mock的其它类型的SyncSwitch
就行。
我测验了这个他给的这个计划,觉得改动有点大,在当时的我看来,单元测验的效果是为了保证自己的功用不被意外回滚。而我觉得这个PR被回滚的概率很小,因而我想着是不是能够和官方同学商量一下,不必写测验。
官方同学给我的回复让我对单元测验有了新的认知。gaaclarke觉得一个不完美的测验也比没有测验要好,而zanderso则给出了另一个理由,一切能被cherry-pick到beta或stable分支的功用都需求有单元测验,假如一个功用没有单元测验,那么即便有需求,它也不或许被cherry-pick到beta或stable分支。
他们的回复让我更加理解了单元测验的重要性,可是我当时觉得gaaclarke给的计划改动有点大,所以想了一个新计划,运用FLUTTER_RELEASE
这个宏来做条件编译,在非release形式下为SyncSwitch
增加逻辑使得其能够知道它是否被调用过,这样能够尽量少改动详细完成来做单元测验。可是这个计划终究没有被gaaclarke采纳,他觉得条件编译使得维护变得复杂,并不是一个好计划。
所以终究我仍是依照gaaclarke的主张完成了终究版别的单元测验,同时也向gaaclarke表达了我自己的担忧。这个计划将原本无需露出的头文件都露出到了image_encoding.h
中,gaaclarke给了我一个主张,能够增加一个image_encoding_impl.h
来处理这个问题,这的确是个好主意。
在经过多轮的测验和评论后,这个PR终于成功合入官方。
#28369 Prevent app from accessing the GPU in the background in EncodeImage[3]
整个进程和结果得到了gaaclarke的认可,他对此表明赞许以及感谢。
其实我觉得这个进程中,我从gaaclarke那儿学到了十分多的东西,包括编码能力以及怎么写好单元测验等等。
3.3 Rasterizer::DrawToSurface场景的Crash
这是闲鱼GPU后台Crash的终究一个场景,也是三个场景中最为棘手的一个,其仓库如下:
从仓库剖析,问题十分明晰。咱们需求保证Rasterizer::DrawToSurface
办法不要在后台拜访GPU。可是这个场景和之前场景却有着比较大的差异,之前的场景假如咱们无法拜访GPU,那么咱们能够运用CPU来做兜底逻辑。可是在Rasterizer::DrawToSurface
时无法拜访GPU,那么应该怎么处理呢。
正在我还在苦恼怎么来处理这个问题时,官方忽然提了一个Issue:Crash in Metal from MTLReleaseAssertionFailure[4],我细心看了一下仓库,发现他们遇到的竟然和我遇到的是同一个问题!这个Issue的优先级是P2,仍是十分紧迫的,由于我决议尽我所能,和官方一同处理这个问题。
为了说清楚这个问题,我写了一段详细的剖析进程[5],阐述了这个问题和之前遇到的GPU后台Crash是一类问题,所以咱们需求在Rasterizer::DrawToSurface
时,也运用is_gpu_disabled_sync_switch
。那么假如当前无法拜访GPU,该怎么做呢,我忽然想到,DrawToSurface
是为了让这一帧上屏,让用户能够看见。那么假如此刻运用在后台,用户原本就看不见这一帧,那么咱们为什么不直接将这一帧丢掉掉呢?这一帧丢掉会有问题吗,我细心剖析了一下,应该没有问题,由于当用户从后台回到前台时,Animator::Start
会被调用,然后会调用RequestFrame
去保证最新的一帧上屏。
为了能更快处理这个问题,我还提了一个PR,供官方作为处理问题的一个挑选计划。gaaclarke在看了我的剖析后,觉得有道理,不过他仍是不太确认是不是应该在Rasterizer::DrawToSurface
这么顶层的当地运用is_gpu_disabled_sync_switch
。他觉得或许这个问题应该从Skia层处理更为合适。
而在经过一阵子调研后,gaaclarke决议采纳我的这个计划,终究进过几轮的评论和改进,我和gaaclarke一同完成了这个PR,这个PR终究被合入了主干。
[#28383 Started providing the GPU sync switch to Rasterizer.DrawToSurface()](Started providing the GPU sync switch to Rasterizer.DrawToSurface())
4. 总结
Flutter运用在后台拜访GPU导致Crash的问题至此得到了圆满处理,相信不久的将来大家就能在Flutter release版别体验到。未来闲鱼团队会一如既往在Flutter上持续深耕,处理Flutter在落地进程中遇到的各种问题,给大家带来更好的用户体验。
References
[1]
#13908 Made a way to turn off the OpenGL operations on the IO thread for backgrounded apps: github.com/flutter/eng…[2]
#28159 Prevent app from accessing the GPU in the background in MultiFrameCodec: github.com/flutter/eng…[3]
#28369 Prevent app from accessing the GPU in the background in EncodeImage: github.com/flutter/eng…[4]
Crash in Metal from MTLReleaseAssertionFailure: github.com/flutter/flu…[5]
剖析进程: github.com/flutter/flu…