天猫11.11:手机淘宝 521 性能优化项目揭秘
又是一年雙十一,億萬用戶都會在這一天打開手機(jī)淘寶,高興地在會場頁面不斷瀏覽,面對琳瑯滿目的商品圖片,搶著添加購物車,下單付款。為了讓用戶 更順暢更方便地實(shí)現(xiàn)這一切,做到“如絲般順滑”,雙十一前夕手機(jī)淘寶成立了“521”(我愛你)性能優(yōu)化項(xiàng)目,在日常優(yōu)化基礎(chǔ)之上進(jìn)行三個(gè)方面的專項(xiàng)優(yōu)化 攻關(guān),分別是1)H5頁面的一秒法則;2)啟動時(shí)間和頁面幀率提升20%;3)Android內(nèi)存占用降低50%。優(yōu)化過程中遇到的困難,思考后找尋的方 案,實(shí)施后提取的經(jīng)驗(yàn)都會在下面詳細(xì)地介紹給讀者。
第一章 一秒法則的實(shí)現(xiàn)
“1S法則”是面向Web側(cè),H5鏈路上加載性能和體驗(yàn)方向上的一個(gè)指標(biāo),具體指:1)“強(qiáng)網(wǎng)”(4G/WIFI)下,1秒完全完成頁面加載,包括首屏資源,可看亦可用;2)3G下1秒完成首包的返回;3)2G下1秒完成建連。
在移動網(wǎng)絡(luò)環(huán)境下,http請求和資源加載與有線網(wǎng)絡(luò)或者PC時(shí)代相比有著本質(zhì)區(qū)別,尤其是在2G/3G網(wǎng)絡(luò)下,往往一個(gè)資源請求建連的時(shí)間都會 是整個(gè)Request-Response流程里面的大頭,一些小資源上拖累效應(yīng)尤其明顯。例如一個(gè)1k的圖片,即使在10k/s 的極慢網(wǎng)速下,理論上0.1秒可下載完畢,但由于建立連接的巨大消耗,這樣一個(gè)請求會要耗上好幾秒。
僅僅“建連”這一個(gè)點(diǎn),就能說明移動時(shí)代的Web側(cè)性能優(yōu)化和PC時(shí)代目標(biāo)和方式都相去甚遠(yuǎn),要求我們必須從更底層,更細(xì)致的去抓,才能取得看起來相對有效的結(jié)果。
15年初的性能情況
| 平均LoadTime-WIFI | 平均LoadTime - 4G | 平均LoadTime - 2G |
| 3.35s | 3.84s | 14.34s |
可以看到優(yōu)化前,平均時(shí)間很難接近1秒。為了實(shí)現(xiàn)優(yōu)化目標(biāo),在技術(shù)和實(shí)施抓手層面,由底層往上,做了四方面事情:
網(wǎng)絡(luò)節(jié)點(diǎn):HttpDNS優(yōu)化
DNS解析想必大家都知道,在傳統(tǒng)PC時(shí)代DNS Lookup基本在幾十ms內(nèi)。而我們通過大量的數(shù)據(jù)采集和真實(shí)網(wǎng)絡(luò)抓包分析(存在DNS解析的請求),DNS的消耗相當(dāng)可觀,2G網(wǎng)絡(luò)大量5-10s,3G網(wǎng)絡(luò)平均也要3-5s。
針對這種情況,手淘開發(fā)了一套HttpDNS-面向無線端的域名解析服務(wù),與傳統(tǒng)走UDP協(xié)議的DNS不同,HttpDNS基于HTTP協(xié)議。基于HTTP的域名解析,減少域名解析部分的時(shí)間并解決DNS劫持的問題。
手淘HttpDNS服務(wù)在啟動的時(shí)候就會對白名單的域名進(jìn)行域名解析,返回對應(yīng)服務(wù)的最近IP(各運(yùn)營商),端口號,協(xié)議類型,心跳等信息。
優(yōu)點(diǎn)
1)防止域名劫持
傳統(tǒng)DNS由Local DNS解析域名,不同運(yùn)營商的Local DNS有不同的策略,某些Local DNS可能會劫持特定的域名。采用HttpDNS能夠繞過Local DNS,避免被劫持;另外,HttpDNS的解析結(jié)果包含HMAC校驗(yàn),也能夠防止解析結(jié)果被中間網(wǎng)絡(luò)設(shè)備篡改。
2)更精準(zhǔn)的調(diào)度
對域名解析而言,尤其是CDN域名,解析得到的IP應(yīng)該更靠近客戶端的地區(qū)和運(yùn)營商,這樣才能有更快的網(wǎng)絡(luò)訪問速度。然而,由于運(yùn)營商策略的多樣 性,其推送的Local DNS可能和客戶端不在同一個(gè)地區(qū),這時(shí)得到的解析結(jié)果可能不是最優(yōu)的。HttpDNS能夠得到客戶端的出口網(wǎng)關(guān)IP,從而能夠更準(zhǔn)確地判斷客戶端的地區(qū) 和運(yùn)營商,得到更精準(zhǔn)的解析結(jié)果。
3)更小的解析延遲和波動
在2G/3G這種移動網(wǎng)絡(luò)下,DNS解析的延遲和波動都比較大。就單次解析請求而言,HttpDNS不會比傳統(tǒng)的DNS更快,但通過 HttpDNS客戶端SDK的配合,總體而言,能夠顯著降低解析延遲和波動。HttpDNS客戶端SDK有幾個(gè)特性:預(yù)解析、多域名解析、TTL緩存和異 步請求。
4)額外的域名相關(guān)信息
傳統(tǒng)DNS的解析結(jié)果只有ip,HttpDNS的解析結(jié)果采用JSON格式,除了ip外,還支持其它域名相關(guān)的信息,比如端口、spdy協(xié)議等。利用這些額外的信息,APP可以啟用或停止某個(gè)功能,甚至利用HttpDNS來做灰度發(fā)布,通過HttpDNS控制灰度的比例。
建連復(fù)用:SSL化,SPDY建連高復(fù)用
出于安全目的,淘寶實(shí)現(xiàn)了全站SSL化。本身和H5鏈路性能優(yōu)化沒有直接的關(guān)系,但是從數(shù)據(jù)層面看,SSL化之后的資源加載耗時(shí)都會略優(yōu)于普通的Http連接。
有讀者會有疑惑,SSL化之后每個(gè)域名首次請求會額外增加一個(gè)“SSL握手”的時(shí)間,DNS建連也會比http的狀態(tài)下要長,這是不可避免的,但是為什么一次完整的RequestRespone 流程耗時(shí)會比http狀態(tài)下短呢?
合理的解釋是:SSL化之后,SPDY可以默認(rèn)開啟,SPDY協(xié)議下的傳輸效率和建連復(fù)用效益將最大化。SPDY協(xié)議下,資源并發(fā)請求數(shù)將不再受瀏覽器webview的并發(fā)請求數(shù)量限制,并發(fā)100+都是可能的。
同時(shí),在保證了域名收斂之后,同樣域名下的資源請求將可以完全復(fù)用第一次的DNS建連和SSL握手,所以,僅在第一次消耗的時(shí)間完全可以被 SPDY后續(xù)帶來的資源傳輸效率,并發(fā)能力,以及連接復(fù)用度帶來的收益補(bǔ)回來。甚至理論上,越復(fù)雜的頁面,資源越多的情況,SSL化+SPDY之后在性能 上帶來的收益越大。
容器層面:離線化和預(yù)加載方案
收益最明顯,實(shí)現(xiàn)中遇到困難最多的就是離線化或者說資源預(yù)加載的方案。預(yù)加載方案是為了在?用戶訪問H5之前,將頁面靜態(tài)資源(HTML/JS/CSS/IMG...)打包預(yù)加載到客戶端;用戶訪問H5時(shí),將網(wǎng)絡(luò)IO攔截并替換為本地文件IO;從而實(shí)現(xiàn)H5加載性能的大幅度提升。
手淘實(shí)現(xiàn)要比上面的通用示意圖復(fù)雜:因?yàn)锳ndroid和iOS安裝包已經(jīng)很大,所以預(yù)加載Zip包(以下簡稱“包”)都是從服務(wù)器端下載到客戶端;本地 需要記錄整體包狀態(tài),并在合適的時(shí)機(jī)與服務(wù)器通信并交換狀態(tài)信息。在包發(fā)布更新的過程中要注意,本地版本和服務(wù)端最新包之間的差量同步,必要的網(wǎng)絡(luò)判 斷,WiFi下才下載等。
面對億級UV,并且在服務(wù)器資源很有限的情況下搞定這個(gè)流程,需要借助CDN來扛住壓力,實(shí)際上CDN扛住了約98%的流量。
需要注意的是預(yù)加載實(shí)際上也是一種緩存,更新比H5稍慢一些,主要受幾個(gè)因素影響:推送到達(dá)率(用戶是否在線,用戶所在網(wǎng)絡(luò)質(zhì)量),總控,服務(wù)端策略等,所以需要通過推拉結(jié)合的觸發(fā)策略并優(yōu)化下載包的體積(增量包)來提升到達(dá)率。
除了優(yōu)化到達(dá)率,手淘還做了url解CDN Combo后再映射的優(yōu)化工作,若 URL 是 Combo URL,那么會對 URL 解 Combo,解析出其中包含的資源。然后嘗試從本地讀取包含的資源,如果所有資源都在本地存在,那么將本地文件內(nèi)容拼裝為一份完整文件并返回;否則 URL 直接走線上,不做任何操作。
提升到達(dá)率和解CDN Combo再映射,這兩個(gè)容器側(cè)對于離線化方案的優(yōu)化對于本次H5鏈路上整體性能的提升有著至關(guān)重要的意義。
前端組件:請求控制,域名收斂,圖片庫,前端性能CheckList
嚴(yán)格執(zhí)行性能方面的CheckList,主要有三個(gè)點(diǎn):
1)圖片資源域名全部收斂到gw.alicdn.com;
2)前端圖片庫根據(jù)強(qiáng)弱網(wǎng)和設(shè)備分辨率做適配;
3)首屏數(shù)據(jù)合并請求為一個(gè)。
在執(zhí)行中,性能的檢查和校驗(yàn)一定要納入到發(fā)布階段,否則就不是一個(gè)合理的流程。性能的工具和校驗(yàn)一定應(yīng)該是工程化,研發(fā)流程里面的一部分,才能夠保障性能自動化,低成本,不退化。
通過以上優(yōu)化方案,H5頁面的平均Loadtime在Wifi,4G下均如期進(jìn)入1秒,3G和2G也有80%多達(dá)成1s法則的目標(biāo)。
第二章 啟動時(shí)間和頁面幀率20%的提升
很多App都會遇到以下幾個(gè)常見的性能問題:啟動速度慢;界面跳轉(zhuǎn)慢;事件響應(yīng)慢;滑動和動畫卡頓。
手機(jī)淘寶也不例外。我們分為兩部分來做,第一部分是啟動階段優(yōu)化,目的解決啟動任務(wù)繁多,缺乏管控的問題,減少啟動和首頁響應(yīng)時(shí)間。第二部分是針 對各個(gè)界面做優(yōu)化,提升界面跳轉(zhuǎn)時(shí)間和滑動幀率,解決卡頓問題。雙十一性能優(yōu)化目標(biāo)之一就是將啟動時(shí)間和頁面幀率在原有基礎(chǔ)上繼續(xù)優(yōu)化提升20%,接下來 就從這兩部分的優(yōu)化過程來做一一介紹。
一.啟動階段的優(yōu)化
手機(jī)淘寶作為阿里無線的航母,接入的業(yè)務(wù)Bundle超過100個(gè),啟動初始化任務(wù)超過30個(gè),這些任務(wù)缺少管控和性能監(jiān)控。
那么首要任務(wù)就是:
建立任務(wù)管理機(jī)制
所有的初始化任務(wù)可以用兩個(gè)維度來區(qū)分:
1)任務(wù)必要性:有些任務(wù)是應(yīng)用啟動所必需的,比如網(wǎng)絡(luò)、主容器;有些任務(wù)則不是必需的,僅僅實(shí)現(xiàn)單個(gè)業(yè)務(wù)功能,甚至是為了業(yè)務(wù)自身體驗(yàn)和性能而考慮在啟動階段提前執(zhí)行,其合理性值得推敲。
2)任務(wù)獨(dú)立性:將應(yīng)用的架構(gòu)簡單分成基礎(chǔ)庫、中間件、業(yè)務(wù)三層,這三層中業(yè)務(wù)層最為龐大,其初始化任務(wù)也最多。對于中間件來說,其初始化可能依賴于另外一個(gè)中間件。但對于一個(gè)獨(dú)立的業(yè)務(wù)模塊來說,其初始化任務(wù)應(yīng)該也具有獨(dú)立性,不存在跟其他業(yè)務(wù)模塊依賴關(guān)系。
啟動階段任務(wù)管理機(jī)制包含了如下幾方面的內(nèi)容
1)任務(wù)可并行
既然很多初始化任務(wù)是獨(dú)立的,那么并行執(zhí)行可以提高啟動效率。
2)任務(wù)可串行
雖然我們期望所有初始化任務(wù)都相互獨(dú)立,但是在實(shí)際中不可避免會存在相互依賴的初始化任務(wù)。為了支持這種情況,我們設(shè)計(jì)任務(wù)的異步串行機(jī)制,這里主要借鑒了前端的Promise思想實(shí)現(xiàn)。
3)任務(wù)可插拔
面對這么多不同優(yōu)先級的初始化任務(wù),任何一個(gè)出現(xiàn)異常都會導(dǎo)致應(yīng)用不能啟動,給穩(wěn)定性帶來嚴(yán)重挑戰(zhàn)。因此我們設(shè)計(jì)了可插拔機(jī)制,當(dāng)某一項(xiàng)初始化任 務(wù)出現(xiàn)問題時(shí)能夠跳過該任務(wù),從而不影響整個(gè)應(yīng)用的啟動使用。這里我們根據(jù)初始化任務(wù)的必要性做了區(qū)分,只有非必要的初始化任務(wù)才會應(yīng)用可插拔的特性,這 也是為了防止出現(xiàn)不執(zhí)行一個(gè)必要的初始化任務(wù)導(dǎo)致應(yīng)用啟動使用出現(xiàn)問題。
4)任務(wù)可配置
在ios上通過plist指定每一項(xiàng)啟動任務(wù), 其中字段optional表示該項(xiàng)是否是必需的,當(dāng)之前運(yùn)行出現(xiàn)crash或者異常時(shí),若值為YES則可以不執(zhí)行該項(xiàng)。
有了任務(wù)管理機(jī)制,并引入懶加載的理念,可以持續(xù)地合理有效管控啟動階段的各項(xiàng)初始化任務(wù),是大型app必不可少的環(huán)節(jié)。
檢測超時(shí)方法,優(yōu)化主線程
性能優(yōu)化前,初始化代碼都在主線程中執(zhí)行,為了啟動性能已將部分初始化任務(wù)放入后臺線程或者異步執(zhí)行。但是隨著手淘業(yè)務(wù)發(fā)展和人員變更,還是出現(xiàn) 了在主線程中執(zhí)行很重的初始化任務(wù)。為此,在ios實(shí)現(xiàn)了一套應(yīng)用運(yùn)行時(shí)方法耗時(shí)檢測機(jī)制,能夠?qū)?yīng)用中所有類的方法調(diào)用做耗時(shí)統(tǒng)計(jì)。方便的找到超時(shí)的方 法調(diào)用之后,就可以有針對性的做出修改,或刪除或異步化。這種方法調(diào)用耗時(shí)檢測機(jī)制同樣適用于APP運(yùn)行過程中,從而找到導(dǎo)致應(yīng)用卡頓的根本原因,最后做 出對應(yīng)修改。
多線程治理
分析各個(gè)模塊的線程數(shù)量,檢查線程池的合理性。通過去掉不必要的線程和線程池,再控制線程池的并發(fā)數(shù)和優(yōu)先級。進(jìn)一步通過框架層的線程池來接管業(yè)務(wù)方的線程使用,以減少線程太多的問題。
減少IO讀寫
從自身業(yè)務(wù)出發(fā),去除若干初始化階段不必要的文件操作,以及將若干非實(shí)時(shí)性要求的文件操作延后處理。Android上對于頻繁讀寫數(shù)據(jù)庫和 SharedPreference以及文件的模塊,通過增加緩存和降低采樣率等手段減少對IO的讀寫。對于SharedPreference進(jìn)行了專門的 優(yōu)化,減少單個(gè)文件的大小,將毫無聯(lián)系的存儲鍵值分開到不同文件中,并且防止將大數(shù)據(jù)塊存儲到SharedPreference中,這樣既不利于性能也不 利于內(nèi)存,因?yàn)镾haredPreference會有額外的一份緩存長期存在。
降級部分功能
例如搖一搖功能,測試發(fā)現(xiàn)應(yīng)用場景不頻密,但業(yè)務(wù)使用了高頻率的游戲模式,會耗電及占用主線程時(shí)間。對該功能做了降級處理,降低檢測頻率。同理,對于其他非必須使用但又占據(jù)較多資源的模塊也都做了適當(dāng)?shù)慕导壧幚怼?/p>
熱啟動時(shí)間的縮短
在安卓手機(jī)上我們把啟動分為兩類進(jìn)行檢測和優(yōu)化:冷啟動和熱啟動。冷啟動是程序進(jìn)程不存在的情況下啟動,熱啟動是指用戶將程序切換到后臺或者不斷按Back鍵退出程序,實(shí)際進(jìn)程還存在的情況下點(diǎn)擊圖標(biāo)運(yùn)行。
之前安卓手淘在按Back鍵退出時(shí)整個(gè)首頁Activity銷毀了,熱啟動會經(jīng)過一個(gè)比較長的過程。優(yōu)化后首頁在退出的時(shí)候并不銷毀 Activity,但是會釋放圖片等主要資源,在下次熱啟動時(shí)就能更快的進(jìn)入。另外,將手淘歡迎頁的界面從其它bundle轉(zhuǎn)移到首頁的模塊,在進(jìn)入歡迎 頁時(shí)就開始初始化首頁資源,做到更快展示。
在經(jīng)過一系列的優(yōu)化后,啟動方面已經(jīng)有了明顯的改善,在進(jìn)入首頁的時(shí)候不會卡頓,GC次數(shù)也減少了一半以上。
二.各個(gè)界面的優(yōu)化
各界面優(yōu)化我們也是圍繞著提高幀率和加快展現(xiàn)而展開的,手淘的幾個(gè)主鏈路界面,都是相對比較復(fù)雜的,既使用多圖,也使用了動態(tài)模板的技術(shù)。功能越 復(fù)雜,也越容易產(chǎn)生性能問題,所以常遇到布局復(fù)雜、過渡繪制多、Activity主要函數(shù)耗時(shí)、內(nèi)容展示慢、界面重新布局(Layout)、GC次數(shù)多等 問題。
優(yōu)化GPU的過渡繪制
通過開發(fā)者選項(xiàng)的GPU過渡繪制選項(xiàng)檢查界面的過渡繪制情況。該優(yōu)化并不復(fù)雜,通過去掉層疊布局中多余的背景設(shè)置、圖片控件有前景內(nèi)容的時(shí)候不顯 示背景、界面背景定義到Activity的主題中、減少Drawable的復(fù)雜Shape使用等手段就可以基本消除過渡繪制,減少對GPU和CPU的浪 費(fèi)。
優(yōu)化層級和布局
層級越多,測量和布局的時(shí)間就會相應(yīng)增加,創(chuàng)建硬件列表的時(shí)間也會相應(yīng)增加。有時(shí)我們會嵌套很多布局來實(shí)現(xiàn)原本只要簡單布局就可以實(shí)現(xiàn)的功能,有 時(shí)還會添加一些測試階段才會使用的布局。通過刪除無用的層級,使用Merge標(biāo)簽或者ViewStub標(biāo)簽來優(yōu)化整個(gè)布局性能。比如一些顯示錯(cuò)誤界面、加 載提示框界面等,不是必須顯示的這些布局可以使用ViewStub標(biāo)簽來提升性能。
另外要靈活使用布局,并不是層級越多就會性能越差,有時(shí)候1層的RelativeLayout會比3層嵌套的LinearLayout實(shí)現(xiàn)的性能更糟糕。
除了靈活使用布局,另外我們還通過提前inflate以及在線程中做一些必要的inflate等來提前初始化布局,減少實(shí)際顯示時(shí)候的耗時(shí)。對于一些復(fù)雜的布局,我們還會自己做復(fù)用池,減少inflate帶來的性能損耗,特別是在列表中。
加快界面顯示
1)可以通過TraceView工具找出主線程的耗時(shí)操作和其他耗時(shí)的線程并作優(yōu)化。另外減少主線程的GC停頓,因?yàn)榧词共⑿蠫C,也會對 heap加鎖,如果主線程請求分配內(nèi)存的話,也會被掛起,所以盡量避免在主線程分配較多對象和較大的對象,特別是在onDraw等函數(shù)中,以減少被掛起的 時(shí)間。另外可以通過去掉ListView ,ScrollView等控件的EdgeEffect效果,來減少內(nèi)存分配和加快控件的創(chuàng)建時(shí)間。
2)利用本地緩存,主要界面緩存上次的數(shù)據(jù),并且配合增量的更新和刪除,可以做到數(shù)據(jù)和服務(wù)端同步,這樣可以直接展示本地?cái)?shù)據(jù),不用等到網(wǎng)絡(luò)返回?cái)?shù)據(jù)。
3)減少不必要的數(shù)據(jù)協(xié)議字段,減少名字長度等,并作壓縮。還可以通過分頁加載數(shù)據(jù)來加快傳輸解析時(shí)間。因?yàn)镴SON越大,傳輸和解析時(shí)間也會越久,引發(fā)的內(nèi)存對象分配也會越多。
4)注意線程的優(yōu)先級,對于占用CPU較多時(shí)間的函數(shù),也要判斷線程的優(yōu)先級。
優(yōu)化動畫細(xì)節(jié)
通過TraceView工具發(fā)現(xiàn),一些Banner輪播廣告和文字動畫在移出可視區(qū)域后,仍然存在定時(shí)刷新,不僅耗電也影響幀率。優(yōu)化措施是在移出可視區(qū)域后停止動畫輪播。
阻斷多余requestLayout
在ListView滑動,廣告動畫變化等過程中,圖片和文字有變化,經(jīng)常會發(fā)現(xiàn)整個(gè)界面被重新布局,影響了性能。尤其布局復(fù)雜時(shí),測量過程很費(fèi)時(shí) 導(dǎo)致明顯卡頓。對于大小基本固定的控件和布局例如TextView,ImageView來說,這是多余的損耗。我們可以用自定義控件來阻斷,重寫方法 requestLayout、onSizeChanged,如果大小沒有變化就阻斷這次請求。對于ViewPager等廣告條,可以設(shè)置緩存子view的 數(shù)量為廣告的數(shù)量。
優(yōu)化中間件
中間件的代碼被上層業(yè)務(wù)方調(diào)用的比較頻繁,容易有較多的高頻率函數(shù),也容易產(chǎn)生細(xì)節(jié)上的問題。除了頻繁分配對象外,例如類初始化性能,同步鎖的額外開銷,接口的調(diào)用時(shí)間,枚舉的使用等等都是不能忽視的問題。
減少GC次數(shù)
安卓上的GC會引起性能卡頓,必須重點(diǎn)優(yōu)化。除了第三章會詳細(xì)介紹對于圖片內(nèi)存引起GC的優(yōu)化,我們還做了如下工作:
1)減少對象分配,找出不必要的對象分配,如可以使用非包裝類型的時(shí)候,使用了包裝類型;字符串的+號和擴(kuò)容;Handler.post(Runnable r)等頻繁使用。
2)對象的復(fù)用,對于頻繁分配的對象需要使用復(fù)用池。
3)盡早釋放無用對象的引用,特別是大對象和集合對象,通過置為NULL,及時(shí)回收。
4)防止泄露,除了最基本的文件、流、數(shù)據(jù)庫、網(wǎng)絡(luò)訪問等都要記得關(guān)閉以及unRegister自己注冊的一些事件外,還要盡量少的使用靜態(tài)變量和單例。
5)控制finalize方法的使用,在高頻率函數(shù)中使用重寫了finalize的類,會加重GC負(fù)擔(dān),使得性能上有幾倍的差別。
6)合理選擇容器,在性能上優(yōu)先考慮數(shù)組,即使我們現(xiàn)在習(xí)慣了使用容器,也要注意頻繁使用容器在性能上的隱患點(diǎn):首先是擴(kuò)容開銷, HashMap擴(kuò)容時(shí)重新Hash的開銷較大。其次是內(nèi)存開銷,HashMap需要額外的Map.Entry對象分配 ,需要額外內(nèi)存,也容易產(chǎn)生更多的內(nèi)存碎片。SparseArray和ArrayList等在內(nèi)存方面更有優(yōu)勢。再次是遍歷,對于實(shí)現(xiàn)了 RandomAccess接口的容器如ArryList的遍歷,不應(yīng)該使用foreach循環(huán)。
7)用工具監(jiān)控和精雕細(xì)琢:在頁面滑動過程中,通過Memory Monitor查看內(nèi)存波動和GC情況,還可通過AlloCation Tracker工具觀察內(nèi)存的分配,發(fā)現(xiàn)很多小對象的分配問題。
8)利用Trace For OpenGL工具找出界面上導(dǎo)致硬件加速耗時(shí)的點(diǎn),例如一些圓角圖片的處理等。
通過多種工具和手段配合,手淘各個(gè)界面性能上有了較大的提高,平均幀率提高了20%,那么內(nèi)存節(jié)省50%又是如何實(shí)現(xiàn)的哩,請看下文。
第三章 Android手機(jī)內(nèi)存節(jié)省50%
Android上應(yīng)用出現(xiàn)卡頓的核心原因之一是主線程完成繪制的周期過長引起丟幀。而影響主線程完成繪制時(shí)間的主要有兩方面,一方面是主線程處于運(yùn)行狀態(tài) 時(shí)需要做的任務(wù)太多但CPU資源有限,另外一方面是GC時(shí)Suspend時(shí)直接掛起了所有線程包括主線程。GC對總體性能的影響在4.x的系統(tǒng)上尤為突 出,一部分是單次GC pause總時(shí)長,一部分是用戶操作過程中GC發(fā)生的次數(shù)。而決定這兩部分的因素就是Dalvik內(nèi)存分配。那么在手淘這樣的大型應(yīng)用中到底是誰占用了?內(nèi)存大頭?呢?
誰占用了內(nèi)存
基于雙11前的手淘Android版本,我們在魅藍(lán)note1(4.4 OS)上滑動完首頁后,dump出其Dalvik Heap,整體內(nèi)存占用的分布情況如下圖。可以看出,byte數(shù)組(a)占用空間最大,絕大多數(shù)是用來存放Bitmap的?像素?cái)?shù)據(jù)(Pixel Data)?。另外(c)與(d)一起占用了18.4%, byte數(shù)組加上Bitmap、BitmapDrawable總共占用了64.4%,成為內(nèi)存占用的主體。這也從側(cè)面說明了手淘是以圖片為瀏覽主體內(nèi)容的 大型應(yīng)用。而往往圖片需要較大的內(nèi)存塊,在分配時(shí)引起GC的可能性也往往最大。那我們能不能將圖片這部分需要的內(nèi)存移走而不在Dalvik Heap分配呢?如果能,那么不單GC會明顯減少,同時(shí)Dalvik Heap總大小也會下降50%左右,對整體性能會有顯著的提升。
(點(diǎn)擊放大圖像)
何處安放的Pixel Data
Ashmem即匿名共享內(nèi)存,使用的核心過程是創(chuàng)建一個(gè)/dev/ashmem設(shè)備文件,控制反轉(zhuǎn)設(shè)置文件的名字和大小,最終把設(shè)備符交給 mmap就得到了共享內(nèi)存。在Android系統(tǒng)中Binder進(jìn)程間通信的實(shí)現(xiàn)就是依賴Ashmem完成不同進(jìn)程間的內(nèi)存共享。但此處并不利用其共享特 性,而是使用它在Native Heap完成內(nèi)存分配。圖片空間如何才能使用Ashmem,答案在Facebook推出的Fresco中已有提及,那就是解碼時(shí)的purgeable標(biāo) 記,這樣在系統(tǒng)底層解碼位圖時(shí)會走Ashmem空間分配,而非Dalvik Heap空間。這樣就解決了像素?cái)?shù)據(jù)存放由Dalvik到Native的問題了嗎?
| 1 | BitmapFactory.Options options =?new?BitmapFactory.Options(); |
| 2 | ??/* |
| 3 | ???* inPurgeable can help avoid big Dalvik heap allocations (from API level 11 onward) |
| 4 | ???*/ |
| 5 | ??options.inPurgeable =?true; |
| 6 | ??Bitmap bitmap = BitmapFactory.decodeByteArray(inputByteArray,?0, inputLength, options); |
?
小心Bitmap空包彈
事實(shí)并非那么簡單,最后實(shí)際解出來Bitmap沒有像素?cái)?shù)據(jù)(沒有到Ashmem分配任何空間),根本沒有去完成jpeg或者png解碼。此時(shí)的 Bitmap是個(gè)空包彈!它所做的只是把輸入的解碼前數(shù)據(jù)拷貝到了native內(nèi)存,如果把這個(gè)Bitmap交給ImageView渲染就糟了,在 View.draw()時(shí)Bitmap會在主線程進(jìn)行圖片解碼。
而且不要天真的以為Bitmap解碼一次之后再多次使用都不會引起二次解碼,在系統(tǒng)內(nèi)存緊張時(shí)底層可能回收Ashmem里這部分內(nèi)存。回收后該Bitmap再次渲染時(shí)又將在主線程完成一次解碼。如果就這樣直接使用該機(jī)制,性能上無疑雪上加霜。
那么怎樣才能避免這個(gè)隱形炸彈呢?還好SDK預(yù)留了一個(gè)C層方法AndroidBitmap_lockPixels。而lockPixels底層 完成的工作大致如下圖所示。第一步是prepareBitmap完成真正的數(shù)據(jù)解碼,在工作線程調(diào)用AndroidBitmap_lockPixels避 免了在主線程進(jìn)行數(shù)據(jù)解碼;第二步是完成對分配出來的Ashmem空間的鎖定,這樣即使在系統(tǒng)內(nèi)存緊張時(shí),也不會回收Bitmap像素?cái)?shù)據(jù),避免多次解 碼。
(點(diǎn)擊放大圖像)
貌似解決了Bitmap渲染的所有問題,但在手淘中則不然。為了兼容低版本系統(tǒng)以及提升webp解碼性能,我們使用了自己的解碼庫libwebp.so,怎樣把它解碼出來的數(shù)據(jù)也存放到Ashmem呢?
libwebp借雞生蛋
如果自有解碼庫libwebp.so要解碼到Ashmem,通過SkBitmap、ashmem_create_region實(shí)現(xiàn)一套類似的機(jī)制 是不太現(xiàn)實(shí)的。一方面Skia庫的源碼編譯兼容會存在很大問題,另一方面很多系統(tǒng)層面的核心接口并沒有對外。所以實(shí)現(xiàn)這點(diǎn)的關(guān)鍵還是要借助系統(tǒng)已經(jīng)提供的 purgeable到Ashmem的機(jī)制,借雞生蛋,穩(wěn)定性和成本上都能得到保證:
完成解碼。
更進(jìn)一步,遷移解碼前數(shù)據(jù)
上面談到的內(nèi)存遷移都是針對Decoded像素?cái)?shù)據(jù)的,而Encoded圖像數(shù)據(jù)在解碼時(shí)會在Dalvik Heap保存一份,解碼完成后再釋放;Ashmem方式解碼時(shí)在底層又會拷貝一份到Native內(nèi)存,這份數(shù)據(jù)直到整個(gè)Bitmap回收時(shí)才釋放。那能否 直接將網(wǎng)絡(luò)下載的Encoded數(shù)據(jù)存放到Native內(nèi)存,省去Dalvik Heap上的開銷以及解碼時(shí)的內(nèi)存拷貝呢?的確可以,將網(wǎng)絡(luò)流數(shù)據(jù)直接轉(zhuǎn)移到?MemoryFile可實(shí)現(xiàn),但遺憾的是真機(jī)測試中發(fā)現(xiàn),小米及其他國產(chǎn)“神機(jī)”(自改ROM),多線程使用MemoryFile獲取fd到BitmapFactory解碼, 會出現(xiàn)系統(tǒng)死機(jī),懷疑是在并發(fā)情況下系統(tǒng)代碼級別的死鎖造成。手機(jī)淘寶放棄了這種方案,改用ByteArrayPool復(fù)用池技術(shù)來減少Dalvik Heap針對Encoded Image的內(nèi)存分配,效果也不錯(cuò)。如果應(yīng)用能接受單線程解碼,還是MemoryFile方案更具優(yōu)勢。
是放手的時(shí)候了
上文提到Bitmap像素?cái)?shù)據(jù)存放到Ashmem,有讀者可能擔(dān)心數(shù)據(jù)回收問題,其實(shí)還是由GC來觸發(fā)Ashmem內(nèi)存的回收。在Dalvik層如果一個(gè)Bitmap已經(jīng)不被任何地方引用,那么在下一次GC時(shí)該Bitmap就會從Ashmem中回收,大致流程示意如下圖。
(點(diǎn)擊放大圖像)
再看內(nèi)存占用
我們再次在魅藍(lán)note1中dump出首頁滑動后的內(nèi)存,如下圖可以看出,原來byte數(shù)組(k)大量占用已經(jīng)不存在了,Bitmap(c)與BitmapDrawable(已不在前14名當(dāng)中)的占用也急驟下降。應(yīng)用的總體內(nèi)存下降近60%。
(點(diǎn)擊放大圖像)
在雙11版本上,針對一些熱門機(jī)型在搜索結(jié)果頁不斷滾動使用,進(jìn)行了不同版本的內(nèi)存占用對比分析,如下圖。可以看出,除華為3c和vivo這類系統(tǒng)內(nèi)存偏小使用上一直受到控制、內(nèi)存較為緊張的外,大部分機(jī)型內(nèi)存的下降幅度都達(dá)到45%以上。
(點(diǎn)擊放大圖像)
撓走GC之癢
內(nèi)存下降不是最終目的,最終要將GC對性能的影響降到最低。仍然以魅藍(lán)note1打開首頁后滑動到底的內(nèi)存堆積圖來做對比。可以看到舊版本內(nèi)存占 用上升趨勢相當(dāng)明顯,一路帶有各式“毛刺”直奔70MB,每形成一個(gè)毛刺就意味一次GC。而雙11版本中,內(nèi)存只在初期有上升,而后很快下降到21MB左 右,后期也顯得平滑得多,沒有那么多的“毛刺”,就意味著GC發(fā)生的次數(shù)在明顯減少。
(點(diǎn)擊放大圖像)
舊版本
(點(diǎn)擊放大圖像)
雙11版本
同時(shí)使用一些熱門機(jī)型,針對雙十一版本在首頁不斷滑動,進(jìn)行前后版本的GC_FOR_ALLOC次數(shù)對比。熱門機(jī)型GC次數(shù)下降了4~8倍,效果非常明顯。
通過上文描述的各個(gè)優(yōu)化方案,手機(jī)淘寶于雙十一前在大部分機(jī)型上達(dá)到了521目標(biāo)-Android手機(jī)內(nèi)存節(jié)省50%,啟動時(shí)間和頁面幀率提升20%,H5頁面實(shí)現(xiàn)1s法則。
從持續(xù)不斷的優(yōu)化中,我們也得到了一套優(yōu)化的經(jīng)驗(yàn)閉環(huán),由觀察問題現(xiàn)象到分析原因,建立監(jiān)控,定下量化目標(biāo),執(zhí)行優(yōu)化方案,驗(yàn)證結(jié)果數(shù)據(jù)再回到觀察新問題。每一次閉環(huán)只能解決部分問題,只有不斷抓住細(xì)微的優(yōu)化點(diǎn)“啃”下去,才能得到螺旋上升的良好結(jié)果。
當(dāng)然,隨著手機(jī)機(jī)型的日益碎片化,程序功能的復(fù)雜化多樣化,性能調(diào)優(yōu)是沒有止境的,在部分低端機(jī)和低內(nèi)存手機(jī)上手淘性能問題依然不容樂觀。欲窮千里目,還需更上一層樓,接下來我們還會努力通過更多更細(xì)致的優(yōu)化方案來達(dá)到“如絲般順滑”。
轉(zhuǎn)載于:https://www.cnblogs.com/sybboy/p/8881067.html
總結(jié)
以上是生活随笔為你收集整理的天猫11.11:手机淘宝 521 性能优化项目揭秘的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Mybatis通过colliection
- 下一篇: mac 安装配置java环境变量