【iOS】一段防护代码引发的内存风暴

admin 2024年2月17日19:26:28评论8 views字数 12326阅读41分5秒阅读模式

一、背景

K歌App在8月31号晚上收到了线上大量的用户反馈,应用使用中出现闪退。

收到反馈后,开发同学在TME的火眼APM平台上根据用户id进行搜索判断,是否有共性的Crash堆栈。将所用的用户都检索了后发现,并没有相关的堆栈信息。

没有堆栈又出现闪退,大概率是watchdog或者oom导致。考虑到K歌线上全量开启了MetricKit进行稳定性收集,如果是Watchdog,应该也能够在APM平台上看到。因此将线索定位在了OOM上。

捞取了反馈用户的客户端日志查看,果然能够在用户的日志中看到Memorywarning的打印。

因此,可以确定这次的闪退问题是内存OOM导致的。

【iOS】一段防护代码引发的内存风暴

在灯塔上报与K歌的APM的内存模块上查看内存模块的曲线,也可以看到相关的内存水位与内存警告上报有明显的上涨。
K歌的内存水位上报曲线
【iOS】一段防护代码引发的内存风暴
分页面内存警告次数曲线
可以注意到,按照页面上报的数据,几乎所有的页面都出现了内存警告次数上涨的情况。
【iOS】一段防护代码引发的内存风暴

二、问题排查

反馈用户的版本集中在K歌的8.12.38版本上。该版本在线上运行了一个月时间。
现在出现有大量反馈,优先考虑引起的原因是配置下发变更或者前后端代码发布,影响了线上代码分支逻辑。
因此,在细化了内存水位与内存警告的数据后,对比前一天同时段数据进行查看,将怀疑时间集中在了30号的10-12点左右。
【iOS】一段防护代码引发的内存风暴
拉通各个团队的同学一起排查对应时间点的相关发布,发现在前一天的10点钟,有一个配置发布,在外网开启了针对数组越界的防护代码。
出于控制变量的角度,将相关配置进行回滚。
在回滚配置后观察,在上报报表上对比两天同时段的数据,发现内存水位和内存警告数都出现了回落与下降。

【iOS】一段防护代码引发的内存风暴

回滚当天的表现如下

【iOS】一段防护代码引发的内存风暴

因此确定是这个配置修改打开外网的防护开关引起的OOM问题。

三、问题定位

虽然通过回滚配置并对比外网数据,发现问题被解决了,但是具体原因还需要进行分析才能明确。

3.1防护源码

K歌在启动后,会读取后台配置,通过OC的Runtime,将Array相关接口进行hook,进行常见的防护判断。
主要是以下相关方法:
【iOS】一段防护代码引发的内存风暴
【iOS】一段防护代码引发的内存风暴

对不可变数组NSArray,将以下3个方法进行swizzle进行保护。

initWithObjects:count:

objectAtIndex:

objectAtIndexedSubscript:

对可变的NSMutableArray,将这5个方法进行了swizzle替换。

objectAtIndex: 

addObject:

removeObjectAtIndex:

replaceObjectAtIndex:withObject:

insertObject:atIndex: 

以下是替换至业务层的防护代码实现:

【iOS】一段防护代码引发的内存风暴
【iOS】一段防护代码引发的内存风暴
上述的相关防护代码,主要是对业务层增加了参数合法性的前置校验判断,避免出现数组越界访问等会导致应用Crash的情况。
那么为什么开启这段防护代码后,会出现内存问题呢?

3.2问题复现

首先在本地直接开启相关配置后进行调试,利用Xcode和Instrument查看实时内存水位查看能否复现问题。

启动App并打开相关配置后,将App挂在K歌的直播歌房等场景中,运行一段时间后能观察到应用内存出现了上涨。
尤其是在动画&前后台切换等场景下,内存使用出现了上涨,且在退出相关场景后,内存没有出现对应的下降。
对比正常情况下的表现不一致。因此判断此时已经出现了内存泄露。
通过MemoryGraph,导出了两个对应时间下的内存快照对比。
左侧是使用App约40分钟后,App使用内存在1.55G,右侧则是使用60分钟,App内存增长到了1.83G的。
【iOS】一段防护代码引发的内存风暴
对比发现,主要是@autoreleasepool content对象,出现了大量增长,由左侧的13w个对象增长到20w个对象。
相关的内存占用从522M增长到了786M,增长了250M的内存空间。总共增长300M,占总增长的80%。
因此可以确定是@autoreleasepool content这个对象在某些场景下出现了内存泄露,不断累积,导致了OOM。
那么,为什么对数组的相关方法Hook后,会导致大量的@autoreleasepool content对象创建呢?且为什么这些对象都不会被释放?
通过Xcode的Malloc Stack Logging选项,可以查看这些@autoreleasepool content对象的创建堆栈。
我们发现@autoreleasepool content对象都集中在NSMutableArray 的 kscrash_objectAtIndex: 方法中被创建。
【iOS】一段防护代码引发的内存风暴
而这个方法正好是我们进行Hook防护的代码之一。因此基本可以确定是这个方法引入的问题。
【iOS】一段防护代码引发的内存风暴

