UIKit解剖(-)逆向UITableViewController分析Bug

之前在做XXXSDK的时候,我hook的UITableViewsetDelegate:方法。整个SDK在接入手淘、天猫以及闲鱼等其他App的时候都没啥问题。

上周,UC的同学突然找到说,给我说了如下图所示的问题:

商业保密,不显示了

卧槽,这下我就懵逼了,看样子是把整个rowHeight给Hook坏了,那这是为什么呢?

从开发UITableView的正向角度来说:我们一般都需要给其提供一个必选的UITableViewDataSource和一个可选的UITableViewDelegate,其中,涉及到高度的是如下这个API:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;

有人说可以直接通过tableview.rowHeight设置高度,但是对于不同cell不同高度的动态需求,但是这里我们暂不提这种分支情况。

通过UC同学的协助,我们发现了如下输出:

通过输出不难发现,是最后的delegate被从对应的UIViewController改成了一个乱七八糟没实现对应heightForRowAtIndexPath方法的对象。

为什么会这样呢?

通过如下图所示的调用栈,

调用栈最下层是UC同学的代码;

self.tableview = [[xxxTableView alloc] init]

调用栈最上层是我们的一层防护性hook,其代码如下:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Swizzle([UIScrollView class], @selector(init), @selector(swizzled_init));
    });
}

- (instancetype)swizzled_init
{
    id obj = [self swizzled_init];
    UIScrollView *scrollView = (UIScrollView *)obj;
    if (!scrollView.delegate) {
        //scrollView.delegate = [UIScrollViewDelegateDummyStub sharedStub];
    }
    return obj;
}

这段代码是什么作用呢?

我们之前提了UITableViewDelegate不是必需,因此,为了能够抓去所有UITableView的代码,我们会提供一个内置的默认delegate(当时的实现存在bug,没有实现heightForRowAtIndexPath方法)

而且,为了防止我们的delegate覆盖了有delegate的情况,我们还特地做了!scroll.delegate的判断。

按照我们的预期设想,存在两种时间顺序情况:

  1. 我们的init先执行,此时肯定会进入我们设置默认的逻辑;然后当外部代码调用tableview.delegate = xxx的时候,会把我们这个替换掉,不会影响正常的逻辑。
  2. 我们的init后执行(比如某些子类覆盖的情况),那这样的话,当子类已经设置好delegate后,压根不会进入我们的设置逻辑。

然而,就是这一小段看起来无错的代码导致了UC的App出现了文章开头的Bug。

逆向分析UITableViewController

基于10.2的UIKit,我们通过汇编来分析-[UITableViewController setTableView:]的流程:

     // 保存寄存器
