之前在做XXXSDK的时候,我hook的UITableView
的setDelegate:
方法。整个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
的判断。
按照我们的预期设想,存在两种时间顺序情况:
- 我们的init先执行,此时肯定会进入我们设置默认的逻辑;然后当外部代码调用
tableview.delegate = xxx
的时候,会把我们这个替换掉,不会影响正常的逻辑。 - 我们的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_retain
,bl objc_release
,不用管,反正都是ARC帮我们自动插入的。 - 可以看出,当传入给UITableViewController的tableView含有
dataSource
和delegate
,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。