UIKit解剖(-)逆向UITableViewController分析Bug
source link: http://satanwoo.github.io/2017/08/06/UITableViewController/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
UIKit解剖(-)逆向UITableViewController分析Bug
之前在做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。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK