前言
QQ在互联网发展史中的重要地位毋庸置疑。尽管近些年由于自身的膨胀和微信的崛起,QQ的人气有些下跌,但多数网虫在上网时仍会挂着QQ —— 它早已融入了我们的生活。
上周在网上闲逛时,无意中看到了QQ国际版的iOS客户端 —— QQ International,它的应用名叫QQ爱(QQi),跟我上中学时听过的一首网络歌曲同名。看了一下App Store里的App截图,简单利落,是我喜欢的风格;更重要的是,包体不到65M,比原版QQ小了近一半,摒弃了很多乱七八糟的东西。我马上就删掉了原版QQ,换成了QQ爱
昨日在外玩耍时,一位好友给我发来一条消息。我打开QQ爱,刚想点进对话列表给他回复,没想到QQ爱竟然闪退了——
我的第一反应当然是插件冲突。因为对于最新的盘古越狱,重启可以禁用所有的插件,所以我重启iPhone,又测试了几遍,发现问题仍然存在。
因为闪退发生在QQ爱 主界面 的 最常见场景,所以一开始我是不相信QQ爱有这样明显bug的。但是看了App Store上的网友评价,10条里竟然有2条反馈类似的问题——
我开始怀疑,网友给QQ爱的1颗半星并非空穴来风,这款App确实存在严重bug。网友的评论都是上个月发布的,距今已过去近20天,bug都没有得到修复,难道是QQ爱团队太忙了吗?
写完《我的失败与伟大》系列创业心得后,我的空闲时间稍微多了些;那我就帮人帮到底,把这个bug给修复了吧
以下操作的环境是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:]
没毛病。这就尴尬了
跟我一起思考:
点击问题对话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
结构体的指针,所以X0
和tm
中每个成员的关系是这样的:
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:]
的返回值;其中,W0
是X0
的低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行代码的大概作用:
- 把
D0
的值取出来,放入X8/W8
; - 把
X8/W8
的值存起来; - 把
X0
指向这个存起来的值;X0
是localtime
的唯一参数,即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_t
。localtime
接收了一个非法参数,自然也得不出正确的结果,于是返回了空指针。QQ爱在这里没有做容错处理,而是坚定地认为返回值一定是一个正确的tm
结构体指针,然后草率地读取其成员;结果碰上了一个空指针,出现了EXC_BAD_ACCESS
,酿成大错
我想,这就是问题的来龙去脉了。
修复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(¤tTime);
return localTime->tm_wday;
}
%end
完美
搞定bug后,进入问题对话,才发现罪魁祸首是——
如果你最近更新了QQ签名,这条签名就会在聊天时,推送到对方的手机上;导致程序崩溃的,就是这条签名的日期。正因为不是每个人最近都更新过QQ签名,所以出现问题的对话只是少数;为什么有的对话崩有的不崩,就解释得通了。
顺便一提,在我看来,这个签名推送功能面向的群体,主要是深沉忧郁、故作高冷的花季少男少女。他们一般都是被动地等待别人叩开心门,不太会主动表露心扉,所以非常需要代码来“推”他们一把。
我接触过的少男少女中,大多数人的审美和品味还停留在花里胡哨的大杂烩阶段,追求的是红黄绿钻,玩的是QQ空间和聊天主题,应该不是QQ爱的主要目标群体。
基于这个认知,我建议主打简洁、大方的QQ爱去掉这个功能,因为它跟产品的定调是不一致的。早期产品追求的,应该是 让少数用户爱上自己,而不是 让多数用户喜欢自己;产品是腾讯的强项,我想这个道理他们应该是明了的。
此外,我还很好奇QQ爱的源代码里,这个函数是怎么实现的,那个32位的W8
是怎么来的?如果当事团队看到了这篇文章,麻烦贴一下代码哈
后记
我想,看到这里的朋友中,有些人可能会想:“这么低级的错误QQ都会犯,看来腾讯的工程师也不比我强嘛!”
曾几何时,我也抱持着这样的心态,觉得天大地大,老子最大。现在想想,这是错误的,更是危险的。
上个月,我去参加了华东师范大学举办的ROS Summer School,最后一天的课程结束后,张新宇老师带我们去参观了他们的机器人实验室。
张老师在国外生活了近10年时间,回国前,在北卡罗来纳大学教堂山分校从事机器人的研究工作,眼界十分开阔,完爆我这种没有出过国的弱旅
当时有一个外地过来上课的硕士生,可能是自觉机器人搞得还不错,就跟张老师说:“ROS在国内刚刚起步,我感觉清北学生的研究水平是不是跟我差不多。”
张老师的回答,让我印象十分深刻。他说(大意):“你错了,差很多。你要知道,我们国家的筛选机制是很严格的,清北的学生,一定比绝大多数学生强。我们必须先承认、接受这个事实,才有追赶的机会;就像中国近年不再喊‘超英赶美’的口号一样——我们已经认识到,中国跟美国比还存在鸿沟般的差距,需要多少辈人的前赴后继才可能弥补。自己不再阿Q,反而才有不需要阿Q的可能。”
没有在腾讯/BAT干过,或者压根进不了腾讯/BAT的朋友,千万不要自我安慰;我们就是不如人家。
QQ的朋友,也千万不要小看微信,觉得微信的功能QQ都有,微信有什么可牛比的;事实胜于雄辩,微信就是最牛比的。
记住,只有承认差距,才能正视问题;但是,不要妄自菲薄,方可奋起直追。
送给大家,也送给自己。谢谢阅读!
我还在找工作;我的期望是 结识优秀人才、加入出色团队、从事核心业务。如果您有合适的机会,请不吝赐教。谢谢!