->  0x18c84c640 <+0>:   stp    x26, x25, [sp, #-0x50]!
    0x18c84c644 <+4>:   stp    x24, x23, [sp, #0x10]
    0x18c84c648 <+8>:   stp    x22, x21, [sp, #0x20]
    0x18c84c64c <+12>:  stp    x20, x19, [sp, #0x30]
    0x18c84c650 <+16>:  stp    x29, x30, [sp, #0x40]

    // 获取原先UITableViewController的旧tableView
    0x18c84c654 <+20>:  add    x29, sp, #0x40            ; =0x40 
    0x18c84c658 <+24>:  mov    x20, x0
    0x18c84c65c <+28>:  mov    x0, x2
    0x18c84c660 <+32>:  bl     0x1851c8090               ; objc_retain
    0x18c84c664 <+36>:  mov    x19, x0
    0x18c84c668 <+40>:  adrp   x8, 124100
    0x18c84c66c <+44>:  ldr    x1, [x8, #0xd78]
    0x18c84c670 <+48>:  mov    x0, x20
    0x18c84c674 <+52>:  bl     0x1851c2f60               ; objc_msgSend
    0x18c84c678 <+56>:  mov    x29, x29
    0x18c84c67c <+60>:  bl     0x1851ca48c               ; objc_retainAutoreleasedReturnValue

    // 比较旧的tableview和新的tableview
    0x18c84c680 <+64>:  mov    x21, x0
    0x18c84c684 <+68>:  cmp    x21, x19

    // 如果两个tableView一致,直接返回
    0x18c84c688 <+72>:  b.eq   0x18c84c7d4               ; <+404>

    // 获取旧的tableView的datasource
    0x18c84c68c <+76>:  adrp   x8, 124074
    0x18c84c690 <+80>:  ldr    x23, [x8, #0x2e0]
    0x18c84c694 <+84>:  mov    x0, x21
    0x18c84c698 <+88>:  mov    x1, x23
    0x18c84c69c <+92>:  bl     0x1851c2f60               ; objc_msgSend
    0x18c84c6a0 <+96>:  mov    x29, x29
    0x18c84c6a4 <+100>: bl     0x1851ca48c               ; objc_retainAutoreleasedReturnValue


    // 从self的成员对象便宜792处取出UIFilteredDataSource
    0x18c84c6a8 <+104>: mov    x22, x0
    0x18c84c6ac <+108>: adrp   x8, 124145
    0x18c84c6b0 <+112>: ldrsw  x8, [x8, #0x7ac]
    0x18c84c6b4 <+116>: ldr    x8, [x20, x8]
    0x18c84c6b8 <+120>: cmp    x22, x20
    0x18c84c6bc <+124>: ccmp   x22, x8, #0x4, ne

    // 如果不一致,把旧的tableview的datasource 置为nil
    0x18c84c6c0 <+128>: b.ne   0x18c84c6d8               ; <+152>
    0x18c84c6c4 <+132>: adrp   x8, 124073
    0x18c84c6c8 <+136>: ldr    x1, [x8, #0x3c0]
    0x18c84c6cc <+140>: mov    x2, #0x0
    0x18c84c6d0 <+144>: mov    x0, x21
    0x18c84c6d4 <+148>: bl     0x1851c2f60               ; objc_msgSend

    // 获取旧的tableview的delegate
    0x18c84c6d8 <+152>: adrp   x8, 124074
    0x18c84c6dc <+156>: ldr    x24, [x8, #0x7d8]
    0x18c84c6e0 <+160>: mov    x0, x21
    0x18c84c6e4 <+164>: mov    x1, x24
    0x18c84c6e8 <+168>: bl     0x1851c2f60               ; objc_msgSend
    0x18c84c6ec <+172>: mov    x29, x29
    0x18c84c6f0 <+176>: bl     0x1851ca48c               ; objc_retainAutoreleasedReturnValue
        0x18c84c6f4 <+180>: mov    x25, x0
    0x18c84c6f8 <+184>: bl     0x1851c8150               ; objc_release

    // 判断旧的delegate是不是当前的UITableViewController
    0x18c84c6fc <+188>: cmp    x25, x20
    0x18c84c700 <+192>: b.ne   0x18c84c718               ; <+216>
    0x18c84c704 <+196>: adrp   x8, 124073
    0x18c84c708 <+200>: ldr    x1, [x8, #0x3c8]

    // 如果不是,就把旧的tableview的delegate置为nil
    0x18c84c70c <+204>: mov    x2, #0x0
    0x18c84c710 <+208>: mov    x0, x21
    0x18c84c714 <+212>: bl     0x1851c2f60               ; objc_msgSend
    0x18c84c718 <+216>: adrp   x8, 124080
    0x18c84c71c <+220>: ldr    x1, [x8, #0xe80]
    0x18c84c720 <+224>: mov    x0, x21
    0x18c84c724 <+228>: bl     0x1851c2f60               ; objc_msgSend
    0x18c84c728 <+232>: mov    x29, x29
    0x18c84c72c <+236>: bl     0x1851ca48c               ; objc_retainAutoreleasedReturnValue
    0x18c84c730 <+240>: mov    x25, x0

    // 将uitableviewcontroller的tableview通过setView:置为新的
    0x18c84c734 <+244>: adrp   x8, 124076
    0x18c84c738 <+248>: ldr    x1, [x8, #0x4b0]
    0x18c84c73c <+252>: mov    x0, x20
    0x18c84c740 <+256>: mov    x2, x19
    0x18c84c744 <+260>: bl     0x1851c2f60               ; objc_msgSend

    // 新的tableview的datasource判断是不是为空,为空通过_applyDefaultDataSourceToTable将其UIFilteredDataSource
    0x18c84c748 <+264>: adrp   x8, 124080
    0x18c84c74c <+268>: ldr    x1, [x8, #0x810]
    0x18c84c750 <+272>: mov    x0, x19
    0x18c84c754 <+276>: mov    x2, x25
    0x18c84c758 <+280>: bl     0x1851c2f60               ; objc_msgSend
    0x18c84c75c <+284>: mov    x0, x19
    0x18c84c760 <+288>: mov    x1, x23
    0x18c84c764 <+292>: bl     0x1851c2f60               ; objc_msgSend
    0x18c84c768 <+296>: mov    x29, x29
    0x18c84c76c <+300>: bl     0x1851ca48c               ; objc_retainAutoreleasedReturnValue
    0x18c84c770 <+304>: mov    x23, x0
    0x18c84c774 <+308>: bl     0x1851c8150               ; objc_release
    0x18c84c778 <+312>: cbnz   x23, 0x18c84c790          ; <+336>
    0x18c84c77c <+316>: adrp   x8, 124100
    0x18c84c780 <+320>: ldr    x1, [x8, #0xd80]
    0x18c84c784 <+324>: mov    x0, x20
    0x18c84c788 <+328>: mov    x2, x19
    0x18c84c78c <+332>: bl     0x1851c2f60               ; objc_msgSend

    // 新的tableview的delegaate判断是不是为空,为空通过将delegate置为self(即当前的UITableViewController)
    0x18c84c790 <+336>: mov    x0, x19
    0x18c84c794 <+340>: mov    x1, x24
    0x18c84c798 <+344>: bl     0x1851c2f60               ; objc_msgSend
    0x18c84c79c <+348>: mov    x29, x29
    0x18c84c7a0 <+352>: bl     0x1851ca48c               ; objc_retainAutoreleasedReturnValue
    0x18c84c7a4 <+356>: mov    x23, x0
    0x18c84c7a8 <+360>: bl     0x1851c8150               ; objc_release


    0x18c84c7ac <+364>: cbnz   x23, 0x18c84c7c4          ; <+388>
    0x18c84c7b0 <+368>: adrp   x8, 124073
    0x18c84c7b4 <+372>: ldr    x1, [x8, #0x3c8]
    0x18c84c7b8 <+376>: mov    x0, x19
    0x18c84c7bc <+380>: mov    x2, x20
    0x18c84c7c0 <+384>: bl     0x1851c2f60               ; objc_msgSend
    0x18c84c7c4 <+388>: mov    x0, x25
    0x18c84c7c8 <+392>: bl     0x1851c8150               ; objc_release
    0x18c84c7cc <+396>: mov    x0, x22
    0x18c84c7d0 <+400>: bl     0x1851c8150               ; objc_release
    0x18c84c7d4 <+404>: mov    x0, x21
    0x18c84c7d8 <+408>: bl     0x1851c8150               ; objc_release

    // 恢复寄存器

    0x18c84c7dc <+412>: mov    x0, x19
    0x18c84c7e0 <+416>: ldp    x29, x30, [sp, #0x40]
    0x18c84c7e4 <+420>: ldp    x20, x19, [sp, #0x30]
    0x18c84c7e8 <+424>: ldp    x22, x21, [sp, #0x20]
    0x18c84c7ec <+428>: ldp    x24, x23, [sp, #0x10]
    0x18c84c7f0 <+432>: ldp    x26, x25, [sp], #0x50
    0x18c84c7f4 <+436>: b      0x1851c8150               ; objc_release
  • 一看到adrp, ldr的搭配,基本可以确定是取某个方法进行调用。
  • 看到一大堆的bl objc_retainbl objc_release,不用管,反正都是ARC帮我们自动插入的。
  • 可以看出,当传入给UITableViewController的tableView含有dataSourcedelegate,UITableViewController都不会对其进行处理;否则会进行一个默认的设置。

我自己理解后转写的伪代码如下:

UITableView *oldTableView = [self __existingTableView]; 

if (oldTableView == xxxtableView) {
    return 
} else {
    id oldDataSource = [oldTableView dataSource];
    // x21 被赋值成了oldTableView, x22 oldDataSource

    // 取UITableViewController 792偏移的成员变量 filteredDataSource
    id filteredDataSource = [self _filteredDataSource];

    if (oldDataSource != filteredDataSource) 
    {

    } else {
        [oldTableView setDataSource:nil];
    }

    id oldDelegate = [oldTableView delegate];
    // x25 被赋值了oldDelegate

    if (oldeDelegate != self)
    {
        goto //
    } else {
        [oldTableView setDelegate:nil];
    }

    id oldRefreshControl = [oldTableView _refreshControl];
    // x25 被赋值了oldRefreshControl

    [self setView:xxtableView];
    [xxxtableView _setRefreshControl:oldRefreshControl];

    id newDataSource = [xxxtableview dataSource];
    if (!newDataSource) {
        [self _applyDefaultDataSourceToTable:xxxTableView];
    }

    id newDelegate = [xxxtableview delegate];
    if (!newDelegate) {
        [xxxTableView setDelegate:self];
    }
}

结论

通过上面对汇编和伪代码的理解,我们可以很轻易的得出结论:当我们处于第一种情形的实现,我们将tableview.delegate设置成了我们的stub。因为不为空,所以UITableViewController默认不会对其进行处理,而由于我们当时没有提供stub对于heightForRowAtIndexPath的实现,导致出现了UC的bug。