前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【iOS】一段防护代码引发的内存风暴

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

作者头像
QQ音乐技术团队
发布2023-10-23 17:59:00
4440
发布2023-10-23 17:59:00
举报

一、背景

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

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

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

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

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

在灯塔上报与K歌的APM的内存模块上查看内存模块的曲线,也可以看到相关的内存水位与内存警告上报有明显的上涨。

K歌的内存水位上报曲线

分页面内存警告次数曲线

可以注意到,按照页面上报的数据,几乎所有的页面都出现了内存警告次数上涨的情况。

二、问题排查

反馈用户的版本集中在K歌的8.12.38版本上。该版本在线上运行了一个月时间。

现在出现有大量反馈,优先考虑引起的原因是配置下发变更或者前后端代码发布,影响了线上代码分支逻辑。

因此,在细化了内存水位与内存警告的数据后,对比前一天同时段数据进行查看,将怀疑时间集中在了30号的10-12点左右。

拉通各个团队的同学一起排查对应时间点的相关发布,发现在前一天的10点钟,有一个配置发布,在外网开启了针对数组越界的防护代码。

出于控制变量的角度,将相关配置进行回滚。

在回滚配置后观察,在上报报表上对比两天同时段的数据,发现内存水位和内存警告数都出现了回落与下降。

回滚当天的表现如下

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

三、问题定位

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

3.1防护源码

K歌在启动后,会读取后台配置,通过OC的Runtime,将Array相关接口进行hook,进行常见的防护判断。

主要是以下相关方法:

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

initWithObjects:count:

objectAtIndex:

objectAtIndexedSubscript:

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

objectAtIndex:

addObject:

removeObjectAtIndex:

replaceObjectAtIndex:withObject:

insertObject:atIndex:

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

上述的相关防护代码,主要是对业务层增加了参数合法性的前置校验判断,避免出现数组越界访问等会导致应用Crash的情况。

那么为什么开启这段防护代码后,会出现内存问题呢?

3.2问题复现

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

启动App并打开相关配置后,将App挂在K歌的直播歌房等场景中,运行一段时间后能观察到应用内存出现了上涨。

尤其是在动画&前后台切换等场景下,内存使用出现了上涨,且在退出相关场景后,内存没有出现对应的下降。

对比正常情况下的表现不一致。因此判断此时已经出现了内存泄露。

通过MemoryGraph,导出了两个对应时间下的内存快照对比。

左侧是使用App约40分钟后,App使用内存在1.55G,右侧则是使用60分钟,App内存增长到了1.83G的。

对比发现,主要是@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: 方法中被创建。

而这个方法正好是我们进行Hook防护的代码之一。因此基本可以确定是这个方法引入的问题。

四、原因分析

4.1 现象分析

首先关注调用堆栈。

@autoreleasepool content 对象就是我们常说的自动释放池,在系统底层映射的应该就是AutoreleasePoolPage 对象。

4.1.1调用堆栈观察

1. 先关注堆栈的倒数第4层调用,调用创建AutoreleasePoolPage的方法入口就是kscrash_objectAtIndex: 方法。我们可以逆向查看这个方法的二进制实现

可以看到由于对应的源码文件在业务层默认采用的是ARC方式编译。因此编译过程中,编译器会自动插入 autorelease 的调用。所以这个逻辑的最终实现,映射MRC模式下的实现应该是

