本文由 微信終端開發團隊團隊發布在騰訊雲+社區
前言
相信大家都遇到過一段特殊文本可以讓iOS設備所有app閃退的經歷。前段時間大年初一,又出現某個印度語字符引起iOS11系統奔潰。所幸微信客戶端做了保護並沒有引起太大問題。一般來說,特殊字符閃退是系統漏洞引起,只要更新系統就行。但大部分用戶不願意更新系統,而蘋果也不一定第一時間解決問題。另外后台可以攔截惡意文本傳遞,但對於本地已下發的消息,后台沒有辦法讓它刪除。所以客戶端還是要做些保護預防特殊字符閃退。
方案
由於無法事先知道字符串里包含特殊字符,所以只能先讓它排版/繪制,看看是否出現問題。做法是,在排版/繪制字符串前,先設置標記位,排版/繪制結束后,移除標記位;一旦發現標記位存在,就意味着這字符串可能有問題,下次就不顯示這個字符串:
這里有幾個問題:
- 有可能在排版/繪制過程中,其它線程crash,導致標記位不能正常移除。所以crash時要判斷crash線程是否為排版/繪制線程。
- 究竟crash多少次才能判斷這字符串是有問題的。最早做法是crash一次就直接屏蔽,但很多用戶反饋,說某些好友昵稱無法顯示。其實iOS繪制字符串時也會極少概率出現閃退,從而誤判。但crash兩次才屏蔽的話,如果用戶連續收到N條惡意消息,那么至少crash 2N次才徹底把所有有問題消息屏蔽。因此,第一次字符串crash先不屏蔽,后續連續字符串crash的話,直接屏蔽。這樣crash N+1次就能處理完了。
整個邏輯代碼大致如下:
// MessageItemView.mm, CP是CrashProtected的簡稱 @implementation MessageItemView - (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 @implementation MMCPLabel @synthesize cpKey = m_cpKey; // 對常用的排版/繪制接口做檢查 - (void)layoutSublayersOfLayer:(CALayer *)layer { CScopedCrashCounter crashCounter(m_cpKey); [super layoutSublayersOfLayer:layer]; } - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx { CScopedCrashCounter crashCounter(m_cpKey); [super drawLayer:layer inContext:ctx]; } - (CGSize)sizeThatFits:(CGSize)size { CScopedCrashCounter crashCounter(m_cpKey); return [super sizeThatFits:size]; } @end// MMCPUtil.mm // 利用C++特性,在聲明C++類臨時變量時,會自動執行構造函數,離開作用域會執行析構函數 // 因此構造函數做crashCount+1,析構函數做crashCount-1 class CScopedCrashCounter { public: CScopedCrashCounter(NSString *cpKey) { m_cpKey = cpKey; [MMCPUtil increaseCrashCountWithKey:m_cpKey]; } ~CScopedCrashCounter() { [MMCPUtil decreaseCrashCountWithKey:m_cpKey]; } private: NSString *m_cpKey; }; @implementation MMCPUtil @synthesize crashKeyMemoryMappedKV = m_crashKeyMemoryMappedKV; // 被判定為惡意信息對應的key @synthesize crashCountMemoryMappedKV = 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 = [self lastCPKey:info->si_pid]; if (key == nil) return; if (m_isLastTimeCrashedBySpecialCharacter == NO) { // 設置當前是特殊字符引起的閃退,如果crash次數大於1,則屏蔽這字符串顯示 [self setLastTimeCrashedBySpecialCharacter:YES]; if ([m_crashCountMemoryMappedKV getInt32ForKey:key] > 1) { [m_crashKeyMemoryMappedKV setBool:YES forKey:key]; } } else { // 連續特殊字符閃退,直接屏蔽 [m_crashKeyMemoryMappedKV setBool:YES forKey:key]; } } @end
即使有了上面的N+1優化,當N很大時,客戶端還是要crash很多次才能正常使用。之前有用戶亂掃二維碼被拉進炸群,如果不發紅包,群主不停炸群;用戶頻繁crash,也無法退群。不少用戶會選擇卸載重裝客戶端。因此客戶端要加上安全模式的機制。當客戶端檢測出連續三次crash,下次啟動會出現安全模式的界面,提示用戶如何處理:
對於頻繁閃退的群聊,主界面提供快捷入口方便用戶退群。另外對於可能誤判的字符串,界面也提供入口方便用戶恢復字符串顯示:
為了讓后台第一時間發現新的特殊字符變種,客戶端檢測出特殊字符crash后,會把相關信息上報到后台。通過客戶端上報、后台攔截的閉環,能大大降低特殊字符傳播范圍。這方案不僅用於特殊字符,還能用於其他惡意信息,如炸群消息、GIF、小視頻、鏈接等。
MemoryMappedKV
由於需要埋點的地方太多了,昵稱、消息內容、頭像等等,為了不影響滑動性能,guoling同學開發了一套基於mmap的高性能通用key-value存儲組件,敬請留意WeMobileDev后續文章。
問答
更多與短視頻相關的精華問答,盡在騰訊雲+社區!
相關閱讀
此文已由作者授權騰訊雲+社區發布,轉載請注明文章出處
原文鏈接:https://cloud.tencent.com/developer/article/1066209

