值传递?址传递,慎用形参,崩溃修复记录

查询崩溃问题流程

  1. 拿到崩溃日志

  2. 查看崩溃线程、崩溃原因

  3. 查看崩溃函数堆栈

  4. 确定崩溃调用参数

  5. 根据控制台日志来具体分析问题

例子1:

  1. 拿到崩溃日志:
image.png
  1. 查看崩溃线程、崩溃原因
image.png

如图,崩溃线程是线程5,崩溃类型是EXC_BREAKPOINT(SIGTRAP),下表是常见的崩溃异常,可以看到EXC_BREAKPOINT(SIGTRAP)是一种调试器相关的,跟踪/断点捕获,多见于异常抛出。

UNIX 信号 注释
SIGSEGV 访问无效的内存地址。地址存在,但是应用程序无法访问。
SIGABRT 程序崩溃。由 C函数 abort() 初始化。通常意味着系统检测到某些事务出错,例如 assert() 或者 NSAssert() 校验失败。
SIGBUS 访问无效的内存地址。地址不存在,或对齐无效。(The address does not exist, or the alignment is invalid.)
SIGTRAP 调试器相关
SIGILL 尝试执行非法的、有缺陷、未知的或者需要权限的指令。
Mach 异常 描述 注释
EXC_BAD_ACCESS 错误内存访问 访问“错误”内存地址。“错误”可能指“地址不存在”或者“应用没有权限访问”。因此通常与 SIGBUSSIGSEGV 相关联。
EXC_CRASH 异常跳出 通常与 SIGABRT 相关联,意思是由于检测到代码抛出的未捕获异常而使应用程序异常退出。
EXC_BREAKPOINT 跟踪/断点捕获 通用与 SIGTRAP 相关联。可以由你自己的代码或者 NSExceptions 抛出时触发。
EXC_GUARD 违反了受保护资源的防护(Violated Guarded Resource Protection) 由违背受保护资源防护触发,例如‘某些文件描述符’。
EXC_BAD_INSTRUCTION 非法指令 通常与特定非法或未定义指令/操作数相关。
EXC_RESOURCE 资源限制 应用由于达到资源消耗限制而退出。
00000020 十六进制异常类型 非 'OS Kernel' 异常。
  1. 查看函数堆栈
image.png

如图所示,我们最后崩溃在libobjc.A.dylib的objc_opt_respondsToSelector+48的地方,实际上,这是objc是否响应selector的地方,我们可以查看objc的源码,以下选自objc4-838

// Calls [obj respondsToSelector]
BOOL
objc_opt_respondsToSelector(id obj, SEL sel)
{
#if __OBJC2__
 if (slowpath(!obj)) return NO;
 Class cls = obj->getIsa();
 if (fastpath(!cls->hasCustomCore())) {
 return class_respondsToSelector_inst(obj, sel, cls);
 }
#endif
 return ((BOOL(*)(id, SEL, SEL))objc_msgSend)(obj, @selector(respondsToSelector:), sel);
}

为了弄清楚究竟崩在哪一行,我们需要把它转成汇编

image.png

注意,我们最后走到的是+48,这并不代表我们是执行完+48所对应的代码才崩溃的,恰恰是执行上一句代码崩溃,而上一句代码转成汇编后的返回地址是+48,而上一句对应的是

if (slowpath(!obj)) return NO;

也就是说此时objc不存在,结合前面的DDLog打印函数,我们基本可以确定我们打印的对象已经被释放了,但是指针还没有清空,即指针所指向的内存已释放,而指针本身的地址不为null,所以它指向了一块不可访问的内存。我们回到第5行,ResetVTSession,来确定打印的是个啥

image.png
  1. 确定崩溃参数,还记得我们前面说的吗,崩溃的偏移号不是代表我们执行完这一句才崩溃,而是上一句,之所以显示+112偏移地址是因为上一句执行完毕的返回地址是这个,可以很清楚的看到汇编其实已经给我们注释出来了,是"ResetVtSession = %@"调用出现的问题,我们转成正常代码

    image-20220801151644486.png

现在我们确定了引发崩溃的参数 vtSession.

  1. 现在我们来具体分析一下这个函数的究竟有什么问题,其实我们都不需要具体分析自己的日志就能看出来。
image.png

问题出在这里,vtSession = NULL,这是一句没什么作用的代码,反而很有迷惑性,为什么呢?我们来分析一下这个方法想干什么,先强制编完剩下的帧VTCompressionSessionCompleteFrames,相当于快速处理完还没处理的内容,然后VTCompressionSessionInvalidate(vtSession)CFRelease(vtSession),这两步是销毁session,并释放内存,最后再把vtSession置空,看起来perfect,但是不要忘了我们的参数vtSession是值传递!换句话说我们在函数内部的vtSession只是外部调用的值拷贝,就算我们把它置为空,也不影响外部的指针不为空,下次如果有其他线程重新调进来,就会引发崩溃。所以解决方案有两种,一是改为址传递,改为

void MHH264VideoSource::ResetVtSession(VTCompressionSessionRef& vtSession)

二是仍然是值传递,不过外面手动把调用指针置空

// before:
     ResetVtSession(this->m_portrait_vtSession);
     ResetVtSession(this->m_landscape_vtSession);
// after:
    ResetVtSession(this->m_portrait_vtSession);
    this->m_portrait_vtSession = nullptr;
    ResetVtSession(this->m_landscape_vtSession);
    this->m_landscape_vtSession = nullptr;

总结

我们先通过崩溃日志确定崩溃类型和崩溃原因,然后根据崩溃堆栈来具体锁定诱发崩溃的原因,然后再回到SDK层去查看具体引发崩溃的代码和变量,最后我们再根据自己的代码来具体排查为什么会这样。

后话:虽然后来复盘的时候第5步我并没有写根据日志来排查,那是因为最后看了一圈日志最后又查回来到这个函数,发现是这里的问题,我一开始其实没看出来这里的问题,复盘的时候想写简单点,毕竟业务上的设计各家各有不同,但是最基本的程序bug却是类似的。

后记

后面再分享一下我排查的具体操作吧,总体绕了一圈弯路

  1. 首先查看调用ResetVtSession的地方是ResetVtSession(this->m_landscape_vtSession)时崩溃,表明横屏编码器不存在,但是竖屏编码器能正常释放

  2. 接着查看调用释放的地方是verifyProcess(),这是一个嗅探机制,旨在查看当前的码流是否正常发送中,如果不正常,就重新创建编码器或刷新关键帧,根据日志判断,当时检测到竖屏码流不能正常发送中,于是重新创建了一个竖屏的编码器,但是横屏的没有创建,所以这一步可以得到一个信息:横屏的编码器压根儿没有(但是不能确定是已经释放了还是根本没创建)

  3. 接着看上面的日志,发现走到了创建流程,但是到加锁创建的那一步,直接被return掉了,这里可以确定,vtSession并不为空,说明上一次释放并没有把指针置空

image-20220801154958737.png
  1. 接着就回到了上面,发现是释放函数的问题。那么为什么之前一直没出过问题呢?因为之前是启动扩展进程,每次都是新创建一个进程,所以everything is new,上一个扩展进程反正已经没了,没置空也不影响,所以一直没啥问题,但是这一次咱们是用主进程采集流并发送的,导致整一个videoSource压根儿已经创建过就不用再创建了,所以这里vt_Session指针没置空就很危险了,不光会导致下一次创建的时候创建不了,而且一旦走到析构就直接咖喱给给了。

你可能感兴趣的:(值传递?址传递,慎用形参,崩溃修复记录)