1、微信超級Bug
大家好,給大家介紹一下,這是Bug:
應該有很多Android的用戶熟悉上面這圖。
(本文同步發佈於:http://www.52im.net/thread-1099-1-1.html)
2、事件背景
國慶前幾天,微信Android大量用戶反饋接收或發送類似“15。。。。。。。。。。。。。。。”信息會導致微信聊天界面卡死,程序崩潰。這對微信來說是很嚴重的事情啊,一時半會反饋也鋪天蓋地的過來,我們得知這個問題後,第一時間對這個問題進行了緊急修復並在兩天內覆蓋了全網大部分用戶,最終這個問題得到了解決。追根溯源,毫無疑問這鍋開發小弟來背,這次不能冤枉了產品MM哈哈。
與此同時,很多熱心的網友也開始分析原因,25號當日就有行內大神通過ANR日誌和反編譯debug,一步步推敲出此次ANR的根源,給出了卡死的原因。請受小弟一拜,實在佩服佩服!
詳情可參考鏈接:http://androidwing.net/index.php/243
下圖是網友分析結果圖:
根據該網友的推敲,此次卡死的真正原因在於:“這個wwk是始終等於0的,也就是不滿足while內部的dVar2的置空條件,也就造成了while死循環”。這裏具體怎麼做到動態反編譯的?
這個知乎的回答很詳細:https://www.zhihu.com/question/65828771
3、真正原因揭曉
真正的原因確實如網友分析的,主要是卡在了這個while循環裏面,這個循環的主要作用是將當前文字內容按具體的規則進行斷句排版,見下圖。
因爲dVar2且dVar2.getText一直不爲空,一直滿足這個條件,所以造成死循環。
而dVar2這個值爲null的條件取決於下面這個函數:
“i4”變量實際是斷句算法返回截斷的實際位置,dvar2.getLength()實際是當前行的文字長度,這裏因爲斷句算法的bug,造成了”i4”這個變量一直返回0,而當前行文字長度dvar2.getLength()是>0的,所以這個dVar2永遠不會被賦值爲空。
繼續追根問底:是什麼原因造成斷句算法一直返回0呢,實際上斷句算法是調用了以下這個函數:
該函數返回了一個對象a其包含兩個參數,一個是斷句的位置(a.wwk),及斷句後的文字長度(a.width),主要是因爲在判斷換行的時候,因爲考慮到標點符號不應該位於行首這條規則,需要將當前行最後一個非標點符號截斷到下一行,而截斷受另外一條規則限制,截斷不可以爲英文或者數字,這導致“15。。。。。。。。。。。。。。。”最後返回截斷的位置爲0,並將結果返回,所以才產生了死循環,造成這個bug。
4、那麼問題來了
很多網友也開始討論,爲什麼要自己排版,放着好端端的系統TextView不用?到底好在哪裏?效果是怎麼樣的?
不着急,諸多問題的來龍去脈得容小弟一一道來。
5、爲什麼有這個需求?
實際上,世界上大部分需求都源於用戶。這需求還得得益於之前有幾個用戶會反饋說“微信Android的聊天氣泡好像沒有iOS的美觀,比較死板”。這個問題也引起了我們的關注。
那事實是否如此呢?我們對iOS和Android進行了對比,如下圖:
從效果圖看,iOS確實比Android好看了些,至少最右邊並不會有多餘的padding這麼明顯,簡單來說多餘的padding產生的原因是氣泡寬度受屏幕大小的限制,所以這裏TextView即是氣泡有了最大的寬度限制,當剩下的空間不足以容下一個字符時,系統排版會選擇自動換行,導致了這個問題的產生。
6、又一個問題
那麼,iOS的排版是否就是完美的呢,其實仔細觀察並非這樣,從上圖可以看出,除了Android,iOS也會有這種問題,那就是氣泡中的文字左右參差不齊。
一開始我們懷疑,會不會是微信應用本身使用該組件不當的原因造成,而非系統組件的問題。於是乎,在手機上,我們隨便找了一些熱門app,仔細對比,同樣的問題依然存在。
知乎:
掘金:
支付寶:
等等。。。
而且除了移動端,pc端同樣也有諸類問題。結合上面這些對比,確實市面上大部分應用都存在這個問題。通過這次反饋,我們也開始在思考能不能在移動客戶端的文字排版上做得更人性化一些,體驗上更好?。就這個問題,我們找了設計的同學一起探討,認爲確實有這個必要。於是就開始有了下一步。
7、排版要怎麼排?
對於文字排版,這容易讓人想起,“我的(word)哥”,微軟對於這款應用,有沒有一些文字左右對齊的手段或者方案可以參考呢?
下圖爲word的左對齊效果,也就是Android的TextView默認對其方式:
下圖爲word的居中‘硬’對齊效果:
下圖爲word的居中‘軟’對齊效果:
從這種效果上看,“軟對齊方式”更美觀,體驗最好。於是我們能想到的就是動態調整字間距的方式來實現這種效果(word也是這麼實現的)。
那既然要動態調整字體間距,是不是可以一味的這麼做就可以?答案當然不是,如果這麼做就像‘硬對齊方式’一樣,顯得過於生硬了。
我們就這個問題跟設計組的同事進行討論,通過他們的調研及嘗試,得出了一個合理的方案,那就是最多允許有一個英文字符寬度的調整範圍,將調整的寬度平均分配到當前行每個字符中去,對用戶來說影響是最小的,同時也保持了一定的美觀。
8、實踐自定義排版
對於Android來說,實現這條規則並不難,要麼是改造系統TextView,要麼自己寫個自定義view實現文字排版及渲染,最後我們採用了後者這個方案。
原因在於:
系統TextView真正排版及繪製的邏輯不在其本身,而是交給三個繼承了Layout的子類負責,分別爲StaticLayout、DynamicLayout、BoringLayout,我們更常用的是StaticLayout,它只負責靜態的文字處理,關於各自Layout的區別,這裏了就不展開講了。系統TextView並沒有暴露接口去代理它們。當然沒有接口不意味着做不到,我們完全可以通過反射等手段代理它,但其實這麼做的話,代價是比較大的。
原因有三:
1)其一:從Android 2.3到Android 8.0,TextView的代碼雖說變化不會很大,但從Layout來看,實現的邏輯或者接口也好都有所變更,如果通過這個方式,代理的兼容性會是一個問題;
2)其二:TextView堪稱Android最複雜的一個組件之一,幾個Layout邏輯代碼的複雜程度很高,自己實現所有的Layout接口,本身就是一件複雜且工作量很大的工作;
3)其三:實際上自己實現一個Layout,基本上就實現了一個顯示組件,排版和渲染都是要處理的,所以這樣實現的意義不大,甚至反而不靈活。
迴歸正題,我們對系統TextView的規則進行對比,最後我們確定了以下幾條規則:
1、最多允許有一個字母字符寬度的來調整字間距;
2、對於標點符號儘量規避不出現在行首;
3、對於英文單詞或數字不截斷排版。
於是我們開始進行簡單的demo實現。效果如下圖:
對比優化前的效果,確實這麼做效果是明顯的。但仔細觀察,還是會發現,對於一些特殊的中文全角符號(如,《》()【】等)因爲有多餘的padding存在,放在行首和行末也會導致參差不齊的效果。
於是我們多增加了一條規則:
對一些常見的有多餘padding的全角符號位於行首或行末時,默認減去多餘的padding來達到更好的對齊效果。
最後的優化效果,如圖:
最後一張是應用了4條規則的效果圖,整體文字的對齊效果比系統默認的排版改善了不少。
9、問題又來了
那既然效果是不錯的,是否存在其他問題?確實如此。
9.1 小語種處理問題
因爲微信對小語種是支持的,對於一些特殊的小語種,如泰語,阿拉伯語等,泰語的排版方式並非簡單的橫排,字符與字符之間是有上下關係的,而對於阿拉伯語,是從右往左排列的。如果只是按上面所講的幾個規則,那麼排版後的效果肯定是不合理的。
考慮到小語種存在多樣性,排版規則不統一,而且使用小語種用戶比例小,但也不能讓其排版錯誤不管,所以對於這種情況,我們通過一個簡單的正則表達式去匹配是否屬於能處理的字符串範圍內,這就是爲什麼有網友分析”15。。。。。。。。”這個事件時,一開始會懷疑是正則匹配耗時造成的。
下圖爲該網友的分析:
而實際上,這個簡單的正則表達式,如該網友測試的一樣,處理起來很快,基本都在1ms內,對性能的影響可忽略。通過正則去判斷後,如果是可處理的字符串則應用上面的規則進行排版,如果是特殊的字符串,則用系統的TextView代理顯示。
9.2 適配率問題
既然小語種的問題可以解決,但這裏又產生一個問題,現網上的用戶, 使用特殊字符的頻率多高?這問題直接關係到我們這個排版組件的適配率,也就是對用戶體驗改善多少?在我們看來,一般人並不會發些奇奇怪怪的符號在微信裏面,所以能應用上這個排版規則的應該佔大多數。當然這裏只是猜想,如果這樣確定可行性也太草率了。
於是我們針對這個問題,進行了一輪灰度,灰度的結果如下:
通過這次灰度,現網用戶能應用上該組件適配的情況達到了預期的結果。
9.3 性能問題
如果該組件的性能跟系統相差太多,甚至嚴重影響幀率,造成用戶卡頓,這當然也是不可取的。我們針對這個問題,進行了本地的自動化幀率測試及與系統TextView進行函數間的對比。
下圖是實驗數據:
得出結論:
從微觀上:通過函數進行對比,CellTextView對比系統TextView性能稍差2倍,主要差距在於繪製文字時需要單字調整間距;
從宏觀上:CellTextView對實際幀率的影響較小,用戶無明顯感知性能變差。
通過以上的嘗試及灰度結果來看,做這個事情其實是很有意義的,那麼最後也敲定下了這個優化方案。
10、事件結尾
整個需求的來龍去脈就是這樣子的,其實梳理這個過程的來龍去脈來,一來可以讓自己不斷反思該過程存在的一些問題,二來呢,因爲本次bug確實對大家造成了不好的影響(真的是深感歉意啊!),可以讓大家清楚這個事情是怎麼發生的,至少大家不會卡得不明不白的。
寫代碼萬萬要小心謹慎,考慮周全啊。這次痛定思痛,吃一塹,長一智吧。願天下的程序統統沒有bug。對,統統沒有!
最後貼上一張優化後的效果圖:
文章寫得不好的地方,望見諒,大神莫噴莫噴。小弟我要背鍋去面壁了。
(原文鏈接:點此進入)
附錄:更多微信、QQ技術文章彙總
[1] 有關QQ、微信的技術文章:
《微信團隊披露:微信界面卡死超級bug“15。。。。”的來龍去脈》
《月活8.89億的超級IM微信是如何進行Android端兼容測試的》
《微信客戶端團隊負責人技術訪談:如何着手客戶端性能監控和優化》
《微信團隊原創分享:Android版微信的臃腫之困與模塊化實踐之路》
《微信團隊原創分享:微信客戶端SQLite數據庫損壞修復實踐》
《騰訊原創分享(一):如何大幅提升移動網絡下手機QQ的圖片傳輸速度和成功率》
《騰訊原創分享(二):如何大幅壓縮移動網絡下APP的流量消耗(下篇)》
《騰訊原創分享(二):如何大幅壓縮移動網絡下APP的流量消耗(上篇)》
《如約而至:微信自用的移動端IM網絡層跨平臺組件庫Mars已正式開源》
《開源libco庫:單機千萬連接、支撐微信8億用戶的後臺框架基石 [源碼下載]》
《微信新一代通信安全解決方案:基於TLS1.3的MMTLS詳解》
《微信團隊原創分享:Android版微信後臺保活實戰分享(進程保活篇)》
《微信團隊原創分享:Android版微信後臺保活實戰分享(網絡保活篇)》
《Android版微信從300KB到30MB的技術演進(PPT講稿) [附件下載]》
《微信團隊原創分享:Android版微信從300KB到30MB的技術演進》
《微信技術總監談架構:微信之道——大道至簡(PPT講稿) [附件下載]》
《微信海量用戶背後的後臺系統存儲架構(視頻+PPT) [附件下載]》
《微信異步化改造實踐:8億月活、單機千萬連接背後的後臺解決方案》
《架構之道:3個程序員成就微信朋友圈日均10億發佈量[有視頻]》
《微信團隊原創分享:Android內存泄漏監控和優化技巧總結》
《微信團隊原創Android資源混淆工具:AndResGuard [有源碼]》
《移動端IM實踐:Android版微信如何大幅提升交互性能(一)》
《移動端IM實踐:Android版微信如何大幅提升交互性能(二)》
《移動端IM實踐:WhatsApp、Line、微信的心跳策略分析》
《移動端IM實踐:谷歌消息推送服務(GCM)研究(來自微信)》
[2] 有關QQ、微信的技術故事:
《技術往事:創業初期的騰訊——16年前的冬天,誰動了馬化騰的代碼》
《技術往事:史上最全QQ圖標變遷過程,追尋IM巨人的演進歷史》
《開發往事:深度講述2010到2015,微信一路風雨的背後》
(本文同步發佈於:http://www.52im.net/thread-1099-1-1.html)