QQ爱 —— 记修复QQ International iOS客户端闪退bug的一次完整过程

前言

QQ在互联网发展史中的重要地位毋庸置疑。尽管近些年由于自身的膨胀和微信的崛起,QQ的人气有些下跌,但多数网虫在上网时仍会挂着QQ —— 它早已融入了我们的生活。

上周在网上闲逛时,无意中看到了QQ国际版的iOS客户端 —— QQ International,它的应用名叫QQ爱(QQi),跟我上中学时听过的一首网络歌曲同名。看了一下App Store里的App截图,简单利落,是我喜欢的风格;更重要的是,包体不到65M,比原版QQ小了近一半,摒弃了很多乱七八糟的东西。我马上就删掉了原版QQ,换成了QQ爱:relaxed:

昨日在外玩耍时,一位好友给我发来一条消息。我打开QQ爱,刚想点进对话列表给他回复,没想到QQ爱竟然闪退了——

我的第一反应当然是插件冲突。因为对于最新的盘古越狱,重启可以禁用所有的插件,所以我重启iPhone,又测试了几遍,发现问题仍然存在。

因为闪退发生在QQ爱 主界面最常见场景,所以一开始我是不相信QQ爱有这样明显bug的。但是看了App Store上的网友评价,10条里竟然有2条反馈类似的问题——

我开始怀疑,网友给QQ爱的1颗半星并非空穴来风,这款App确实存在严重bug。网友的评论都是上个月发布的,距今已过去近20天,bug都没有得到修复,难道是QQ爱团队太忙了吗?

写完《我的失败与伟大》系列创业心得后,我的空闲时间稍微多了些;那我就帮人帮到底,把这个bug给修复了吧:wink:

以下操作的环境是iPhone SE,越狱iOS 9.3.3,QQ International 4.8.0。

观察闪退规律

结束昨日的奔波,回到狭小的出租屋后,顶着酷暑,在昏暗的台灯下,一枚屌丝,也就是我,又将QQ爱来回把玩了好多遍,想要观察它闪退的规律;结果发现,并不是所有的对话都会导致闪退,而我也看不出来问题对话与正常对话的区别。值得庆幸的是,问题对话一定会导致闪退,bug可以重现。无需多言,我们可以开始从代码层面分析问题了。

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

绝大多数iOS工程师最熟悉的界面类,一定是UITableView。这个类里的- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath,是当某个cell被点击时得到触发的函数。因为闪退正是发生在点击某一个代表对话的cell之后,那么从这个函数入手,看看是不是它的问题,就是再正常不过的想法了。

接下来的问题是,QQ首页的对话界面,隶属于哪个类?- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath来自于哪个类?别急,我们一步一步来。

找到QQ首页对话界面的controller

由V寻找C,是书和论坛已经讲解过一万遍的操作,我再啰嗦就不好了。直接上代码(因Cycript还不兼容9.3.3,我们用LLDB代替)——

先用debugserver启动(或附加)QQ爱:

