iOS版微信是如何防止特殊字符导致的炸群、APP崩溃的?

1、引言

相信大家都遇到过一段特殊文本可以让iOS设备所有app闪退的经历。前段时间大年初一,又出现某个印度语字符引起iOS11系统奔溃,所幸iOS版微信客户端做了保护并没有引起太大问题(字符处理这类技术问题,其实曾在Android版微信上导致过严重的用户体验危机。

一般来说,特殊字符闪退是系统漏洞引起,只要更新系统就行。但大部分用户不愿意更新系统,而苹果也不一定第一时间解决问题。另外后台可以拦截恶意文本传递,但对于本地已下发的消息,后台没有办法让它删除。所以客户端还是要做些保护预防特殊字符闪退。

2、微信的思路

由于无法事先知道字符串里包含特殊字符,所以只能先让它排版/绘制,看看是否出现问题。做法是,在排版/绘制字符串前,先设置标记位,排版/绘制结束后,移除标记位。

一旦发现标记位存在,就意味着这字符串可能有问题,下次就不显示这个字符串:

iOS版微信是如何防止特殊字符导致的炸群、APP崩溃的?_第1张图片

这里有几个问题:

有可能在排版/绘制过程中,其它线程crash,导致标记位不能正常移除。所以crash时要判断crash线程是否为排版/绘制线程。

究竟crash多少次才能判断这字符串是有问题的:最早做法是crash一次就直接屏蔽,但很多用户反馈,说某些好友昵称无法显示。其实iOS绘制字符串时也会极少概率出现闪退,从而误判。但crash两次才屏蔽的话,如果用户连续收到N条恶意消息,那么至少crash 2N次才彻底把所有有问题消息屏蔽。

因此,第一次字符串crash先不屏蔽,后续连续字符串crash的话,直接屏蔽。这样crash N+1次就能处理完了。

3、具体的iOS代码实现

正如第2节的思路那样。整个逻辑代码大致如下。

MessageItemView.mm:

1
2
3
4
5
6
7
8
9
10
11
12
13
//CP是CrashProtected的简称
@implementationMessageItemView
- ( void )initContentLabel {
     m_label = [[MMCPLabel alloc] init];
     m_label.cpKey = [MMCPUtil generateKeyWithObject:self.messageModel];
     if ([MMCPUtil isUnsafeWithKey:m_label.cpKey]) {
         // 检测出messageModel消息内容有问题,屏蔽显示
         m_label.text = @ "该内容无法显示" ;
     else {
         m_label.text = self.messageModel.content;
     }
}
@end

MMCPLabel.mm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@implementationMMCPLabel
@synthesizecpKey = m_cpKey;
// 对常用的排版/绘制接口做检查
- ( void )layoutSublayersOfLayer:(CALayer *)layer {
     CScopedCrashCounter crashCounter(m_cpKey);
     [superlayoutSublayersOfLayer:layer];
}
- ( void )drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
     CScopedCrashCounter crashCounter(m_cpKey);
     [superdrawLayer:layer inContext:ctx];
}
- (CGSize)sizeThatFits:(CGSize)size {
     CScopedCrashCounter crashCounter(m_cpKey);
     return [supersizeThatFits:size];
}
@end

MMCPUtil.mm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 利用C++特性,在声明C++类临时变量时,会自动执行构造函数,离开作用域会执行析构函数
// 因此构造函数做crashCount+1,析构函数做crashCount-1
classCScopedCrashCounter {
public :
     CScopedCrashCounter(NSString*cpKey) {
         m_cpKey = cpKey;
         [MMCPUtil increaseCrashCountWithKey:m_cpKey];
     }
     ~CScopedCrashCounter() {
         [MMCPUtil decreaseCrashCountWithKey:m_cpKey];
     }
private :
     NSString*m_cpKey;
};
 
@implementationMMCPUtil
@synthesizecrashKeyMemoryMappedKV = m_crashKeyMemoryMappedKV;  // 被判定为恶意信息对应的key
@synthesizecrashCountMemoryMappedKV = m_crashCountMemoryMappedKV;  // 每个key crash次数
- (BOOL)isUnsafeWithKey:(NSString*)key {
     return [m_crashKeyMemoryMappedKV getBoolForKey:key] == YES;
}
- ( void )increaseCrashCountWithKey:(NSString*)key {
     // 这里记录key所在线程
     ...
     int32_t count = [m_crashCountMemoryMappedKV getInt32ForKey:key];
     [m_crashCountMemoryMappedKV setInt32:count +  1  forKey:key]
}
- ( void )decreaseCrashCountWithKey:(NSString*)key {
     int32_t count = [m_crashCountMemoryMappedKV getInt32ForKey:key];
     [m_crashCountMemoryMappedKV setInt32:MAX( 0 , count -  1 ) forKey:key];
}
// crash回调函数
- ( void )onSignalCrash:(siginfo_t *)info {
     // 先找到跟crash线程相同的key
     NSString*key = [selflastCPKey:info->si_pid];
     if (key == nil)  return ;
     if (m_isLastTimeCrashedBySpecialCharacter == NO) {
         // 设置当前是特殊字符引起的闪退,如果crash次数大于1,则屏蔽这字符串显示
         [selfsetLastTimeCrashedBySpecialCharacter:YES];
         if ([m_crashCountMemoryMappedKV getInt32ForKey:key] >  1 ) {
             [m_crashKeyMemoryMappedKV setBool:YESforKey:key];
         }
     else {
         // 连续特殊字符闪退,直接屏蔽
         [m_crashKeyMemoryMappedKV setBool:YESforKey:key];
     }
}
@end

4、还需要从用户体验上做更进一步改进

即使有了上面的N+1优化,当N很大时,客户端还是要crash很多次才能正常使用。之前有用户乱扫二维码被拉进炸群,如果不发红包,群主不停炸群;用户频繁crash,也无法退群。不少用户会选择卸载重装客户端。因此客户端要加上安全模式的机制。

当客户端检测出连续三次crash,下次启动会出现安全模式的界面,提示用户如何处理:

iOS版微信是如何防止特殊字符导致的炸群、APP崩溃的?_第2张图片

对于频繁闪退的群聊,主界面提供快捷入口方便用户退群。另外对于可能误判的字符串,界面也提供入口方便用户恢复字符串显示:

iOS版微信是如何防止特殊字符导致的炸群、APP崩溃的?_第3张图片

为了让后台第一时间发现新的特殊字符变种,客户端检测出特殊字符crash后,会把相关信息上报到后台。通过客户端上报、后台拦截的闭环,能大大降低特殊字符传播范围。这方案不仅用于特殊字符,还能用于其他恶意信息,如炸群消息、GIF、小视频、链接等。

你可能感兴趣的:(软件开发,iOS)