代码语言:javascript
复制
- (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次引用。

2.@autoreleasepool content之间存在非常深的链式持有关系。

每个@autoreleasepool content在运行过程中其实是一个双向链表的结构@autoreleasepool content是固定4KB的内存大小。

在运行过程中,如果当前pool满了,则会创建下一个pool,并会互相持有,分别作为对方的父节点与子节点。

如下图,可以看到我们选中的这个@autoreleasepool content 被大量的父节点持有,同时这个对象也持有了大量的子节点。

4.1.3 正常情况下的表现

此时我们可以对比正常没有进行防护的应用表现,可以发现内存不再增长,观察此时的MemoryGraph。

一,此时内存快照中的@autoreleasepool content的数量没有随着对应用的操作而增长,而是维持在一个相对稳定的数量上。

二,没有发现基于__CFRunLoopDoObservers链路创建的@autorelease-pool content对象。

三,没有发现大量链式持有的@autoreleasepool content集合。

通过对比可以得出结论,是由于这段针对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,即可找到对应实现。

简单解释下__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中Runloop的执行,就是驱动自身的Observer通知状态变更,处理Source0和Source1事件。

核心函数就是__CFRunLoopRun 这个函数。这块相关的文章太多了,我们直接贴一张相关的图片。

可以看到,__CFRunLoopDoObservers 的作用,就是将当前状态的变更对注册进来的observer进行通知。

实际会在上图中所有通知Observer的时候调用__CFRunLoopDoObservers这个函数。

到这里我们可以解释为什么__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是一个双向链表,有成员变量同时指向父节点和子节点。整体结构定义如下:

(ps:翻objc4源码的时候发现在最新版的AutoreleasepoolPage与这块有些不同,新加入了AutoreleasePoolPageData的相关定义。但是总逻辑是一致的。)

4.3.2 add函数

在应用运行过程中,如果有某个对象的autorelease方法被调用,则会最终调用到AutoreleasePoolPage的函数中。

调用栈为

这个函数的逻辑就是将obj引用压入自己当前的栈顶中,并修改next指针至下一个可控空间。注意此时obj可以为任何对象,也可以为nil指针。

代码语言:javascript
复制
    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的。

代码语言:javascript
复制
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 指针。

代码语言:javascript
复制
#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的栈顶中。

代码语言:javascript
复制
    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之间的双向链表引用关系。

代码语言:javascript
复制
    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函数的实现。(有删减一些与本次分析无关分支,大家可以自己读下相关完整源码)

代码语言:javascript
复制

    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的源码实现

代码语言:javascript
复制
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。

代码语言:javascript
复制
"<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对象。那么

2. 系统会在Runloop启动的时候,通过遍历Runloop Observer,触发Autoreleasepool调用Push函数,向栈内加入一个哨兵对象作为标志位。

3. 在Runloop结束后,通过遍历Runloop Observer调用Autoreleasepool的Pop函数,逐一释放对象,直到遇见哨兵对象为止。

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

那么就会导致

1. Runloop Observer对象,在哨兵对象之前被添加进入了Autoreleasepool的栈顶。

2. Observer没有对应被移出Autoreleasepool的调用时机。

3. 循环执行的的Runloop函数,导致RunloopObserver被不断地压入Autoreleasepool池子中。

4. Autoreleasepool只有4k空间用于存储数据,当池子满了,则会自动创建下一个Autoreleasepool对象。

5. 大量的Autoreleasepool随着Runloop的在内存中被创建且不释放。

6. 随着App的运行时间变长,已申请内存空间不断增加,可用空间越来越少,最终导致应用OOM💥。

现在回过头看我们内存快照中的两张图,能与此处的结论能够相互印证。

且这个底层逻辑,因为不与特定入口相关联,不影响业务上任何的逻辑执行,但是只要使用App,且使用的时间足够长,就会导致内存的可用空间不断减少,最终导致OOM。也是为什么我们的上报能够看到在App的任何页面中都出现了内存警告的大幅增长。

五、其他

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

因为这次的线上问题是在一个运行了一个月的稳定版本上发生,团队成员们第一时间基于对应时间内的配置变更着手,发现并解决了问题。但是如果这个问题是发生在一个全新的版本上,通过配置变更的思路来排查就不可用了,需要通过其他手段来进行有效的线上分析。

团队重新利用了Demo工程复现相关场景,并观察利用TME的火眼内存工具。

在对内存分区进行扫描,以及监控内存分配堆栈,两个维度监控,查看是否能够在线上抓取到相关问题。

在通过调整下发的检测参数后,也发现了对应的上报。图一是扫描出来的PoolPage对象,与其中的引用关系。

图二是相关对象的创建堆栈捕获。

5.2 不同CF版本的代码对比

公司的其他团队的同学有反馈他们之前也遇到这个问题,不过更加前置的在内部阶段就发现了问题。

同时他们提到,这个问题之前的系统版本是没有问题的,是在后续的iOS系统版本上才会出现这个问题。

因此我们对比了下不同的CF源码版本,果然发现了差异

早一点CF版本在__CFRunLoopDoObservers 里面遍历observer的话,rlm是使用NSSet来管理Observer,是用的下标取法

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

也就是确实这个问题应该只会在高一点的版本中才会出现。

六、问题解决

分析清楚了原因,那么这里的问题就很好解决了。

只需要将相关的防护逻辑,从ARC编译修改为MRC的编译模式,移除编译器自动加入的autorelease调用,即可避免问题的产生。

通过工程修改,也验证了这个问题已经不会再出现。

七、复盘与总结

这一次线上问题的排查,实际上过程中也是拉通了相当多业务团队同学们来一起追溯原因。

事后团队内复盘也发现了不少基建能力上的不足。

1. 线上性能指标监控告警的不完善。

由于这个问题实际上不会影响任何业务逻辑的调用和执行,所以问题的发现是在配置发布接近2天后才被开发感知到并介入干预。

事后通过技术的监控也都能发现了数据的异常,因此也都在第一时间对这些指标在火眼(APM平台)上增加了告警。

2. 技术代码的有效性验证不足。

这段防护逻辑实际上在20年就在K歌的工程中实现了,只是由于一直没有遇到实际的业务场景,所以外网一直都没开。

过了3年时间,大家都默认这段代码应该都没问题了,导致这次第一次遇到实际场景后开放相关开关就踩坑了。

也通过这次实实在在的外网问题反馈,聚类,排查,归因,解决,明白了团队在应用稳定性建设上还有相当多的不足与后路要走。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-10-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯音乐技术团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景
  • 二、问题排查
  • 三、问题定位
    • 3.1防护源码
      • 3.2问题复现
      • 四、原因分析
        • 4.1 现象分析
          • 4.1.1调用堆栈观察
          • 4.1.3 正常情况下的表现
        • 4.2 Runloop的执行
          • 4.2.2 __CFRunLoopDoObservers什么时候被调用
          • 4.3.1 结构定义
          • 4.3.2 add函数
          • 4.3.3 AutoreleasePoolPage的使用
          • 4.3.4 objc_autoreleasePoolPush
          • 4.3.5 objc_autoreleasePoolPop
          • 4.4 总结
        • 4.5 Runloop与Autoreleasepool的结合
          • 4.6 OOM的原因分析
          • 五、其他
          • 5.1可用的线上内存分析工具
            • 5.2 不同CF版本的代码对比
            • 六、问题解决
            • 七、复盘与总结
            相关产品与服务
            云直播
            云直播(Cloud Streaming Services,CSS)为您提供极速、稳定、专业的云端直播处理服务,根据业务的不同直播场景需求,云直播提供了标准直播、快直播、云导播台三种服务,分别针对大规模实时观看、超低延时直播、便捷云端导播的场景,配合腾讯云视立方·直播 SDK,为您提供一站式的音视频直播解决方案。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档