主要總結(jié) 用兩周的時(shí)間淺顯的過(guò)了一遍shwen大佬的高手開發(fā)課 高質(zhì)量開發(fā)章節(jié)內(nèi)容赏半,以便后續(xù)繼續(xù)學(xué)習(xí)使用
- 1. native崩潰
- 2. 內(nèi)存優(yōu)化
- 啟動(dòng)優(yōu)化
- I/O優(yōu)化
- 存儲(chǔ)優(yōu)化
- 網(wǎng)絡(luò)優(yōu)化
- UI優(yōu)化
- 安裝包優(yōu)化
1. native崩潰
Android 崩潰分為 Java 崩潰和 Native 崩潰昼弟。
Java 崩潰 : 是在 Java 代碼中悬蔽,出現(xiàn)了未捕獲異常棒仍,導(dǎo)致程序異常退出。
Native 崩潰 : 一般都是因?yàn)樵?Native 代碼中訪問(wèn)非法地址抡蛙,也可能是地址對(duì)齊出現(xiàn)了問(wèn)題护昧,或者發(fā)生了程序主動(dòng) abort,這些都會(huì)產(chǎn)生相應(yīng)的 signal 信號(hào)粗截,導(dǎo)致程序異常退出捏卓。
崩潰捕捉流程
崩潰捕捉難點(diǎn):
- 文件句柄泄漏,導(dǎo)致創(chuàng)建日志文件失敗 (提前申請(qǐng)文件句柄 fd 預(yù)留)
- 棧溢出了慈格,導(dǎo)致日志生成失敗 (signalstack)
- 堆的內(nèi)存都耗盡了,導(dǎo)致日志生成失敗 (Breakpad)
- 堆破壞或二次崩潰導(dǎo)致日志生成失敗 (Breakpad)
使用Breakpad 捕捉Native異常 :鏈接 Github lib 工具類
崩潰分析
崩潰現(xiàn)場(chǎng):
- 崩潰信息 (進(jìn)程名 線程名 崩潰堆棧 )
- 系統(tǒng)信息 (logcat 機(jī)型 系統(tǒng) 廠商 cpu等信息)
- 內(nèi)存信息 (使用內(nèi)存 剩余內(nèi)存)
崩潰嘗試解決:
- 查找可能存在的問(wèn)題 (根據(jù)logcat 日志 等信息定位)
- 嘗試規(guī)避 (try catch 遥金、避免使用某一個(gè)api)
- Hook 解決 (例如 Android 7.0 Toast失效)
2. 內(nèi)存優(yōu)化
內(nèi)存造成的問(wèn)題:
- 異常 (OOM浴捆、內(nèi)存分配失敗)
- 卡頓 (java內(nèi)存不足引發(fā)的頻繁GC gc的時(shí)候程序會(huì)有卡頓 、系統(tǒng)負(fù)載過(guò)高)
舉例子:
Android 3.0到8.0 關(guān)于Bitmap優(yōu)化的演變稿械,從開始的數(shù)據(jù)放native bitmap放java內(nèi)存选泻,到都放Java內(nèi)存,演變到8.0則都放在Native層美莫。
如何將Bitmap定義在Native層
eg:
// 步驟一:申請(qǐng)一張空的 Native Bitmap
Bitmap nativeBitmap = nativeCreateBitmap(dstWidth, dstHeight, nativeConfig, 22);
// 步驟二:申請(qǐng)一張普通的 Java Bitmap
Bitmap srcBitmap = BitmapFactory.decodeResource(res, id);
// 步驟三:使用 Java Bitmap 將內(nèi)容繪制到 Native Bitmap 中
mNativeCanvas.setBitmap(nativeBitmap);
mNativeCanvas.drawBitmap(srcBitmap, mSrcRect, mDstRect, mPaint);
// 步驟四:釋放 Java Bitmap 內(nèi)存
srcBitmap.recycle();
srcBitmap = null页眯;
存在的問(wèn)題:1.版本兼容問(wèn)題 2頻繁申請(qǐng)釋放Java Bitmap
測(cè)量?jī)?nèi)存
adb shell dumpsys meminfo <package_name|pid> [-d]
脫離 Android Studio,實(shí)現(xiàn)一個(gè)自定義的“Allocation Tracker”
原理:
項(xiàng)目使用了 inline hook 來(lái)攔截內(nèi)存對(duì)象分配時(shí)候的 RecordAllocation 函數(shù)厢呵,通過(guò)攔截該接口可以快速獲取到當(dāng)時(shí)分配對(duì)象的類名和分配的內(nèi)存大小窝撵。
在初始化的時(shí)候我們?cè)O(shè)置了一個(gè)分配對(duì)象數(shù)量的最大值,如果從 start 開始對(duì)象分配數(shù)量超過(guò)最大值就會(huì)觸發(fā)內(nèi)存 dump襟铭,然后清空 alloc 對(duì)象列表碌奉,重新計(jì)算。
內(nèi)存優(yōu)化
- 設(shè)備分級(jí) (可參考device-year-class 對(duì)設(shè)備進(jìn)行分級(jí) 寒砖,根據(jù)設(shè)備環(huán)境分配內(nèi)存)
- Bitmap優(yōu)化 (從Java內(nèi)存遷移到Native赐劣、圖片尺寸/質(zhì)量壓縮、超容器大圖處理哩都、重復(fù)圖片優(yōu)化魁兼、Bitmap內(nèi)存及時(shí)回收)
- 緩存管理 (統(tǒng)一緩存管理機(jī)制 負(fù)責(zé)各個(gè)模塊大小)
- 進(jìn)程優(yōu)化 (一個(gè)空進(jìn)程也會(huì)占用很多內(nèi)存漠嵌,避免多余進(jìn)程的使用)
- 內(nèi)存泄漏 (LeakCanary 檢測(cè)對(duì)象生命周期是否異常咐汞、游標(biāo)釋放盖呼、強(qiáng)引用持有對(duì)象回收、Handler 弱引用 碉考、WebView單獨(dú)進(jìn)程維護(hù))
- 資源優(yōu)化 (壓縮圖片塌计、Svg圖片類型使用、so庫(kù)優(yōu)化侯谁、語(yǔ)言文件優(yōu)化等)
啟動(dòng)優(yōu)化
app啟動(dòng)四個(gè)狀態(tài)
- 預(yù)覽窗口顯示 :系統(tǒng)在拉起app進(jìn)程之前锌仅,會(huì)先根據(jù)app的 Theme 屬性創(chuàng)建預(yù)覽窗口。當(dāng)然如果我們禁用預(yù)覽窗口或者將預(yù)覽窗口指定為透明墙贱,用戶在這段時(shí)間依然看到的是桌面热芹。
- 閃屏顯示: 在app進(jìn)程和閃屏窗口頁(yè)面創(chuàng)建完畢,并且完成一系列 inflate view惨撇、onmeasure伊脓、onlayout 等,可以看到歡迎界面了
- 主頁(yè)顯示: 在完成主窗口創(chuàng)建和頁(yè)面顯示的準(zhǔn)備工作后魁衙,用戶可以看到app的主界面报腔,對(duì)應(yīng) onCreate;
- 界面可操作性 : 走到 onResume 方法剖淀,用戶即可進(jìn)行操作
優(yōu)化方式
- 閃屏優(yōu)化 (歡迎界面 設(shè)置默認(rèn)背景)
- 業(yè)務(wù)梳理 (避免Applcation集體初始化纯蛾,可以維護(hù)進(jìn)程池用于不重要模塊的懶加載)
- 線程優(yōu)化 (避免多個(gè)任務(wù)等待執(zhí)行,通過(guò)有向無(wú)環(huán)圖 設(shè)置加載順序 減少 CPU 調(diào)度帶來(lái)的波動(dòng)纵隔,讓應(yīng)用的啟動(dòng)時(shí)間更加穩(wěn)定)
- 安裝包不壓縮 (啟動(dòng)過(guò)程需要的文件翻诉,我們可以指定在安裝包中不壓縮,這樣也會(huì)加快啟動(dòng)速度捌刮,但帶來(lái)的影響是安裝包體積增大)
更黑科技點(diǎn)的優(yōu)化方式
- I/O 優(yōu)化 (磁盤I/O優(yōu)化 sp文件過(guò)大碰煌,db過(guò)大 都需要優(yōu)化,啟動(dòng)過(guò)程不建議出現(xiàn)網(wǎng)絡(luò) I/O)
- 數(shù)據(jù)/類/資源 重排 (ReDex 調(diào)整dex排序 绅作、支付寶資源重排優(yōu)化 )
I/O優(yōu)化
CPU 和內(nèi)存相比磁盤是高速設(shè)備芦圾,整個(gè)流程的瓶頸在于磁盤 I/O 的性能
文件系統(tǒng)
- 虛擬文件系統(tǒng)(VFS)。它主要用于實(shí)現(xiàn)屏蔽具體的文件系統(tǒng)俄认,為應(yīng)用程序的操作提供一個(gè)統(tǒng)一的接口堕扶。
- 文件系統(tǒng)(File System)。ext4梭依、F2FS 都是具體文件系統(tǒng)實(shí)現(xiàn)稍算,文件元數(shù)據(jù)如何組織、目錄和索引結(jié)構(gòu)如何設(shè)計(jì)役拴、怎么分配和清理數(shù)據(jù)糊探,
- 頁(yè)緩存(Page Cache)。在讀文件的時(shí)候會(huì),先看它是不是已經(jīng)在 Page Cache 中科平,如果命中就不會(huì)去讀取磁盤褥紫。
磁盤
在一些低端機(jī)上面,大量跟 I/O 相關(guān)的卡頓原因:
- 內(nèi)存不足瞪慧。當(dāng)手機(jī)內(nèi)存不足的時(shí)候髓考,系統(tǒng)會(huì)回收 Page Cache 和 Buffer Cache 的內(nèi)存,大部分的寫操作會(huì)直接落盤弃酌,導(dǎo)致性能低下
- 寫入放大氨菇。內(nèi)存重復(fù)寫入需要先進(jìn)行擦除操作,但這個(gè)擦除操作的基本單元是 block 塊妓湘,一個(gè) page 頁(yè)的寫入操作將會(huì)引起整個(gè)塊數(shù)據(jù)的遷移查蓉,這就是典型的寫入放大現(xiàn)象
- 由于低端機(jī)的 CPU 和閃存的性能相對(duì)也較差,在高負(fù)載的情況下容易出現(xiàn)瓶頸
系統(tǒng)為了緩解磁盤碎片問(wèn)題榜贴,可以引入 fstrim/TRIM 機(jī)制豌研,在鎖屏、充電等一些時(shí)機(jī)會(huì)觸發(fā)磁盤碎片整理唬党。
mmap
它是通過(guò)把文件映射到進(jìn)程的地址空間鹃共,帶來(lái)的好處:
- 減少系統(tǒng)調(diào)用。我們只需要一次 mmap() 系統(tǒng)調(diào)用驶拱,后續(xù)所有的調(diào)用像操作內(nèi)存一樣及汉,而不會(huì)出現(xiàn)大量的 read/write 系統(tǒng)調(diào)用
- 減少數(shù)據(jù)拷貝。普通的 read() 調(diào)用屯烦,數(shù)據(jù)需要經(jīng)過(guò)兩次拷貝;而 mmap 只需要從磁盤拷貝一次就可以了房铭,并且由于做過(guò)內(nèi)存映射驻龟,也不需要再拷貝回用戶空間。
缺點(diǎn):
- 虛擬內(nèi)存增大
- 磁盤延遲
I/O跟蹤
- Java Hook (找到切入點(diǎn) 缸匪。缺點(diǎn):性能差 沒(méi)辦法監(jiān)控Native代碼 每個(gè)版本源碼可能不同 都需要兼容)
- Native Hook (暫時(shí)了解)
I/O優(yōu)化
- 對(duì)大文件使用 mmap 或者 NIO 方式 (MappedByteBuffer就是 Java NIO 中的 mmap 封裝)
- Buffer 復(fù)用 (Okio開源庫(kù)翁狐,它內(nèi)部的 ByteString 和 Buffer 通過(guò)重用等技巧,很大程度上減少 CPU 和內(nèi)存的消耗)
- 存儲(chǔ)結(jié)構(gòu)和算法的優(yōu)化
存儲(chǔ)優(yōu)化
SharedPreferences 存在的問(wèn)題
- 跨進(jìn)程不安全 (由于沒(méi)有使用跨進(jìn)程的鎖凌蔬,就算使用MODE_MULTI_PROCESS 也會(huì)丟失露懒。)
- 加載緩慢。(sp 文件的加載使用了異步線程砂心,而且加載線程并沒(méi)有設(shè)置線程優(yōu)先級(jí)懈词。)
- 全量寫入 (無(wú)論是調(diào)用 commit() 還是 apply(),即使我們只改動(dòng)其中的一個(gè)條目辩诞,都會(huì)把整個(gè)內(nèi)容全部寫到文件坎弯。而且即使我們多次寫入同一個(gè)文件,SP 也沒(méi)有將多次修改合并為一次)
- 卡頓 (由于提供了異步落盤的 apply 機(jī)制,在崩潰或者其他異常情況可能會(huì)導(dǎo)致數(shù)據(jù)丟失抠忘。當(dāng)應(yīng)用收到系統(tǒng)廣播撩炊,或者被調(diào)用 onPause 等一些時(shí)機(jī),系統(tǒng)會(huì)強(qiáng)制把所有的 sp 對(duì)象數(shù)據(jù)落地到磁盤崎脉。如果沒(méi)有落地完成拧咳,這時(shí)候主線程會(huì)被一直阻塞)
MMKV 的實(shí)現(xiàn)原理,里面有一些非常不錯(cuò)的思路囚灼。例如利用文件鎖保證跨進(jìn)程的安全骆膝、使用 mmap 保證數(shù)據(jù)不會(huì)丟失、選用性能和存儲(chǔ)空間更好的 Protocol Buffer 代替 XML啦撮、支持增量更新等
ContentProvider
ContentProvider 的生命周期默認(rèn)在 Application onCreate() 之前谭网,而且都是在主線程創(chuàng)建的,盡量不做過(guò)多操作赃春,通過(guò) ContentProvider 也可以獲取到數(shù)據(jù)愉择,比如系統(tǒng)提供的 日歷、通訊錄等等
序列化
- Serializable
??Java原生序列化機(jī)制 缺點(diǎn):大量的反射和臨時(shí)變量织中,而且在序列化對(duì)象的時(shí)候锥涕,不僅會(huì)序列化當(dāng)前對(duì)象本身,還會(huì)遞歸序列化對(duì)象引用的其他對(duì)象狭吼。
??我們能做的優(yōu)化:1. writeObject 和 readObject 方法层坠,Serializable在反射的時(shí)候會(huì)先檢查是否自己實(shí)現(xiàn)了 writeObj 和 readObj . 2 writeReplace 和 readResolve 方法,可以自定義序列化返回結(jié)果,
read/writeResolve read/writeObject 順序 區(qū)別
// 序列化
E/test:SerializableTestData writeReplace
E/test:SerializableTestData writeObject
// 反序列化
E/test:SerializableTestData readObject
E/test:SerializableTestData readResolve
Serializable 注意: 類的 static 變量以及被聲明為 transient 的字段刁笙,默認(rèn)的序列化機(jī)制都會(huì)忽略該字段破花。 開發(fā)中盡量設(shè)置 serialVersionUID
??2. Parcelable
在寫入和讀取的時(shí)候都需要手動(dòng)添加自定義代碼,使用起來(lái)相比 Serializable 會(huì)復(fù)雜很多疲吸。但Parcelable 不需要采用反射的方式去實(shí)現(xiàn)序列化和反序列化
??3. Json
優(yōu)點(diǎn):速度更快座每,體積更小,結(jié)果可讀便于排查問(wèn)題摘悴。使用方便峭梳,支持跨平臺(tái)、跨語(yǔ)言蹂喻,支持嵌套引用
??4. Protocol Buffers
由于使用二進(jìn)制格式葱椭,相比Json 體積更小,傳輸更快口四。
??5. SQLite
??SQLite本身是支持多線程 多進(jìn)程操作的孵运,通過(guò)文件鎖來(lái)控制多進(jìn)程的并發(fā)。SQLite 鎖的粒度精確到db蔓彩,沒(méi)有到某一行掐松。
??多進(jìn)程可以同時(shí)獲取 SHARED 鎖來(lái)讀取數(shù)據(jù)踱侣,但是只有一個(gè)進(jìn)程可以獲取 EXCLUSIVE 鎖來(lái)寫數(shù)據(jù)庫(kù)。
??多線程情況下可以使用連接池大磺,或者使用WAL模式抡句,將讀和寫完全的并發(fā)執(zhí)行。
??通過(guò)正確的建立索引杠愧,可以提升 SQLite 的查詢速度待榔。通過(guò)調(diào)整默認(rèn)的頁(yè)大小和緩存大小,可以提升 SQLite 的整體性能流济。
網(wǎng)絡(luò)優(yōu)化
網(wǎng)絡(luò)優(yōu)化大頭在網(wǎng)絡(luò)請(qǐng)求:
- DNS 解析 (通過(guò) DNS 服務(wù)器锐锣,拿到對(duì)應(yīng)域名的 IP 地址 優(yōu)化點(diǎn):HTTPDNS 百度DNS優(yōu)化)
- 創(chuàng)建連接 (服務(wù)器建立連接,這里包括 TCP 三次握手绳瘟、TLS 密鑰協(xié)商等工作 優(yōu)化點(diǎn):連接復(fù)用雕憔,通過(guò)將連接存放連接池和Http2.0的多路復(fù)用 或者Http1.0的 keep-alive 都對(duì)連接有優(yōu)化作用)
- 發(fā)送 / 接收數(shù)據(jù) (如何根據(jù)網(wǎng)絡(luò)狀況將帶寬利用好,怎么樣快速地偵測(cè)到網(wǎng)絡(luò)延時(shí)糖声,在弱網(wǎng)絡(luò)下如何調(diào)整包大小 優(yōu)化點(diǎn):Http2.0 頭部壓縮 和 請(qǐng)求Gzip壓縮 算法壓縮等方式)
- 關(guān)閉連接 (從主動(dòng)關(guān)閉和被動(dòng)關(guān)閉兩種情況優(yōu)化)
UI優(yōu)化
關(guān)于屏幕適配可以參考限寬適配 和 今日頭條UI適配
優(yōu)化點(diǎn):
- 盡量使用硬件加速 (Gpu可以分擔(dān)cpu渲染工作量提升速度)
- View的異步加載 /重用 (子線程Looper替換為UI線程Looper 用完記得還原 斤彼。重用可以參考 ListView 和 RecycleView復(fù)用原理)
- measure/layout 優(yōu)化 (減少UI層級(jí) 推薦使用ConstraintLayout 代替Rl 和Ll ,避免重復(fù)設(shè)置背景)
- Compose 學(xué)習(xí)和使用 (聲明式UI布局 讓界面更加簡(jiǎn)單 減少xml解析時(shí)間 蘸泻,ui的未來(lái))
安裝包優(yōu)化
- 代碼方面 可以使用 ProGuard對(duì)代碼進(jìn)行壓縮簡(jiǎn)化琉苇,但是對(duì)混淆規(guī)則 要避免重復(fù)keep 。更高級(jí)的牽扯到dex的壓縮 和 Native Lib的分割和合并 參考:redex github Lib合并參考
- 資源壓縮可以參考 AndResGuard AndResGuard美團(tuán)講解 和 騰訊的 matrix思路(官方提供: Lint 和 shrinkResources)