四、原因分析

4.1 现象分析

首先关注调用堆栈。
【iOS】一段防护代码引发的内存风暴
@autoreleasepool content 对象就是我们常说的自动释放池,在系统底层映射的应该就是AutoreleasePoolPage 对象。

4.1.1调用堆栈观察

1. 先关注堆栈的倒数第4层调用,调用创建AutoreleasePoolPage的方法入口就是kscrash_objectAtIndex: 方法。我们可以逆向查看这个方法的二进制实现
【iOS】一段防护代码引发的内存风暴
可以看到由于对应的源码文件在业务层默认采用的是ARC方式编译。因此编译过程中,编译器会自动插入 autorelease 的调用。所以这个逻辑的最终实现,映射MRC模式下的实现应该是
- (id)kscrash_objectAtIndex:(NSUInteger)index {    if (index < self.count) {       return [[self kscrash_objectAtIndex:index] autorelease];     } else {       return nil;   }}
这里可以解释为什么这个方法会与AutoreleasePool有所关联。
2. 再看上一层的调用堆栈。@autoreleasepool content的创建堆栈集中在__CFRunLoopDoObservers函数。少量在其他堆栈中。
可以认为,主要是在__CFRunLoopDoObservers 这个场景下出现了不符合预期的情况。
4.1.2 引用关系观察
通过内存快照选中查看单个的@autoreleasepool content对象的内存引用关系,发现有两个现象:
1. 单个@autoreleasepool content中存在对某些对象的多次重复持有。以下是单个引用关系,其中+32 bytes 和 +40 bytes 分别是当前对象的父节点和子节点。
其余的则是被添加到pool中进行内存管理的对象们。
可以看到该对象主要持有了大量的CFRunloopObserver对象,且会对C-FRunloopObserver对象产生多次持有。
如图中,可以看到这个@autoreleasepool content对象存在了对多个CFRunloopObserver的63和62次引用。
【iOS】一段防护代码引发的内存风暴
2.@autoreleasepool content之间存在非常深的链式持有关系。
每个@autoreleasepool content在运行过程中其实是一个双向链表的结构@autoreleasepool content是固定4KB的内存大小。
在运行过程中,如果当前pool满了,则会创建下一个pool,并会互相持有,分别作为对方的父节点与子节点。
如下图,可以看到我们选中的这个@autoreleasepool content 被大量的父节点持有,同时这个对象也持有了大量的子节点。
【iOS】一段防护代码引发的内存风暴

4.1.3 正常情况下的表现

此时我们可以对比正常没有进行防护的应用表现,可以发现内存不再增长,观察此时的MemoryGraph。
一,此时内存快照中的@autoreleasepool content的数量没有随着对应用的操作而增长,而是维持在一个相对稳定的数量上。
二,没有发现基于__CFRunLoopDoObservers链路创建的@autorelease-pool content对象。
三,没有发现大量链式持有的@autoreleasepool content集合。
【iOS】一段防护代码引发的内存风暴
通过对比可以得出结论,是由于这段针对MutableArray 的 objectAtIndex方法进行防护逻辑,使得@autoreleasepool content 对象被大量创建且不释放,不断积累,导致了应用出现OOM
如果我们能将上述现象解释明白,这个问题的原因也就解决了。

4.2 Runloop的执行

