天天看點

微信“ 15。。。。。。。。。”來龍去脈

作者:changelcai

大家好,給大家介紹一下,這是Bug

微信“ 15。。。。。。。。。”來龍去脈

應該有很多Android的使用者熟悉上面這圖。。。

國慶前幾天,微信Android大量使用者回報接收或發送類似“15。。。。。。。。。。。。。。。”資訊會導緻微信聊天界面卡死,整個垮掉,竟然垮掉。這對微信來說是很嚴重的事情啊,一時半會回報也鋪天蓋地的過來,我們得知這個問題後,第一時間對這個問題進行了緊急修複并在兩天内覆寫了全網大部分使用者,最終這個問題得到了解決。追根溯源,毫無疑問這鍋開發小弟來背,這次不能冤枉了産品MM哈哈。

與此同時,很多熱心的網友也開始分析原因,25号當日就有行内大神通過ANR日志和反編譯debug,一步步推敲出此次ANR的根源,給出了卡死的原因。請受小弟一拜,實在佩服佩服!原文連結

下圖是網友分析結果圖:

微信“ 15。。。。。。。。。”來龍去脈

根據該網友的推敲,此次卡死的真正原因在于:“這個wwk是始終等于0的,也就是不滿足while内部的dVar2的置空條件,也就造成了while死循環”。這裡具體怎麼做到動态反編譯的?這個知乎的回答很詳細,學習可參見:連結

真正的原因确實如網友分析的,主要是卡在了這個while循環裡面,這個循環的主要作用是将目前文字内容按具體的規則進行斷句排版。

微信“ 15。。。。。。。。。”來龍去脈

因為dVar2且dVar2.getText一直不為空,一直滿足這個條件,是以造成死循環。而dVar2這個值為null的條件取決于下面這個函數

微信“ 15。。。。。。。。。”來龍去脈

“i4”變量實際是斷句算法傳回截斷的實際位置,dvar2.getLength()實際是目前行的文字長度,這裡因為斷句算法的bug,造成了”i4”這個變量一直傳回0,而目前行文字長度dvar2.getLength()是>0的,是以這個dVar2永遠不會被指派為空。繼續追根問底:是什麼原因造成斷句算法一直傳回0呢,實際上斷句算法是調用了以下這個函數:

微信“ 15。。。。。。。。。”來龍去脈

該函數傳回了一個對象a其包含兩個參數,一個是斷句的位置(a.wwk),及斷句後的文字長度(a.width),主要是因為在判斷換行的時候,因為考慮到标點符号不應該位于行首這條規則,需要将目前行最後一個非标點符号截斷到下一行,而截斷受另外一條規則限制,截斷不可以為英文或者數字,這導緻15。。。。。。。。。。。最後傳回截斷的位置為0,并将結果傳回,是以才産生了死循環,造成這個bug。

那麼問題來了

很多網友也開始讨論,為什麼要自己排版,放着好端端的系統TextView不用?到底好在哪裡?效果是怎麼樣的?

不着急,諸多問題的來龍去脈得容小弟一一道來。

實際上,世界上大部分需求都源于使用者。這需求還得得益于之前有幾個使用者會回報說“微信Android的聊天氣泡好像沒有iOS的美觀,比較死闆”。這個問題也引起了我們的關注。

那事實是否如此呢?我們對iOS和Android進行了對比,如下圖:

微信“ 15。。。。。。。。。”來龍去脈

從效果圖看,iOS确實比Android好看了些,至少最右邊并不會有多餘的padding這麼明顯,簡單來說多餘的padding産生的原因是氣泡寬度受螢幕大小的限制,是以這裡TextView即是氣泡有了最大的寬度限制,當剩下的空間不足以容下一個字元時,系統排版會選擇自動換行,導緻了這個問題的産生。

又一個問題

那麼,iOS的排版是否就是完美的呢,其實仔細觀察并非這樣,從上圖可以看出,除了Android,iOS也會有這種問題,那就是氣泡中的文字左右參差不齊。