FunMaker-SE:~ root# debugserver *:1234 -x backboard /var/containers/Bundle/Application/0B8733CF-9B1B-40C0-B8DF-AF91C874932B/QQ.app/QQ
debugserver-@(#)PROGRAM:debugserver  PROJECT:debugserver-340.3.124
 for arm64.
Listening to port 1234 for a connection from *...

然后用LLDB连过去:

(lldb) process connect connect://localhost:1234
Process 1409 stopped
* thread #1: tid = 0x8f25, 0x000000012003d000 dyld`_dyld_start, stop reason = signal SIGSTOP
    frame #0: 0x000000012003d000 dyld`_dyld_start
dyld`_dyld_start:
->  0x12003d000 <+0>:  mov    x28, sp
    0x12003d004 <+4>:  and    sp, x28, #0xfffffffffffffff0
    0x12003d008 <+8>:  movz   x0, #0
    0x12003d00c <+12>: movz   x1, #0
(lldb) c
Process 1409 resuming

待QQ爱启动完成后,打印出当前界面的结构:

Process 1409 stopped
* thread #1: tid = 0x8f25, 0x00000001809c0fd8 libsystem_kernel.dylib`mach_msg_trap + 8, stop reason = signal SIGSTOP
    frame #0: 0x00000001809c0fd8 libsystem_kernel.dylib`mach_msg_trap + 8
libsystem_kernel.dylib`mach_msg_trap:
->  0x1809c0fd8 <+8>: ret    

libsystem_kernel.dylib`mach_msg_overwrite_trap:
    0x1809c0fdc <+0>: movn   x16, #0x1f
    0x1809c0fe0 <+4>: svc    #0x80
    0x1809c0fe4 <+8>: ret    
(lldb) po [[UIApp keyWindow] recursiveDescription]
<UIWindow: 0x14dfa5780; frame = (0 0; 320 568); opaque = NO; autoresize = LM+RM+TM+BM; gestureRecognizers = <NSArray: 0x14dfa71a0>; layer = <UIWindowLayer: 0x14dfa4000>>
   | <UILayoutContainerView: 0x14f25da00; frame = (0 0; 320 568); autoresize = W+H; layer = <CALayer: 0x14f25d5e0>>
   |    | <UITransitionView: 0x14f25e6c0; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x14dd0d300>>
...
<QQMessageViewCell: 0x14e2d1c00; baseClass = UITableViewCell; frame = (0 114; 320 70); autoresize = W; layer = <CALayer: 0x14f5dbb40>>
   |    |    |    |    |    |    |    |    |    |    |    | <UITableViewCellContentView: 0x14f5db6b0; frame = (0 0; 320 69.5); gestureRecognizers = <NSArray: 0x14f5dc300>; layer = <CALayer: 0x14f5daf00>>
   |    |    |    |    |    |    |    |    |    |    |    |    | <UILabel: 0x14f5dcab0; frame = (71 13.68; 176 18.64); text = '乃斯尔拉 新疆阿图什'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x14f5dc970>>
...
(lldb) 

然后一路nextResponder,直到C出现:

(lldb) po [0x14e2d1c00 nextResponder]
<UITableViewWrapperView: 0x14e228200; frame = (0 0; 320 499); gestureRecognizers = <NSArray: 0x14f3e40f0>; layer = <CALayer: 0x14f3e44b0>; contentOffset: {0, 0}; contentSize: {320, 499}>

(lldb) po [0x14e228200 nextResponder]
<QQMessageView: 0x14e21aa00; baseClass = UITableView; frame = (0 0; 320 519); gestureRecognizers = <NSArray: 0x14f208ae0>; layer = <CALayer: 0x14dd09ff0>; contentOffset: {0, -20}; contentSize: {320, 450}>

(lldb) po [0x14e21aa00 nextResponder]
<QQView: 0x14f236df0; frame = (0 0; 320 519); clipsToBounds = YES; layer = <CALayer: 0x14f202f30>>

(lldb) po [0x14f236df0 nextResponder]
<UIView: 0x14f226160; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x14f1f7a60>>

(lldb) po [0x14f226160 nextResponder]
<QQRecentController: 0x14e83ea00>

在Hopper里查看[QQRecentController tableView: didSelectRowAtIndexPath:]

Clutch将QQ爱砸壳,然后丢到Hopper里分析;分析结束后,定位到[QQRecentController tableView: didSelectRowAtIndexPath:]

可以看到,这个函数挺大,里面内容挺多。为了检查这个函数是否有问题,我们可以在函数的开头下断点,然后一直ni下去,看程序什么时候崩;但这个工作量无疑是比较大的。

为了简化操作,我们把断点下在函数的最后一条指令上,然后看看会不会触发。如果触发了,说明函数没问题;如果没触发就崩了,说明就是[QQRecentController tableView: didSelectRowAtIndexPath:]的问题,咱们再着重分析这个函数。

这个函数的最后一条指令是:

0x00000001003eff0c         ret

下个断点:

(lldb) im li -o
[  0] 0x00000000000b0000
...
(lldb) br s -a 0x00000000000b0000+0x00000001003eff0c
Breakpoint 1: where = QQ`___lldb_unnamed_function13087$$QQ + 4284, address = 0x000000010049ff0c

然后点击问题对话cell,看看效果:

Process 1409 stopped
* thread #1: tid = 0x8f25, 0x000000010049ff0c QQ`___lldb_unnamed_function13087$$QQ + 4284, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x000000010049ff0c QQ`___lldb_unnamed_function13087$$QQ + 4284
QQ`___lldb_unnamed_function13087$$QQ:
->  0x10049ff0c <+4284>: ret    
    0x10049ff10 <+4288>: mov    x0, x21
    0x10049ff14 <+4292>: mov    x1, x27
    0x10049ff18 <+4296>: bl     0x101b06450               ; symbol stub for: objc_msgSend
(lldb) ni
Process 1409 stopped
* thread #1: tid = 0x8f25, 0x00000001860d7dc4 UIKit`-[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1316, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x00000001860d7dc4 UIKit`-[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:] + 1316
UIKit`-[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:]:
->  0x1860d7dc4 <+1316>: mov    x0, x20
    0x1860d7dc8 <+1320>: bl     0x1804c0150               ; objc_release
    0x1860d7dcc <+1324>: adrp   x8, 101151
    0x1860d7dd0 <+1328>: ldr    x0, [x8, #1192]
(lldb)  

ni后,我们进入了UIKit中,说明断点所在的就是最后一条指令;[QQRecentController tableView: didSelectRowAtIndexPath:]没毛病。这就尴尬了:cry:

跟我一起思考:
点击问题对话cell,触发[QQRecentController tableView: didSelectRowAtIndexPath:];程序不崩,说明这个函数没问题,那么有问题的函数一定在后面。这种情况下,我们怎么寻找问题函数呢?还得从界面下手。

点击正常对话cell,在界面上可以观察到的现象是:首页对话界面消失,私聊界面出现。这几个现象,正好可以对应一系列函数。对于首页对话界面来说,触发的函数有:

- (void)viewWillDisappear:(BOOL)animated;
- (void)viewDidDisappear:(BOOL)animated;

对于私聊界面来说,触发的函数有:

- (void)viewDidLoad;
- (void)viewWillAppear:(BOOL)animated;
- (void)viewDidAppear:(BOOL)animated;

这些函数都在[QQRecentController tableView: didSelectRowAtIndexPath:]之后调用,都有很大的嫌疑;但真相只有一个,让我们各个击破。

[QQRecentController viewWillDisappear:]

这个函数的最后一条指令是:

0x00000001003f0a3c         ret

下断点:

(lldb) br s -a 0x00000000000b0000+0x00000001003f0a3c
Breakpoint 2: where = QQ`___lldb_unnamed_function13090$$QQ + 208, address = 0x00000001004a0a3c
(lldb) c
Process 1409 resuming
Process 1409 stopped
* thread #1: tid = 0x8f25, 0x00000001004a0a3c QQ`___lldb_unnamed_function13090$$QQ + 208, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x00000001004a0a3c QQ`___lldb_unnamed_function13090$$QQ + 208
QQ`___lldb_unnamed_function13090$$QQ:
->  0x1004a0a3c <+208>: ret    

QQ`___lldb_unnamed_function13091$$QQ:
    0x1004a0a40 <+0>:   stp    x29, x30, [sp, #-16]!
    0x1004a0a44 <+4>:   mov    x29, sp
    0x1004a0a48 <+8>:   str    x0, [sp, #-16]!
(lldb) ni
Process 1409 stopped
* thread #1: tid = 0x8f25, 0x0000000185fb9434 UIKit`-[UIViewController _setViewAppearState:isAnimating:] + 820, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000185fb9434 UIKit`-[UIViewController _setViewAppearState:isAnimating:] + 820
UIKit`-[UIViewController _setViewAppearState:isAnimating:]:
->  0x185fb9434 <+820>: adrp   x8, 101389
    0x185fb9438 <+824>: ldr    x24, [x8, #3448]
    0x185fb943c <+828>: mov    x0, x19
    0x185fb9440 <+832>: mov    x1, x24
(lldb)  

[QQRecentController tableView: didSelectRowAtIndexPath:]类似,此函数嫌疑排除;继续下一个。

[QQRecentController viewDidDisappear:]

这个函数的最后一条指令是:

0x00000001003f0a94         ret

下断点:

(lldb) br s -a 0x00000001003f0a94+0x00000000000b0000
Breakpoint 1: where = QQ`___lldb_unnamed_function13091$$QQ + 84, address = 0x00000001004a0a94
Process 1433 stopped
* thread #1: tid = 0x8f25, 0x00000001001dea50 QQ`___lldb_unnamed_function5198$$QQ + 28, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x18)
    frame #0: 0x00000001001dea50 QQ`___lldb_unnamed_function5198$$QQ + 28
QQ`___lldb_unnamed_function5198$$QQ:
->  0x1001dea50 <+28>: ldr    w0, [x0, #24]
    0x1001dea54 <+32>: mov    sp, x29
    0x1001dea58 <+36>: ldp    x29, x30, [sp], #16
    0x1001dea5c <+40>: ret    
(lldb)  

程序崩了,且停在了0x1001dea50 - 0xb0000 = 0x10012EA50处。当这种情况出现时,我们就不需要再排除其他函数了,问题就出在0x10012EA50附近;过去看看。

[NSDate getDayOfWeekOfTime:]

这是一个很简单的函数,引起我们注意的,是其中唯一的一个BL;它调用了localtime函数。

localtime的功能比较简单,即:

Convert a time value to a broken-down local time.

即把一个时间戳给格式化成本地时间。它的参数是一个指向time_t的指针,返回一个指向tm结构体的指针。这个函数会导致什么问题呢?

localtime下断点,我们看看程序是怎么崩的;注意,因为刚才QQ爱崩掉了,我又重启了程序。此时QQ爱的ASLR偏移已经变了:

(lldb) im li -o
[  0] 0x00000000000d8000
...
(lldb) br s -a 0x00000000000d8000+0x000000010012ea4c
Breakpoint 1: where = QQ`___lldb_unnamed_function5198$$QQ + 24, address = 0x0000000100206a4c
Process 1775 stopped
* thread #1: tid = 0x104d0, 0x0000000100206a4c QQ`___lldb_unnamed_function5198$$QQ + 24, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100206a4c QQ`___lldb_unnamed_function5198$$QQ + 24
QQ`___lldb_unnamed_function5198$$QQ:
->  0x100206a4c <+24>: bl     0x101b2edc8               ; symbol stub for: localtime
    0x100206a50 <+28>: ldr    w0, [x0, #24]
    0x100206a54 <+32>: mov    sp, x29
    0x100206a58 <+36>: ldp    x29, x30, [sp], #16
(lldb) p $x0
(unsigned long) $0 = 6171021788
(lldb) x/1 $x0
0x16fd251dc: 0x579f951b
(lldb) ni
Process 1775 stopped
* thread #1: tid = 0x104d0, 0x0000000100206a50 QQ`___lldb_unnamed_function5198$$QQ + 28, queue = 'com.apple.main-thread', stop reason = instruction step over
    frame #0: 0x0000000100206a50 QQ`___lldb_unnamed_function5198$$QQ + 28
QQ`___lldb_unnamed_function5198$$QQ:
->  0x100206a50 <+28>: ldr    w0, [x0, #24]
    0x100206a54 <+32>: mov    sp, x29
    0x100206a58 <+36>: ldp    x29, x30, [sp], #16
    0x100206a5c <+40>: ret    
(lldb) p $x0
(unsigned long) $2 = 0
(lldb) ni
Process 1775 stopped
* thread #1: tid = 0x104d0, 0x0000000100206a50 QQ`___lldb_unnamed_function5198$$QQ + 28, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x18)
    frame #0: 0x0000000100206a50 QQ`___lldb_unnamed_function5198$$QQ + 28
QQ`___lldb_unnamed_function5198$$QQ:
->  0x100206a50 <+28>: ldr    w0, [x0, #24]
    0x100206a54 <+32>: mov    sp, x29
    0x100206a58 <+36>: ldp    x29, x30, [sp], #16
    0x100206a5c <+40>: ret    
(lldb)  

可以看到,造成QQ爱崩溃的,正是0x100206a50 <+28>: ldr w0, [x0, #24],我们来近距离观察一下罪魁祸首,搞清问题出现的来龙去脉。

struct tm *localtime(const time_t *time)

还记得书中的金句吗?我把它扩充了一下:

(对于armv7和armv7s来说,)函数的前4个参数存放在R0到R3中,其他参数存放在栈中;返回值放在R0中。对于arm64来说,函数的参数存放在X0到Xn中,返回值放在X0中。

当进程执行到0x100206a50 <+28>: ldr w0, [x0, #24]时,我打印出了当前的X0,即localtime函数的返回值,它是0。

但是,我们期望的返回值,不应该是一个指向tm结构体的指针吗?我们来推测一下ldr w0, [x0, #24]的意图。

tm结构体是这么定义的:

int    tm_sec   Seconds [0,60]. 
int    tm_min   Minutes [0,59]. 
int    tm_hour  Hour [0,23]. 
int    tm_mday  Day of month [1,31]. 
int    tm_mon   Month of year [0,11]. 
int    tm_year  Years since 1900. 
int    tm_wday  Day of week [0,6] (Sunday =0). 
int    tm_yday  Day of year [0,365]. 
int    tm_isdst Daylight Savings flag. 

在arm64中,每个int类型的对象大小为4字节(大家可以用sizeof函数自己验证一下);因为X0是指向tm结构体的指针,所以X0tm中每个成员的关系是这样的:

tm_sec   [X0] 
tm_min   [X0 + #4] 
tm_hour  [X0 + #8]
tm_mday  [X0 + #12]
tm_mon   [X0 + #16]
tm_year  [X0 + #20]
tm_wday  [X0 + #24]
tm_yday  [X0 + #28]
tm_isdst [X0 + #32]

也就是说,ldr w0, [x0, #24]的目的,就是把tm结构体的成员tm_wday读入W0,然后作为[NSDate getDayOfWeekOfTime:]的返回值;其中,W0X0的低32位,正好存储一个4字节的int类型对象。

其实,这些成员和函数的 名称,就已经可以从很大程度上验证我们的推理了:tm_wday的含义是:

Day of week [0,6] (Sunday =0).

[NSDate getDayOfWeekOfTime:]的名称吻合,它们的功能应该是一样的。

好了,struct tm *localtime(const time_t *time)的底细被我们摸清了,那问题到底出在哪里呢?

坦白说,我对这个函数的用法并不熟悉,对[NSDate getDayOfWeekOfTime:]的汇编也不敏感,看不出来它的问题。咋办呢?

我们看看正确的实现,对比一下就知道了嘛!

QQ HD

因为QQ爱目前只支持iPhone,所以我的iPad Air上保留的仍是QQ HD;在我的iPad上打开问题对话,是不会闪退的。另外,对于QQ这样的庞然大物来说,为了减轻工程师的维护负担,函数和变量的命名方式会相对统一,即对于QQ HD、QQ、QQ爱来说,一些提供通用功能的函数名,甚至是实现,应该是高度雷同的。

那么,我们就去看看QQ HD里,有没有实现[NSDate getDayOfWeekOfTime:],它的汇编代码是什么样的:

确实有这个函数,而且确实有一些差别。我把QQ爱和QQ HD的[NSDate getDayOfWeekOfTime:]以文本形式粘贴在下面,方便我们对比:

来自QQ HD的正确版本

0x000000010028a664         stp        x29, x30, [sp, #-0x10]!                   ; Objective C Implementation defined at 0x102a27180 (class)
0x000000010028a668         mov        x29, sp
0x000000010028a66c         sub        sp, sp, #0x10
0x000000010028a670         fcvtzs     x8, d0
0x000000010028a674         str        x8, [sp, #0x8]
0x000000010028a678         add        x0, sp, #0x8
0x000000010028a67c         bl         imp___stubs__localtime
0x000000010028a680         ldr        w0, [x0, #0x18]
0x000000010028a684         mov        sp, x29
0x000000010028a688         ldp        x29, x30, [sp], #0x10
0x000000010028a68c         ret        
来自QQ爱的错误版本

0x000000010012ea34         stp        x29, x30, [sp, #-0x10]!                   ; Objective C Implementation defined at 0x10215a998 (class)
0x000000010012ea38         mov        x29, sp
0x000000010012ea3c         sub        sp, sp, #0x10
0x000000010012ea40         fcvtzu     w8, d0
0x000000010012ea44         stur       w8, [x29, #-0x4]
0x000000010012ea48         sub        x0, x29, #0x4
0x000000010012ea4c         bl         imp___stubs__localtime
0x000000010012ea50         ldr        w0, [x0, #0x18]
0x000000010012ea54         mov        sp, x29
0x000000010012ea58         ldp        x29, x30, [sp], #0x10
0x000000010012ea5c         ret       

它们的差异主要集中在3行代码:

正确                                  错误
fcvtzs     x8, d0                     fcvtzu     w8, d0
str        x8, [sp, #0x8]             stur       w8, [x29, #-0x4]
add        x0, sp, #0x8               sub        x0, x29, #0x4

结合ARM官方手册,我们很容易推测出3行代码的大概作用:

  1. D0的值取出来,放入X8/W8
  2. X8/W8的值存起来;
  3. X0指向这个存起来的值;X0localtime的唯一参数,即const time_t *time;因此,X8/W8就是time_t类型的UNIX时间戳。

这么看来,两者的主要区别,就在于一个用了X8,一个用了W8。这又有什么影响呢?

D0是64位寄存器;X8是64位寄存器,而W8是32位寄存器。大家还记得吗,QQ爱执行localtime后,得到了一个空指针;且errno的值是2:

Process 1815 stopped
* thread #1: tid = 0x11ecb, 0x00000001001f6a50 QQ`___lldb_unnamed_function5198$$QQ + 28, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x18)
    frame #0: 0x00000001001f6a50 QQ`___lldb_unnamed_function5198$$QQ + 28
QQ`___lldb_unnamed_function5198$$QQ:
->  0x1001f6a50 <+28>: ldr    w0, [x0, #24]
    0x1001f6a54 <+32>: mov    sp, x29
    0x1001f6a58 <+36>: ldp    x29, x30, [sp], #16
    0x1001f6a5c <+40>: ret    
(lldb) p errno
(void *) $1 = 0x0000000000000002

2代表着ENOENT,即

No such file or directory

说实话,我并不知道这个错误在这种场合下是什么意思。但我猜测,导致出错的原因在于——

对于arm64来说,time_t的长度是64位的(可用sizeof验证);而QQ爱把这个64位长度的time_t对象从D0中取出,放入了32位的W8,再存到[x29, #-0x4]处。此时,[x29, #-0x4]的低32位来自D0,是正确的,但高32位的值,却并非来自D0,是错误的。

然后,当X0的值变成x29 - 0x4,作为参数传递给localtime时,指针指向的值就不再是一个合法的time_tlocaltime接收了一个非法参数,自然也得不出正确的结果,于是返回了空指针。QQ爱在这里没有做容错处理,而是坚定地认为返回值一定是一个正确的tm结构体指针,然后草率地读取其成员;结果碰上了一个空指针,出现了EXC_BAD_ACCESS,酿成大错:cold_sweat:

我想,这就是问题的来龙去脉了。

修复bug

厘清了bug的来龙去脉,剩下的代码工作就相对轻松了,我们只需要模仿QQ HD的实现,把[NSDate getDayOfWeekOfTime:]重写一遍就可以了。核心代码如下:

#include <ctime>

%hook NSDate

+ (int)getDayOfWeekOfTime:(double)arg1
{
	time_t currentTime = (time_t)arg1;
	struct tm *localTime;
	localTime = localtime(&currentTime);
	return localTime->tm_wday;
}

%end

编译打包,安装测试,看看效果:

完美:sunglasses:

搞定bug后,进入问题对话,才发现罪魁祸首是——

如果你最近更新了QQ签名,这条签名就会在聊天时,推送到对方的手机上;导致程序崩溃的,就是这条签名的日期。正因为不是每个人最近都更新过QQ签名,所以出现问题的对话只是少数;为什么有的对话崩有的不崩,就解释得通了。

顺便一提,在我看来,这个签名推送功能面向的群体,主要是深沉忧郁、故作高冷的花季少男少女。他们一般都是被动地等待别人叩开心门,不太会主动表露心扉,所以非常需要代码来“推”他们一把。

我接触过的少男少女中,大多数人的审美和品味还停留在花里胡哨的大杂烩阶段,追求的是红黄绿钻,玩的是QQ空间和聊天主题,应该不是QQ爱的主要目标群体。

基于这个认知,我建议主打简洁、大方的QQ爱去掉这个功能,因为它跟产品的定调是不一致的。早期产品追求的,应该是 让少数用户爱上自己,而不是 让多数用户喜欢自己;产品是腾讯的强项,我想这个道理他们应该是明了的。

此外,我还很好奇QQ爱的源代码里,这个函数是怎么实现的,那个32位的W8是怎么来的?如果当事团队看到了这篇文章,麻烦贴一下代码哈:grin:

后记

我想,看到这里的朋友中,有些人可能会想:“这么低级的错误QQ都会犯,看来腾讯的工程师也不比我强嘛!”

曾几何时,我也抱持着这样的心态,觉得天大地大,老子最大。现在想想,这是错误的,更是危险的。

上个月,我去参加了华东师范大学举办的ROS Summer School,最后一天的课程结束后,张新宇老师带我们去参观了他们的机器人实验室。

张老师在国外生活了近10年时间,回国前,在北卡罗来纳大学教堂山分校从事机器人的研究工作,眼界十分开阔,完爆我这种没有出过国的弱旅:cry:

当时有一个外地过来上课的硕士生,可能是自觉机器人搞得还不错,就跟张老师说:“ROS在国内刚刚起步,我感觉清北学生的研究水平是不是跟我差不多。”

张老师的回答,让我印象十分深刻。他说(大意):“你错了,差很多。你要知道,我们国家的筛选机制是很严格的,清北的学生,一定比绝大多数学生强。我们必须先承认、接受这个事实,才有追赶的机会;就像中国近年不再喊‘超英赶美’的口号一样——我们已经认识到,中国跟美国比还存在鸿沟般的差距,需要多少辈人的前赴后继才可能弥补。自己不再阿Q,反而才有不需要阿Q的可能。”

没有在腾讯/BAT干过,或者压根进不了腾讯/BAT的朋友,千万不要自我安慰;我们就是不如人家。

QQ的朋友,也千万不要小看微信,觉得微信的功能QQ都有,微信有什么可牛比的;事实胜于雄辩,微信就是最牛比的。

记住,只有承认差距,才能正视问题;但是,不要妄自菲薄,方可奋起直追。

送给大家,也送给自己。谢谢阅读!


我还在找工作;我的期望是 结识优秀人才、加入出色团队、从事核心业务。如果您有合适的机会,请不吝赐教。谢谢!

11 个赞

怎么说呢,看完终于知道和大神的差距在哪里了:要是俺遇到这问题,大概删App就完事了,最多去给它评论下存在闪退的问题。

狗神居然花(几天?)时间去定位问题,然后还给找到并修复了……啧啧啧啧,难怪你没女朋友啊:pensive:

1 个赞

:joy:

1 个赞

###时间表

  • 找到70%的bug线索:周日23:00 ~ 周一03:00
  • 找到剩下30%的线索:周一 08:00 ~ 10:00
  • 梳理过程,查缺补漏:周一10:30 ~ 12:00
  • 帖子初稿:周一13:00 ~ 23:00
  • 帖子润色:周二12:00 ~ 14:00
  • 升级论坛:周二14:00 ~ 17:00

然后发帖:wink:

1 个赞

找ViewController这样也许更快哈

po [[[UIWindow keyWindow] rootViewController] _printHierarchy]

2 个赞

这个函数我知道,有篇帖子有;主要是用这个函数,我对不上打印出来的controller和界面

可以可以,发现我还要学好多啊…

恩,输出太多。
例如下面:

<NavigationController 0x14c899000>, state: appeared, view: <UILayoutContainerView 0x14c72b4e0>
   | <LogViewController 0x14c7270a0>, state: disappeared, view:  (view not loaded)
   | <TabViewController 0x14c89e400>, state: disappeared, view: <UILayoutContainerView 0x14c73f820> not in the window
   |    | <HomeViewController 0x14c746e30>, state: disappeared, view: <UIView 0x14d823bf0> not in the window
   |    | <WoViewViewController 0x14c747560>, state: disappeared, view:  (view not loaded)
   | <DisplayViewController 0x14d8a7f10>, state: appeared, view: <UIView 0x14c56d820>

主要是看 state( appearing , disappearing , disappeared, appeared)
叶子节点的 appeared 就是当前显示的。

1 个赞

狗剩大神就是屌到没女朋友啊

1 个赞

学习了,下次试试看

看了下代码,QQI 应该是没适配 arm64,使用了 uint32 类型。据说 QQI 已经不维护了。分析的很详细,狗神犀利

1 个赞

QQI 听之前的leader说是北京的团队在维护,一年多没维护了。听说一个月前才启动更新计划。

应该是这次维护更新没有定位到这个问题吧。

手q这边的对应的代码已经fix了这个问题了

2 个赞

最近一次是上个月更新的,估计是像@xminug 说的那样,一个月前启动了更新计划

太厉害了… 膜拜下啊 …盖一楼…

勾起我对逆向的兴趣,感觉好好玩
而且楼主的求知精神真是赞,大部分人遇到这个问题多半吐吐槽也就忘了。

可以更简单的方式找vc,就是利用facebook开源的FLEX,在cydia中搜索FLEX安装一个FLEX Loader,然后,就可以将FLEX导入到任何的一个app中,直接就可以查看VC等,还有界面,超级便捷

2 个赞

这个很有实用价值,你能写个FLEX的使用帖分享一下心得吗?

可以啊,我整理整理啊

2 个赞

关注flex

是Flipboard 开源的FLEX (https://github.com/Flipboard/FLEX) 吧?

1 个赞