先分析__CFRunLoopDoObservers这个函数。
通过函数名即可知这是在iOS Runloop体系中的一个函数。因为iOS 的Runloop是开源的,省了人工逆向的逻辑,我们直接去appleopensource上下载源码来查看。
Runloop的代码实现在CoreFoundation中,这里是源码地址:
https://opensource.apple.com/source/CF/ 
其中有不同版本的CoreFoundation代码,我们直接查看最新的那一套。
https://opensource.apple.com/source/CF/CF-1153.18/ 
4.2.1 __CFRunLoopDoObservers的函数逻辑
下载后,可以在CFRunLoop.c 这个文件中找到Runloop的实现。直接搜索__CFRunLoopDoObservers,即可找到对应实现。
【iOS】一段防护代码引发的内存风暴
简单解释下__CFRunLoopDoObservers的代码逻辑,函数参数为当前Runloop对象 rl,当前的RunloopMode 对象 rlm,和当前的状态activity。
函数中,绿色框部分是通过CFArrayGetValueAtIndex 函数将 rlm(Runloop Mode)所持有的observers进行一次遍历,取出所有合法且注册监听状态与activity一致的observer,添加进入collectedObservers 这个临时数组中。
在蓝色框部分中,依次遍历collectedObservers这个数组中的observer对象,并通知他们当前runloop的状态发生了变化。即将进入对应的某个activity状态。
此时,我们可以注意到关键函数CFArrayGetValueAtIndex。我们可知iOS的体系中,CoreFoundation 与 Foundation中的对象其实是Full-bridge的,两个框架之间的对象可以进行强制的类型互转。
也就是,我们对NSMutableArray的objcAtIndex进行swizzle,映射下来也会影响到CFArrayGetValueAtIndex这个函数逻辑。
也就解释了为什么在我们的防护逻辑下,__CFRunLoopDoObservers这个函数会调用到我们的kscrash_objectAtIndex 这个方法中来。
且可知此时的数组中的对象全部都是 CFRunloopObserver 类型,也就是我们上面看到的大量反复持有的CFRunloopObserver对象。

4.2.2 __CFRunLoopDoObservers什么时候被调用

iOS的同学都看过下面这张关于Runloop执行的流程的图。
【iOS】一段防护代码引发的内存风暴
可以看到iOS中Runloop的执行,就是驱动自身的Observer通知状态变更,处理Source0和Source1事件。
核心函数就是__CFRunLoopRun 这个函数。这块相关的文章太多了,我们直接贴一张相关的图片。
可以看到,__CFRunLoopDoObservers 的作用,就是将当前状态的变更对注册进来的observer进行通知。
实际会在上图中所有通知Observer的时候调用__CFRunLoopDoObservers这个函数。
【iOS】一段防护代码引发的内存风暴
到这里我们可以解释为什么__CFRunLoopDoObservers这个函数会调用到我们业务层的防护逻辑。以及为什么MemoryGraph中可以看到@autoreleasepool content对象中会引用CFRunloopObserver。
这是由于__CFRunLoopDoObservers中会调用CFGetObjectAtIndex,将持有CFRunloopObserver的数组进行遍历。
导致业务层会调用CFRunloopObserver对象的autorelease方法,将CFRunloopObserver对象加入到自动释放池中。
4.3 @autoreleasepool content 
@autoreleasepool content 其实就是AutoreleasePoolPage。相关实现定义在objc4这套代码里面,一样是全开源的,源码可看:
https://opensource.apple.com/source/objc4/ 

4.3.1 结构定义

Autoreleasepool的结构定义如下,每个AutoreleasepoolPage 的大小固定为4Kb,除了头结点所持有的一些必要信息,其他内存空间都用作持有添加进入释放池的obj对象。
在实际的使用过程中,AutoreleasepoolPage是一个双向链表,有成员变量同时指向父节点和子节点。整体结构定义如下:
【iOS】一段防护代码引发的内存风暴
(ps:翻objc4源码的时候发现在最新版的AutoreleasepoolPage与这块有些不同,新加入了AutoreleasePoolPageData的相关定义。但是总逻辑是一致的。)

4.3.2 add函数

