美文网首页
【iOS内功】ARM汇编实战,解析iOS14 UICollect

【iOS内功】ARM汇编实战,解析iOS14 UICollect

作者: iOS鑫 | 来源:发表于2020-12-15 13:41 被阅读0次
    image.png

    【iOS内功】ARM汇编实战,解析iOS14 UICollectionView死循环问题

    背景

    9月初iOS14正式发布,线上版本新冒出许多Crash。有一个Crash,UICollectionView刷新逻辑死循环,卡死了主线程。

    阳差阳错,中美两个程序员的“误会”造成了这个Crash。

    App有一个页面,自定义了一个XXCollectionView。XXCollectionView嵌套在Cell里,写代码的人偷懒,把delegate设置成自己。Apple工程师也不讲武德,把协议(UICollectionViewDelegate)没声明的方法转发给delegate执行,一点契约精神都没有。

    Apple框架大多不开源,内部有奇怪的逻辑,我们只能通过读汇编指令去分析。刚开始分析源码时,奇怪的逻辑让我怀疑人生,最后通过调试汇编指令,才理清楚具体的原因。

    这个案例挺经典的,分析过程中,用到了oc、lldb调试、arm等常用知识,也用到了归纳法、逻辑推理、逆向思维、抽象思维等思维方法。

    我详细总结了分析过程,和大家交流和探讨。

    iOS内功系列文章

    Crash分析的基础知识了解不多的,可以参考原来写的一些文章

    【iOS内功】Crash分析模型

    【iOS内功】深入解析Crash调用栈的内存布局

    【iOS内功】ARM黑魔法—栈桢的入栈和出栈

    【iOS内功】使用Hopper定位疑难问题

    1.0、Crash Log特征分析

    1.1、环境特征

    crash都发生在iOS14,推断是iOS14系统库逻辑改动引发的Crash。

    1.2、调用堆栈

    堆栈里有UICollectionview和CPXXProxy,接下来可以找一下,哪些页面同时用到这两个类,

    swipe点击事件
    ...
    -[UICollectionView _diffableDataSourceImpl]
    ...
    _CF_forwarding_prep_0
    ...
    -[CPXXProxy forwardInvocation]
    ...
    -[UICollectionView _diffableDataSourceImpl] (开始循环)
    ...
    _CF_forwarding_prep_0
    ...
    -[CPXXProxy forwardInvocation]
    复制代码
    
    image.png

    1.3、场景复现

    符合条件的页面很多,试了几个都复现不了。后面发现有一个页面A,abort日志里有很多swipe事件,页面也用了UICollectionview。点击页面A的各种区域,最后在点击空白处卡死了,并且复现了同样的堆栈。

    2.0、源码调试分析

    为了简洁,下面的术语用缩略词表示
    diffxx代表_diffableDataSourceImpl
    CPXXProxy代表_CPCollectionViewFlowLayoutProxy
    复制代码
    

    2.1、页面A和页面B有什么差别?

    很多页面虽然也用了UICollectionView和CPXXProxy,但不会出现这个crash。于是我找了其中一个页面,下面称为页面B,从逻辑上推导,页面A和页面B逻辑上一定有一个差异,这个差异最终造成页面A的Crash。接下来,分析页面A和B的差异。

    差异1:页面A里嵌套了UICollectionview

    页面A和页面B有一个差异点,页面A的UICollectionView有多种cell,其中一个cell里又嵌套了UICollectionView。cell里的XXCollectionview继承自系统的UICollectionview。

    差异2:页面B不会调用触发“-[CPXXProxy forwardInvocation]”

    找到关键路径的3个方法,添加符号断点。模拟Crash的操作,分析页面A和页面B调用栈的差异.

    -[UICollectionView _diffableDataSourceImpl]
    _CF_forwarding_prep_0
    -[CPXXProxy forwardInvocation]
    复制代码
    

    调试发现,页面A会调用到“-[CPXXProxy forwardInvocation]”,而页面B并不会。页面A走到forwardInvocation,说明系统给CPXXProxy发送了未实现的方法,这个方法是“_diffableDataSourceImpl”。

    2.2、_diffableDataSourceImpl方法是哪里定义的?

    iOS13引入了DiffableDataSource,帮助UITableView和UICollection更方便地实现局部刷新。对外开发的类是NSDiffableDataSourceSnapshot,并没有diffxx方法。

    使用runtime的接口,导出UICollectionView所有的方法,发现里面包括diffxx,diffxx方法并没有对外开放,是一个私有方法。

    2.3、页面A为什么会调用到"-[CPXXProxy _diffableDataSourceImpl]"

    diffxx是UICollectionView自己的方法,为什么转发给delegate,这是挺奇怪的逻辑。

    CPXXProxy被设置为UICollectionView的delegate,它会接收到UICollectionViewDelegate协议声明的方法,但UICollectionViewDelegate里并没有diffxx方法,理论上不应该触发这个方法的调用。

    "-[CPXXProxy _diffableDataSourceImpl]"是UIKit内部逻辑触发的,而UIkit的源码没有开源,所以接下来只能调试Arm汇编继续分析。

    3.0、汇编调试分析

    我们应该从哪个方法入手?梳理一下思路。

    “页面A为什么会出现异常”?触发异常逻辑肯定有一个源点,在这个源点之前页面A的逻辑也应该是正常的。

    页面B作为正常的参照物,我们要找到它和页面A出现逻辑分叉的地方。对比页面A和页面B的调用栈,它们最后一个相同的方法是“-[UICollectionView _diffableDataSourceImpl]”,逻辑分叉就在这个方法里,因此我们就从这个方法入手分析。

    3.1、“-[UICollectionView _diffableDataSourceImpl]”哪行指令出现逻辑分叉?

    页面A指令

    image.png

    w0寄存器的值是1,没有命中tbz指令的跳转,按顺序继续执行下一行指令。一直执行到bl 0x196d04850指令,跳转到0x196d04850。

    注1:tbz指令
    复制代码
    
    image.png

    bl 0x196d04820里面经过几次跳转,最后执行objc_msgSend方法。根据寄存器的值,objc_msgSend里的target是“CPXXProxy”,selector就是是"diffxx"

    image.png

    CPXXProxy里并没有实现diffxx函数,进行消息转发_CF_forwarding_prep_0

    页面B指令

    image.png

    w0寄存器的值是0,命中tbz指令的跳转,跳转到0x1991c12ac继续执行指令,后续也没有调用到CPXXProxy的方法。

    结论

    页面A和页面B的分叉点在tbz指令。tbz是一个条件跳转,页面A里tbz的测试值w0为1,页面B的测试值为0,最后走到了不同的逻辑。

    3.2、w0的差异,是哪里造成的?

    我们要找的关键指令就是“tbz w0 #0x0”,下面分析哪里将w0的值改为1。

    image.png

    执行"bl 0x197157750"指令前x0寄存器还是一个对象,执行后x0寄存器就成了1,说明这个方法调用的返回值就是1,也就是true。

    image.png

    进入"bl 0x197157750"调试,发现最终调用的方法是“CPXXXProxy respondsToSelector”,这个方法的返回值是true。也就是说,调用“-[CPXXXProxy respondsToSelector]”方法时,页面A和页面B结果不一样。

    3.3、为什么“-[CPXXXProxy respondsToSelector]”的返回值不一样

    CPXXXProxy有源码,直接分析源码的逻辑。

    image.png
    CPXXXProxy 简介
    
    CPXXXProxy对象里有一个target属性,它是Cell里嵌套的XXCollectionView。
    
    XXCollectionView的delegate和datasource设置为CPXXXProxy,collectionVie的回调方法先发给CPXXXProxy,CPXXXProxy接管了部分方法,自己不接管的回调方法,CPXXXProxy会再转发给target自行处理。
    复制代码
    

    根据截图显示,调用了“-[_target respondsToSelector:aSelector]”,运行结果是true。说明页面A的_target实现了“_diffableDataSourceImpl”方法,而页面B的_target并没有实现。

    在页面A,XXCollectionView嵌套在cell里,CPXXXProxy的target设置为XXCollectionView。而在页面B,XXCollectionView没有嵌套,CPXXXProxy的target设置为页面B的页面控制器,XXViewController。

    “_diffableDataSourceImpl”本身就是UICollectionView的方法,而XXCollectionView继承于UICollectionView,结果当然是true。

    4.0、总结

    分析到这里,已经豁然开朗,下图是死循环的调用链路。

    image.png
    • =>"-[XXCollectionView _diffableDataSourceImpl"]"
    • =>"-[CPXXXProxy respondsToSelector:@"_diffableDataSourceImpl"]"
      • => "-[XXCollectionView respondsToSelector:@"_diffableDataSourceImpl"]" 结果是True
    • =>"-[CPXXXProxy _diffableDataSourceImpl"]"
    • =>"-[CPXXXProxy forwardInvocation"]"
    • =>"-[XXCollectionView _diffableDataSourceImpl"]"
    • =>...死循环

    参考

    注1:tbz指令

    测试位为0发生跳转,imm指定目的寄存器的某一个位,『b5:b40』组成,0-63或者0-31,有b5决定。哪个目的寄存器由Rt指定,label是偏移地址。

    TBNZ介绍 www.cnblogs.com/rongmouzhan…

    收录:原文地址

    相关文章

      网友评论

          本文标题:【iOS内功】ARM汇编实战,解析iOS14 UICollect

          本文链接:https://www.haomeiwen.com/subject/zwcwgktx.html