一開始我們懷疑,會不會是微信應用本身使用該元件不當的原因造成,而非系統元件的問題。于是乎,在手機上,我們随便找了一些熱門app,仔細對比,同樣的問題依然存在。

知乎:

微信“ 15。。。。。。。。。”來龍去脈

掘金:

微信“ 15。。。。。。。。。”來龍去脈

支付寶:

微信“ 15。。。。。。。。。”來龍去脈

等等。。。

而且除了移動端,pc端同樣也有諸類問題。結合上面這些對比,确實市面上大部分應用都存在這個問題。通過這次回報,我們也開始在思考能不能在移動用戶端的文字排版上做得更人性化一些,體驗上更好?。就這個問題,我們找了設計的同學一起探讨,認為确實有這個必要。于是就開始有了下一步。

對于文字排版,這容易讓人想起,“我的(word)哥”,微軟對于這款應用,有沒有一些文字左右對齊的手段或者方案可以參考呢?

下圖為word的左對齊效果,也就是Android的TextView預設對其方式。

微信“ 15。。。。。。。。。”來龍去脈

下圖為word的居中‘硬’對齊效果:

微信“ 15。。。。。。。。。”來龍去脈

下圖為word的居中‘軟’對齊效果:

微信“ 15。。。。。。。。。”來龍去脈

從這種效果上看,“軟對齊方式”更美觀,體驗最好。

于是我們能想到的就是動态調整字間距的方式來實作這種效果(word也是這麼實作的)。

那既然要動态調整字型間距,是不是可以一味的這麼做就可以?

答案當然不是,如果這麼做就像‘硬對齊方式’一樣,顯得過于生硬了。

我們就這個問題跟設計組的同僚進行讨論,通過他們的調研及嘗試,得出了一個合理的方案,那就是最多允許有一個英文字元寬度的調整範圍,将調整的寬度平均配置設定到目前行每個字元中去,對使用者來說影響是最小的,同時也保持了一定的美觀。

對于Android來說,實作這條規則并不難,要麼是改造系統TextView,要麼自己寫個自定義view實作文字排版及渲染,最後我們采用了後者這個方案。

原因在于:

系統TextView真正排版及繪制的邏輯不在其本身,而是交給三個繼承了Layout的子類負責,分别為StaticLayout、DynamicLayout、BoringLayout,我們更常用的是StaticLayout,它隻負責靜态的文字處理,關于各自Layout的差別,這裡了就不展開講了。系統TextView并沒有暴露接口去代理它們。當然沒有接口不意味着做不到,我們完全可以通過反射等手段代理它,但其實這麼做的話,代價是比較大的。

原因有三:

其一,從Android 2.3到Android 8.0,TextView的代碼雖說變化不會很大,但從Layout來看,實作的邏輯或者接口也好都有所變更,如果通過這個方式,代理的相容性會是一個問題。

其二,TextView堪稱Android最複雜的一個元件之一,幾個Layout邏輯代碼的複雜程度很高,自己實作所有的Layout接口,本身就是一件複雜且工作量很大的工作。

其三,實際上自己實作一個Layout,基本上就實作了一個顯示元件,排版和渲染都是要處理的,是以這樣實作的意義不大,甚至反而不靈活。

回歸正題,我們對系統TextView的規則進行對比,最後我們确定了以下幾條規則:

1、最多允許有一個字母字元寬度的來調整字間距

2、對于标點符号盡量規避不出現在行首

3、對于英文單詞或數字不截斷排版

于是我們開始進行簡單的demo實作。效果如下圖:

微信“ 15。。。。。。。。。”來龍去脈

對比優化前的效果,确實這麼做效果是明顯的。但仔細觀察,還是會發現,對于一些特殊的中文全角符号(如,《》()【】等)因為有多餘的padding存在,放在行首和行末也會導緻參差不齊的效果。于是我們多增加了一條規則

4、對一些常見的有多餘padding的全角符号位于行首或行末時,預設減去多餘的padding來達到更好的對齊效果。

最後的優化效果,如圖:

微信“ 15。。。。。。。。。”來龍去脈

最後一張是應用了4條規則的效果圖,整體文字的對齊效果比系統預設的排版改善了不少。

問題又來了

那既然效果是不錯的,是否存在其他問題?确實如此。

一、小語種處理問題

因為微信對小語種是支援的,對于一些特殊的小語種,如泰語,阿拉伯語等,泰語的排版方式并非簡單的橫排,字元與字元之間是有上下關系的,而對于阿拉伯語,是從右往左排列的。如果隻是按上面所講的幾個規則,那麼排版後的效果肯定是不合理的。

考慮到小語種存在多樣性,排版規則不統一,而且使用小語種使用者比例小,但也不能讓其排版錯誤不管,是以對于這種情況,我們通過一個簡單的正規表達式去比對是否屬于能處理的字元串範圍内,這就是為什麼有網友分析”15。。。。。。。。”這個事件時,一開始會懷疑是正則比對耗時造成的。下圖為該網友的分析:

微信“ 15。。。。。。。。。”來龍去脈

而實際上,這個簡單的正規表達式,如該網友測試的一樣,處理起來很快,基本都在1ms内,對性能的影響可忽略。

通過正則去判斷後,如果是可處理的字元串則應用上面的規則進行排版,如果是特殊的字元串,則用系統的TextView代理顯示。

二、适配率問題

既然小語種的問題可以解決,但這裡又産生一個問題,現網上的使用者, 使用特殊字元的頻率多高?這問題直接關系到我們這個排版元件的适配率,也就是對使用者體驗改善多少?在我們看來,一般人并不會發些奇奇怪怪的符号在微信裡面,是以能應用上這個排版規則的應該占大多數。當然這裡隻是猜想,如果這樣确定可行性也太草率了。

于是我們針對這個問題,進行了一輪灰階,灰階的結果如下:

灰階

結果

目标灰階人數

40W

setText總次數

400w+

平均命中率

96%+

通過這次灰階,現網使用者能應用上該元件适配的情況達到了預期的結果。

三、性能問題

如果該元件的性能跟系統相差太多,甚至嚴重影響幀率,造成使用者卡頓,這當然也是不可取的。我們針對這個問題,進行了本地的自動化幀率測試及與系統TextView進行函數間的對比:

實驗資料:

Attributes

CellTextView

TextView

Remarks

FPS(good)

53.93

54.11

聊天界面,各類長短文本,跑同一個case,在好機器上的幀率

FPS(bad)

41.91

41.41

差機器上的幀率

setText(ns)

1345208

8839618

相同1000char的文字,連續每隔100ms,setText一次,統計30次平均耗時

onMeasure(ns)

2152881

6111

setText觸發onMeasure,30次的平均耗時

onDraw(ns)

16516024

2459097

setText觸發onDraw,30次的平均耗時

sum(ns)

20014113

11304826

setText整個過程,30次的平均耗時

結論:

從微觀上,通過函數進行對比,CellTextView對比系統TextView性能稍差2倍,主要差距在于繪制文字時需要單字調整間距。

從宏觀上,CellTextView對實際幀率的影響較小,使用者無明顯感覺性能變差。

通過以上的嘗試及灰階結果來看,做這個事情其實是很有意義的,那麼最後也敲定下了這個優化方案。

整個需求的來龍去脈就是這樣子的,其實梳理這個過程的來龍去脈來,一來可以讓自己不斷反思該過程存在的一些問題,二來呢,因為本次bug确實對大家造成了不好的影響(真的是深感歉意啊!),可以讓大家清楚這個事情是怎麼發生的,至少大家不會卡得不明不白的。

寫代碼萬萬要小心謹慎,考慮周全啊。這次痛定思痛,吃一塹,長一智吧。願天下的程式統統沒有bug。對,統統沒有!!!

最後貼上一張優化後的效果圖:

微信“ 15。。。。。。。。。”來龍去脈

文章寫得不好的地方,望見諒,大神莫噴莫噴。小弟我要背鍋去面壁了。