在应用运行过程中,如果有某个对象的autorelease方法被调用,则会最终调用到AutoreleasePoolPage的函数中。
调用栈为
【iOS】一段防护代码引发的内存风暴
这个函数的逻辑就是将obj引用压入自己当前的栈顶中,并修改next指针至下一个可控空间。注意此时obj可以为任何对象,也可以为nil指针。
    id *add(id obj)    {        assert(!full());        unprotect();        id *ret = next;  // faster than `return next-1` because of aliasing        *next++ = obj;        protect();        return ret;    }

4.3.3 AutoreleasePoolPage的使用

我们看下在MRC环境下是如何使用创建autoreleasePool的。
int main(int argc, const char * argv[]) { { void * atautoreleasepoolobj = objc_autoreleasePoolPush(); // do whatever you want objc_autoreleasePoolPop(atautoreleasepoolobj); } return 0;}
可以看到实际上就是通过调用autoreleasepoolPush函数启用,其实会有一个返回指针。在需要结束,将池子中的对象都释放的话,调用autoreleasepoolPop函数,将指针传入即可。

4.3.4 objc_autoreleasePoolPush

可以看到,push函数,就是将POOL_SENTINEL作为参数,调用autoreleaseFast函数。
可知POOL_SENTINEL是一个宏定义,其本质就是一个 nil 指针。
#define POOL_SENTINEL nil    static inline void *push() {        id *dest;        {            dest = autoreleaseFast(POOL_SENTINEL);        }        assert(*dest == POOL_SENTINEL);        return dest;    }
看下autoreleaseFast函数的实现。
我们在上面的结构可知,每个AutoreleasepoolPage 其实只有4k的大小。按照每个指针为8个字节计算,实际每个AutoreleasepoolPage能引用的对象就是为500多个。
因此在autoreleaseFast函数中,先通过hotPage获取当前线程当前可用的poolPage对象,并判断了下这个page对象是不是full的。如果没有满,则直接调用add函数,也就是直接将一个nil指针压入了当前poolPage的栈顶中。
    static inline id *autoreleaseFast(id obj)    {        AutoreleasePoolPage *page = hotPage();        if (page && !page->full()) {            return page->add(obj);        } else if (page) {            return autoreleaseFullPage(obj, page);        } else {            return autoreleaseNoPage(obj);        }    }

如果page已经满了,则会调用autoreleaseFullPage函数。创建一个新的PoolPage对象,并设置为当前的hotPage。
同时也会设置两个page之间的父子关系,也就是构建page之间的双向链表引用关系。
    static __attribute__((noinline))    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)    {        // The hot page is full.         // Step to the next non-full page, adding a new page if necessary.        // Then add the object to that page.        assert(page == hotPage());        assert(page->full()  ||  DebugPoolAllocation);        do {            if (page->child) page = page->child;            else page = new AutoreleasePoolPage(page);        } while (page->full());        setHotPage(page);        return page->add(obj);    }

这里能够解释上面MemoryGraph中出现@autoreleasepool content对象出现链式引用的原理。
我们可以得出结论,如果@autoreleasepool content满了,就会创建下一个@autoreleasepool content,如果这个又满了,则会继续创建下一个,无限创建下去。
那么这些@autoreleasepool content到底什么时候会被释放呢?

4.3.5 objc_autoreleasePoolPop

我们看Push函数对称的Pop函数实现。
在实际中调用,我们会调用objc_autoreleasePoolPop (atautoreleasepoolobj),同时我们也知道了,atautoreleasepoolobj 这个指针,其实就是POOL_SENTINEL,也就是一个nil指针。作为哨兵对象。
看下Pop函数的实现。(有删减一些与本次分析无关分支,大家可以自己读下相关完整源码)
    static inline void pop(void *token)  // nil    {        AutoreleasePoolPage *page;        id *stop;        page = pageForPointer(token);        // this        stop = (id *)token;        if (PrintPoolHiwat) printHiwat();        // 遍历清空自己的值        page->releaseUntil(nil);        if (page->child) {            // hysteresis: keep one empty child if page is more than half full            if (page->lessThanHalfFull()) {                page->child->kill();            }            // 删掉孙子节点            else if (page->child->child) {                page->child->child->kill();            }        }    }
可知在当前,token指针就是一个nil指针,那么此处的stop的指针,就是nil,实际上这时候又调用到了page的releaseUntil函数,将POOL_SENTINEL(nil,哨兵对象)作为参数传入。
我们看下releaseUntil的源码实现
void releaseUntil(id *stop) { // Not recursive: we don't want to blow out the stack // if a thread accumulates a stupendous amount of garbage while (this->next != stop) { // Restart from hotPage() every time, in case -release // autoreleased more objects AutoreleasePoolPage *page = hotPage(); // fixme I think this `while` can be `if`, but I can't prove it while (page->empty()) { page = page->parent; setHotPage(page); } page->unprotect(); id obj = *--page->next; memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); page->protect(); if (obj != POOL_BOUNDARY) { objc_release(obj); } } setHotPage(this); }
这里的逻辑就是,利用next指针,从栈顶开始判断,如果当前的栈顶元素不为传入的stop指针相等,那么就会将栈顶对象取出,next指针--即清理这个指针空间,然后调用一次这个取出来对象的release方法。
如果调用release后这个对象的retainCount为0,则会触发这块内存的回收。
如果当前的page全都遍历完,但是依旧没有找到这个stop指针呢?
则代表这个page已经全部释放完了,同时将这个page的父节点设置为hotPage,并进行下一次的遍历查找stop指针。
结合Pop函数,当查找到stop指针后,会再遍历一次page的所有子节点,如果这些节点空了,则会调用page的kill函数,回收page本身的内存占用。

4.4 总结

那么,结合上下逻辑,我们可知poolpage对象的内存创建与回收时机,
在push函数中传入POOL_SENTINEL作为哨兵对象,然后再后续的add函数中,如果满了则会继续创建下一个poolpage。
但是在pop函数中,则会从最子节点开始往回回溯,查找POOL_SENTINEL,并释放这个过程中通过add函数加入所持有的各种业务对象。
在遍历结束中,并释放已经empty的子节点们,完成内存的回收。
因此可知AutoreleasepoolPage的push和pop函数必须对称使用,才能实现合理高效的内存管理逻辑。

4.5 Runloop与Autoreleasepool的结合

这里自然而然可以联想到一个很经典的iOS面试题:iOS中Runloop与Autoreleasepool的关系是什么?
那么我们来回答下这个问题!
我们可以直接获取Runloop中打印一下CFRunLoopObserver的对象,可以观察到两个关键的Observer,他们注册的回调函数都是_wrapRunLoopWithAutoreleasePoolHandler。
"<CFRunLoopObserver {valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler , context = {type = mutable-small, count = 0, values = ()}}",    "<CFRunLoopObserver {valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler , context = {type = mutable-small, count = 0, values = ()}}"
他们的优先级分别为-2147483647和2147483647,转成16进制为0x7FFFFFFF。是一个极大值和极小值。代表两个Observer分别需要在状态通知中,被最早调用和最晚调用。
一个Observer会监听Entry(即将进入Loop),并且优先级最高,会第一个调用PoolPage的Push函数,保证创建释放池发生在其他所有回调之前。
另一个Observer会监听BeforeWaiting(准备进入休眠)Exit(即将退出Loop) ,且优先级最低,确保其他回调都执行结束了,才会回溯自动释放池,并回收内存。
_wrapRunLoopWithAutoreleasePoolHandler这个函数中,就是判断传入的activities类型,分别调用_objc_autoreleasePoolPush 和 _objc_autoreleasePoolPop函数,来进行自动释放池的管理。
因此我们写的业务代码,在Runloop的运行驱动之中,这些调用都在被Runloop创建的AutoreleasePool所环绕,从而实现了内存管理,也帮助我们确保不会出现内存泄露。

4.6 OOM的原因分析

结合上面的背景知识,再来看我们的业务场景
我们swizzle了NSMutableArray的方法,将objectAtIndex转移至业务层的kscrash_objectAtIndex: ,
则会主动调用一次对应obj的autorelease 方法。而autorelease的实现,就是调用当前Autoreleasepool的add函数,将obj加入自动释放池中。
结合上述的Autoreleasepool 和 Runloop的源码可知,在应用运行过程中,
1. 由于业务方的代码Hook,会将通过objectAtIndex来遍历数组对象的对象,添加进入Autoreleasepool之中。也就是CFRunloopObserver对象。那么
【iOS】一段防护代码引发的内存风暴【iOS】一段防护代码引发的内存风暴
2. 系统会在Runloop启动的时候,通过遍历Runloop Observer,触发Autoreleasepool调用Push函数,向栈内加入一个哨兵对象作为标志位。
【iOS】一段防护代码引发的内存风暴
3. 在Runloop结束后,通过遍历Runloop Observer调用Autoreleasepool的Pop函数,逐一释放对象,直到遇见哨兵对象为止。

【iOS】一段防护代码引发的内存风暴

【iOS】一段防护代码引发的内存风暴

4. 最终的状态如下,对比这次Runloop启动前,可以发现当前poolpage中已经多加了observer对象在池子中。

【iOS】一段防护代码引发的内存风暴【iOS】一段防护代码引发的内存风暴

那么就会导致
1. Runloop Observer对象,在哨兵对象之前被添加进入了Autoreleasepool的栈顶。
2. Observer没有对应被移出Autoreleasepool的调用时机。
3. 循环执行的的Runloop函数,导致RunloopObserver被不断地压入Autoreleasepool池子中。
4. Autoreleasepool只有4k空间用于存储数据,当池子满了,则会自动创建下一个Autoreleasepool对象。
5. 大量的Autoreleasepool随着Runloop的在内存中被创建且不释放。
6. 随着App的运行时间变长,已申请内存空间不断增加,可用空间越来越少,最终导致应用OOM💥
现在回过头看我们内存快照中的两张图,能与此处的结论能够相互印证。
【iOS】一段防护代码引发的内存风暴
【iOS】一段防护代码引发的内存风暴
且这个底层逻辑,因为不与特定入口相关联,不影响业务上任何的逻辑执行,但是只要使用App,且使用的时间足够长,就会导致内存的可用空间不断减少,最终导致OOM。也是为什么我们的上报能够看到在App的任何页面中都出现了内存警告的大幅增长。

五、其他

5.1可用的线上内存分析工具

因为这次的线上问题是在一个运行了一个月的稳定版本上发生,团队成员们第一时间基于对应时间内的配置变更着手,发现并解决了问题。但是如果这个问题是发生在一个全新的版本上,通过配置变更的思路来排查就不可用了,需要通过其他手段来进行有效的线上分析。
团队重新利用了Demo工程复现相关场景,并观察利用TME的火眼内存工具。
在对内存分区进行扫描,以及监控内存分配堆栈,两个维度监控,查看是否能够在线上抓取到相关问题。
在通过调整下发的检测参数后,也发现了对应的上报。图一是扫描出来的PoolPage对象,与其中的引用关系。
【iOS】一段防护代码引发的内存风暴
图二是相关对象的创建堆栈捕获。
【iOS】一段防护代码引发的内存风暴

5.2 不同CF版本的代码对比

公司的其他团队的同学有反馈他们之前也遇到这个问题,不过更加前置的在内部阶段就发现了问题。
同时他们提到,这个问题之前的系统版本是没有问题的,是在后续的iOS系统版本上才会出现这个问题。
因此我们对比了下不同的CF源码版本,果然发现了差异
早一点CF版本在__CFRunLoopDoObservers 里面遍历observer的话,rlm是使用NSSet来管理Observer,是用的下标取法
【iOS】一段防护代码引发的内存风暴

而对比最新的CF代码版本,这段逻辑改成了数组的逻辑

【iOS】一段防护代码引发的内存风暴
也就是确实这个问题应该只会在高一点的版本中才会出现。

六、问题解决

分析清楚了原因,那么这里的问题就很好解决了。
只需要将相关的防护逻辑,从ARC编译修改为MRC的编译模式,移除编译器自动加入的autorelease调用,即可避免问题的产生。
通过工程修改,也验证了这个问题已经不会再出现。

七、复盘与总结

这一次线上问题的排查,实际上过程中也是拉通了相当多业务团队同学们来一起追溯原因。
事后团队内复盘也发现了不少基建能力上的不足。
1. 线上性能指标监控告警的不完善。
由于这个问题实际上不会影响任何业务逻辑的调用和执行,所以问题的发现是在配置发布接近2天后才被开发感知到并介入干预。
事后通过技术的监控也都能发现了数据的异常,因此也都在第一时间对这些指标在火眼(APM平台)上增加了告警。
【iOS】一段防护代码引发的内存风暴
2. 技术代码的有效性验证不足。
这段防护逻辑实际上在20年就在K歌的工程中实现了,只是由于一直没有遇到实际的业务场景,所以外网一直都没开。
过了3年时间,大家都默认这段代码应该都没问题了,导致这次第一次遇到实际场景后开放相关开关就踩坑了。
也通过这次实实在在的外网问题反馈,聚类,排查,归因,解决,明白了团队在应用稳定性建设上还有相当多的不足与后路要走。

原文始发于微信公众号(腾讯音乐技术团队):【iOS】一段防护代码引发的内存风暴

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月17日19:26:28
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【iOS】一段防护代码引发的内存风暴https://cn-sec.com/archives/2136416.html

发表评论

匿名网友 填写信息