Android 高級面試-7:網絡相關的三方庫和網絡協議等
1、網絡框架
問題:HttpUrlConnection, HttpClient, Volley 和 OkHttp 的區別?
HttpUrlConnection 的基本使用方式如下:
URL url = new URL("http://www.baidu.com"); // 創建 URLHttpURLConnection connection = (HttpURLConnection) url.openConnection(); // 獲取 HttpURLConnectionconnection.setRequestMethod("GET"); // 設置請求參數connection.setConnectTimeout(5 * 1000);connection.connect();InputStream inputStream = connection.getInputStream(); // 打開輸入流byte[] data = new byte[1024];StringBuilder sb = new StringBuilder();while (inputStream.read(data) != -1) { // 循環讀取String s = new String(data, Charset.forName("utf-8"));sb.append(s);}message = sb.toString();inputStream.close(); // 關閉流connection.disconnect(); // 關閉連接
HttpURLConnect 和 HttpClient 的對比:
- 功能方面:HttpClient 庫要
豐富很多,提供了很多工具
,封裝了 http 的請求頭,參數,內容體,響應,還有一些高級功能,代理、COOKIE、鑒權、壓縮、連接池的處理。HttpClient 高級功能代碼寫起來比較復雜
,對開發人員的要求會高一些,而 HttpURLConnection 對大部分工作進行了包裝,屏蔽了不需要的細節,適合開發人員直接調用
。另外,HttpURLConnection 在 2.3 版本增加了一些 HTTPS 方面的改進,4.0 版本增加一些響應的緩存。 - 穩定性上:HttpURLConnect 是一個通用的、適合大多數應用的輕量級組件。這個類起步比較晚,很容易在主要 API 上做穩步的改善。但是 HttpURLConnection 在 Android 2.2 及以下版本上
存在一些 bug
,尤其是在讀取 InputStream 時調用close()
方法,可能會導致連接池失效。Android2.3 及以上版本
建議選用 HttpURLConnection,2.2 及以下版本建議選用 HttpClient。 - 拓展方面:HttpClient 的 API 數量過多,使得我們很難在不破壞兼容性的情況下對它進行升級和擴展,所以,目前 Android 團隊在提升和優化 HttpClient 方面的
工作態度并不積極
。
OkHttp 和 Volley 的對比:
- OkHttp:現代、快速、高效的 Http 客戶端,支持
HTTP/2 以及 SPDY
. Android 4.4 的源碼中可以看到 HttpURLConnection 已經替換成 OkHttp 實現了。OkHttp處理了很多網絡疑難雜癥
:會從很多常用的連接問題中自動恢復。OkHttp 還處理了代理服務器問題和 SSL 握手失敗問題。 - Volley:適合進行
數據量不大,但通信頻繁的網絡操作
;內部分裝了異步線程;支持 Get,Post 網絡請求和圖片下載;可直接在主線程調用服務端并處理返回結果。缺點是:1).對大文件下載 Volley 的表現非常糟糕;2).只支持 http 請求
。Volley 封裝了訪問網絡的一些操作,底層在 Android 2.3 及以上版本,使用的是 HttpURLConnection,而在 Android 2.2 及以下版本,使用的是 HttpClient.
問題:OkHttp 源碼?
首先從整體的架構上面看,OkHttp 是基于責任鏈設計模式
設計的,責任鏈的每一個鏈叫做一個攔截器。OkHttp 的請求是依次通過重試、橋接、緩存、連接和訪問服務器
五個責任鏈,分別用來:1).根據請求的錯誤碼
決定是否需要對連接進行重試;2).根據請求信息構建一個 key 用來從 DiskLruCache 中獲取緩存,然后根據緩存的響應的信息判斷該響應是否可用;3).緩存不可用的時候,使用連接攔截器建立服務器連接;4).最終在最后一個責任鏈從服務器中拿到請求結果。當從網絡當中拿到了數據之后,會回到緩存連接器,然后在這里根據響應的信息和用戶的配置決定是否緩存本次請求。除此默認的連接器,我們還可以自定義自己的攔截器。
OkHttp 的網絡訪問并沒有直接使用 HttpUrlConnection 或者 HttpClient,而是直接使用 Socket 建立網絡連接,對于流的讀寫,它使用了第三方的庫 okio
。在拿到一個請求的時候,OkHttp 首先會到連接池中尋找可以復用的連接。這里的連接池是使用雙端隊列維護的一個列表。當從連接池中獲取到一個連接之后就使用它來進行網絡訪問。
問題:Volley 實現原理?
RequestQueue queue = Volley.newRequestQueue(this);// 針對不同的請求類型,Volley 提供了不同的 Requestqueue.add(new StringRequest(Request.Method.POST, "URL", new Response.Listener<String>() {@Overridepublic void onResponse(String response) {}}, new Response.ErrorListener() {@Overridepublic void onErrorResponse(VolleyError error) {}}));
底層在 Android 2.3 及以上版本,使用的是 HttpURLConnection,而在 Android 2.2 及以下版本,使用的是 HttpClient. 當創建一個 RequestQueue 的時候會同時創建 4 條線程用于從網絡中請求數據,一條緩存線程用來從緩存中獲取數據。因此不適用于數據量大、通訊頻繁的網絡操作,因為會占用網絡請求的訪問線程。
當調用 add()
方法的時候,會先判斷是否可以使用緩存,如果可以則將其添加到緩存隊列中進行處理。否則將其添加到網絡請求隊列中,用來從網絡中獲取數據。在緩存的分發器中,會開啟一個無限的循環不斷進行工作,它會先從阻塞隊列中獲取一個請求,然后判斷請求是否可用,如果可用的話就將其返回了,否則將請求添加到網絡請求隊列中進行處理。
網絡請求隊列與之類似,它也是在 run()
方法中啟動一個無限循環,然后使用阻塞隊列獲取請求,拿到請求之后來從網絡中獲取響應結果。
問題:網絡請求緩存處理,OkHttp 如何處理網絡緩存的?
問題:Http 請求頭中都有哪些字段是與緩存相關的?
問題:緩存到磁盤上的時候,緩存的鍵是根據哪些來決定的?
OkHttp 的緩存最終是使用的 DiskLruCache
將請求的請求和響應信息存儲到磁盤上。當進入到緩存攔截器的時候,首先會先從緩存當中獲取請求的請求信息和響應信息。它會從響應信息的頭部獲取本次請求的緩存信息,比如過期時間之類的,然后判斷該響應是否可用。如果可用,或者處于未連網狀態等,則將其返回。否則,再從網絡當中獲取請求的結果。當拿到了請求的結果之后,還會再次回到緩存攔截器。緩存攔截器在拿到了響應之后,再根據響應和請求的信息決定是否將其持久化到磁盤上面。
http 請求頭中用來控制決定緩存是否可用的信息:
Expires
:緩存過期時間,用來指定資源到期的時間,是服務器端的具體的時間點;Cache-Control
:相對時間,例如 Cache-Control:3600,代表著資源的有效期是 3600 秒,Cache-Control 與 Expires 可以在服務端配置同時啟用或者啟用任意一個,同時啟用的時候Cache-Control 優先級高。Last-Modify/If-Modify-Since
:Last-modify 是一個時間標識該資源的最后修改時間。ETag/If-None-Match
:一個校驗碼,ETag 可以保證每一個資源是唯一的,資源變化都會導致ETag 變化。ETag 值的變更則說明資源狀態已經被修改。服務器根據瀏覽器上發送的 If-None-Match 值來判斷是否命中緩存。
客戶端第一次請求的時候,服務器會返回上述各種 http 信息,第二次請求的時候會根據下面的流程進行處理:
問題:Retrofit 源碼?
Retrofit 的源碼的兩個核心地方:
- 代理設計模式:JDK 動態代理,核心的地方就是調用
Proxy.newProxyInstance()
方法,來獲取一個代理對象,但是這個方法要求傳入的類的類型必須是接口類型,然后通過傳入的InvocationHandler
接口對我們定義的 Service 接口的方法進行解析,獲取方法的注解等信息,并將其緩存起來。 - 適配器設計模式和策略模式:設配器主要兩個地方,也是 Retrofit 為人稱道的地方,一個是對結果類型的適配,一個是對服務端返回類型的處理。前面的更像是適配器,后面的更像是策略接口。
- 以 RxJava 為例,當代理類的方法被調用的時候會返回一個 Observable. 然后,當我們對 Observable 進行訂閱的時候將會調用
subscribeActual()
,在該方法中根據之前解析的接口方法信息,將它們拼接成一個 OkHttp 的請求,然后使用 OkHttp 從網絡中獲取數據。 - 當拿到了數據之后就是如何將數據轉換成我們期望的類型。這里 Retrofit 也將其解耦了出來。Retrofit 提供了
Converter
用作 OkHttp 的響應到我們期望類型的轉換器。我們可以通過自己定義來實現自己的轉換器,并選擇自己滿意的 Json 等轉換框架。
- 以 RxJava 為例,當代理類的方法被調用的時候會返回一個 Observable. 然后,當我們對 Observable 進行訂閱的時候將會調用
2、網絡基礎
2.1 TCP 和 UDP
問題:TCP 與 UDP 區別與應用?
問題:TCP 中 3 次握手和 4 次揮手的過程:
TCP,傳輸控制協議
,面向連接
,可靠的
,基于字節流
的傳輸層通信協議;
UDP,用戶數據報協議
,面向無連接
,不可靠
,基于數據報
的傳輸層協議。
應用場景:
TCP 被用在對不能容忍數據丟失的場景中,比如用來發送 Http;
UDP 用來可以容忍丟失的場景,比如網絡視頻流的傳輸。
具體區別:
- TCP 協議是
有連接的
,有連接的意思是開始傳輸實際數據之前 TCP 的客戶端和服務器端必須通過三次握手建立連接,會話結束之后也要結束連接。而 UDP 是無連接的。 - TCP 協議保證數據
按序發送
,按序到達
,提供超時重傳
來保證可靠性,但是 UDP不保證按序到達
,甚至不保證到達
,只是努力交付,即便是按序發送的序列,也不保證按序送到。 - TCP 協議
所需資源多
,TCP 首部需 20 個字節(不算可選項),UDP 首部字段只需8個字節。 - TCP 有
流量控制和擁塞控制
,UDP 沒有,網絡擁堵不會影響發送端的發送速率。 - TCP 是
一對一
的連接,而 UDP 則可以支持一對一、多對多、一對多
的通信。 - TCP 面向的是
字節流
的服務,UDP 面向的是報文
的服務。
TCP 握手過程:
- 客戶端通過 TCP 向服務器發送
SYN 報文段
。它不包含應用層信息,其中的SYN 標志位為 1
,然后選擇一個初始序號 (client_isn)
,并將其放置在報文段的序號字段中。 - 當 SYN 報文段到達服務器之后,服務器為該 TCP 連接分配 TCP 緩存和變量,并向該客戶端發送
SYNACK 報文段
。它不包含應用層信息,其中個的SYN 置為 1
,確認號字段
被置為client_isn+1
,最后服務器選擇自己的初始序號 (server_isn)
放在序號字段中。 - 客戶端收到 SYNACK 報文段之后,為連接分配緩存和變量,然后向服務器發送另一個報文段,其中將
server_isn+1
放在確認字段
中,并將SYN 位置為 0
.
問題:為什么要三次握手?
三次握手的目的是建立可靠的通信信道,說到通訊,簡單來說就是數據的發送與接收,而三次握手最主要的目的就是雙方確認自己與對方的發送與接收是正常的
:
- 第一次握手:Client 什么都不能確認;Server 確認了對方發送正常。
- 第二次握手:Client 確認了:自己發送、接收正常,對方發送、接收正常;Server 確認了:自己接收正常,對方發送正常。
- 第三次握手:Client 確認了:自己發送、接收正常,對方發送、接收正常;Server 確認了:自己發送、接收正常,對方發送接收正常。
- 客戶端向服務器發送關閉連接報文段,其中
FIN
置為 1。 - 服務器接收到該報文段之后向發送方會送一個確認字段。
- 服務器向客戶端發送自己的終止報文段。
- 客戶端對服務器終止報文段進行確認。
問題:為什么要四次揮手?
任何一方都可以在數據傳送結束后發出連接釋放的通知,待對方確認后進入半關閉狀態。當另一方也沒有數據再發送的時候,則發出連接釋放通知,對方確認后就完全關閉了 TCP 連接。舉個例子:A 和 B 打電話,通話即將結束后,A 說“我要掛了”,B 回答 “我知道了”,但是 A 可能還會有要說的話,所以隔一段時間,B 再問 “真的要掛嗎”,A 確認之后通話才算結束。
問題:三次握手建立連接時,發送方再次發送確認的必要性?
主要是為了防止已失效的連接請求報文段突然又傳到了 B,因而產生錯誤。假定出現一種異常情況,即 A 發出的第一個連接請求報文段并沒有丟失,而是在某些網絡結點長時間滯留了,一直延遲到連接釋放以后的某個時間才到達 B,本來這是一個早已失效的報文段。但 B 收到此失效的連接請求報文段后,就誤認為是 A 又發出一次新的連接請求,于是就向 A 發出確認報文段,同意建立連接。假定不采用三次握手,那么只要 B 發出確認,新的連接就建立了,這樣一直等待 A 發來數據,B 的許多資源就這樣白白浪費了。
2.2 Http
2.2.1 Http 協議
- 全稱
超文本傳輸協議
**`; - HTTP使用
TCP
作為它的支撐協議,TCP 默認使用80
端口; - 它是
無狀態的
,就是說它不會記錄你之前是否訪問過某個對象,它不保存任何關于客戶的信息; - 它有兩種連接方式,
非持續連接和持續連接
。它們的區別在于,非持續連接發送一個請求獲取內容之后,對內容里的鏈接會再分別發送 TCP 請求獲取;持續連接當獲取到內容之后,復用之前的 TCP
獲取相關的內容。后者節省了建立連接的時間,效率更高。
2.2.2 HTTP 請求報文
GET /somedir/page.jsp HTTP/1.1 部分 1:請求方法-統一資源標識符(URI)-協議/版本
Accept: text/plain; text/html 部分 2:請求頭
Accept-Language: en-gb
Connection: keep-Alive
Host: localhost
User-Agent: Mozilla/4.0
Content-Length: 33
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate lastName=Franks&firstName=Michael 部分 3:實體
- 請求方法共有
GET、POST、HEAD、PUT 和 DELETE
等,其中GET
大約占 90%;HEAD 類似 GET,但不返回請求對象;PUT 表示上傳對象到服務器;DELETE 表示刪除服務器上的對象。 URI
是相應的URI
的后綴,通常被解釋為相對于服務器根目錄的路徑;- 請求頭包含客戶端和實體正文的相關信息,各個請求頭之間使用
“換行/回車”符(CRLF)
隔開; - 請求頭和實體之間有一個空行,該空行只有 CRLF 符,對 HTTP 格式非常重要。
Host
指明對象主機,它在 Web 代理高速緩存中有作用;Connection
可取的值有 keep-Alive 和 close,分別對應持續連接和非持續連接;User-Agent
指明向服務器發送請求的瀏覽器。
2.2.3 HTTP 響應報文
HTTP/1.1 200 OK 部分 1:協議-狀態碼-描述
Server: Microft-IIS/4.0 部分 2:響應頭
Date: Mon, 5 Jan 2004 12:11:22 GMT
Content-Type: text/html
Last-Modified: Mon, 5 Jan 2004 11:11:11 GMT
Content-Length: 112 <html>.....</html> 部分 3:響應實體段
- 響應頭和響應實體之間使用一個 CRLF 符分隔;
Last-Modified
緩存服務器中有作用;- 狀態碼的五種可能取值:
1xx
:指示信息–表示請求已接收,繼續處理2xx
:成功–表示請求已被成功接收、理解、接受3xx
:重定向–要完成請求必須進行更進一步的操作4xx
:客戶端錯誤–請求有語法錯誤或請求無法實現5xx
:服務器端錯誤–服務器未能實現合法的請求
- 常見的狀態碼:
200
OK:請求成功;301
Moved Permanelty: 請求對象被永久轉移;302
重定向只是暫時的重定向,搜索引擎會抓取新的內容而保留舊的地址,搜索搜索引擎認為新的網址是暫時的。而 301 重定向是永久的,搜索引擎在抓取新的內容的同時也將舊的網址替換為了重定向之后的網址。400
Bad Request: 請求不被服務器理解;404
Not Found: 請求的文檔不在服務器;503
Service Unavailable:服務器出錯的一種返回狀態;505
HTTP Version Not Supperted: 服務器不支持的HTTP協議。
2.2.4 HTTP 1.0 與 2.0 的區別
- HTTP/2 采用
二進制格式
而非文本格式; - HTTP/2 是完全
多路復用
的,而非有序并阻塞的——只需一個連接即可實現并行(多路復用允許單一的 HTTP/2 連接同時發起多重的請求-響應消息); - 使用
報頭壓縮
,HTTP/2 降低了開銷(不使用原來的頭部的字符串,比如 UserAgent 等,而是從字典
中獲取,這需要在支持 HTTP/2 的瀏覽器和服務端之間運行); - HTTP/2 讓服務器可以將響應主動
推送
到客戶端緩存中(說白了,就是 HTTP2.0 中,瀏覽器在請求 HTML 頁面的時候,服務端會推送 css、js 等其他資源給瀏覽器,減少網絡空閑浪費)。
2.2.5 Http 長連接
在HTTP/1.0
中默認使用短連接。也就是說,客戶端和服務器每進行一次 HTTP 操作,就建立一次連接,任務結束就中斷連接。當客戶端瀏覽器訪問的某個 HTML 或其他類型的 Web 頁中包含有其他的 Web 資源(如 JavaScript 文件、圖像文件、CSS 文件等),每遇到這樣一個 Web 資源,瀏覽器就會重新建立一個 HTTP 會話。
從HTTP/1.1
起,默認使用長連接,用以保持連接特性。使用長連接的 HTTP 協議,會在響應頭加入這行代碼 Connection:keep-alive
。在使用長連接的情況下,當一個網頁打開完成后,客戶端和服務器之間用于傳輸 HTTP 數據的 TCP 連接不會關閉
,客戶端再次訪問這個服務器時,會繼續使用這一條已經建立的連接。Keep-Alive 不會永久保持連接,它有一個保持時間
,可以在不同的服務器軟件(如 Apache)中設定這個時間。實現長連接需要客戶端和服務端都支持長連接
。
HTTP 協議的長連接和短連接,實質上是 TCP 協議的長連接和短連接。
長連接可以省去較多的 TCP 建立和關閉的操作,減少浪費,節約時間。對于頻繁請求資源的客戶來說,較適用長連接。不過這里存在一個問題,存活功能的探測周期太長,還有就是它只是探測 TCP 連接的存活,屬于比較斯文的做法,遇到惡意的連接時,保活功能就不夠使了。在長連接的應用場景下,client 端一般不會主動關閉它們之間的連接,Client 與 server 之間的連接如果一直不關閉的話,會存在一個問題,隨著客戶端連接越來越多,server 早晚有扛不住的時候,這時候 server 端需要采取一些策略,如關閉一些長時間沒有讀寫事件發生的連接,這樣可以避免一些惡意連接導致 server 端服務受損;如果條件再允許就可以以客戶端機器為顆粒度,限制每個客戶端的最大長連接數,這樣可以完全避免某個客戶端連累后端服務。
2.3 Https
問題:Https 請求慢的解決辦法?DNS,攜帶數據,直接訪問 IP
問題:Http 與 Https 的區別以及如何實現安全性?
問題:Https 原理?
問題:Https 相關,如何驗證證書的合法性,Https 中哪里用了對稱加密,哪里用了非對稱加密,對加密算法(如 RSA)等是否有了解?
2.3.1 Https 連接的過程
SSL 協議的握手過程共分成 5 各步驟,
- 第一步,客戶端給出
協議版本號
、一個客戶端生成的隨機數
,以及客戶端支持的加密方法
; - 第二步,服務器確認雙方使用的
加密方法
,并給出數字證書
、以及一個服務器生成的隨機數
; - 第三步,客戶端確認
數字證書
有效,然后生成一個新的隨機數
,并使用數字證書中的公鑰
加密這個隨機數,發給服務器。 - 第四步,服務器使用自己的
私鑰
,獲取客戶端發來的隨機數。 - 第五步,客戶端和服務器根據約定的
加密方法
,使用前面的三個隨機數
,生成對話密鑰
來加密接下來的整個對話過程。
握手階段有三點需要注意。
- 生成對話密鑰一共需要
三個隨機數
,然后使用這三個隨機數來最終確定通話使用的算法; - 握手之后的對話使用對話密鑰,服務器的公鑰和私鑰只用于加密和解密對話密鑰,無其他作用;(握手之后開啟的正式對話使用的是
對稱加密
,即雙方都能通過密鑰進行解密;握手的過程中,協商最終使用哪種加密算法通話的時候是非對稱加密
,即私鑰加密后的密文,只要是公鑰,都可以解密,但是公鑰加密后的密文,只有私鑰可以解密。這樣,服務端發送給客戶端的消息不安全,但是客戶端回復給服務端的消息是安全的。因為最后還要發送一個隨機數用來確定最終的算法,所以這個過程安全就保證了最終的通話密鑰是安全的。) - 服務器公鑰放在服務器的數字證書之中。
然而直接使用非對稱加密的過程本身也不安全
,會有中間人篡改公鑰
的可能性,所以客戶端與服務器不直接使用公鑰,而是使用數字證書簽發機構頒發的證書
來保證非對稱加密過程本身的安全。第三方使用自己的私鑰對公鑰進行加密
,生成一個證書
。然后客戶端從該證書中讀取出服務器的公鑰
。那么證書的合法性如何確認呢?我們可以使用瀏覽器或操作系統中維護的權威的第三方頒發機構的公鑰,驗證證書的編號是否正確。然后再使用第三方結構的公鑰解密出我們服務器的公鑰即可。
2.3.2 HTTPS 與 HTTP 的一些區別
- HTTPS 協議需要到 CA 申請證書,一般免費證書很少,需要
交費
。 - HTTP 協議運行在
TCP
之上,所有傳輸的內容都是明文
,HTTPS 運行在SSL/TLS
之上,所有傳輸的內容都經過加密
的。 - HTTP 和 HTTPS 使用的是完全不同的連接方式,用的端口也不一樣,前者是
80
,后者是443
。 - HTTPS 可以有效的
防止運營商劫持
,解決了防劫持的一個大問題。
2.4 其他網絡相關
問題:描述一次網絡請求的流程?
瀏覽器輸入域名之后,首先通過 DNS 查找該域名對應的 IP 地址。查找的過程會使用多級的緩存,包括瀏覽器、路由器和 DNS 的緩存。查找的 IP 地址之后,客戶端向 web 服務器發送一個 HTTP 連接請求。服務器收到客戶端的請求之后處理請求,并返回處理結果。客戶端收到服務端返回的結果后將視圖呈現給用戶。
問題:WebSocket 相關以及與 Socket 的區別
問題:談談你對 WebSocket 的理解
問題:WebSocket 與 socket 的區別
WebSocket 是 HTML5 提供的一種在單個 TCP 連接
上進行全雙工
通訊的協議,允許服務端主動向客戶端推送數據。瀏覽器和服務器只需要完成一次握手
,兩者之間就直接可以創建持久性的連接,并進行雙向數據傳輸
。WebSocket 的請求和響應報文的結構與 Http 相似
。相比于 ajax 這種通過不斷輪詢
的方式來從服務端獲取請求的方式,它通過類似于推送
的方式通知客戶端,可以節省更多的網絡資源。
跟 Socket 的區別:Socket 其實并非一個協議
,是應用層與 TCP/IP 協議族通信的中間軟件抽象層,它是一組接口。當兩臺主機通信時,讓 Socket 去組織數據,以符合指定的協議。TCP 連接則更依靠于底層的 IP 協議,IP 協議的連接則依賴于鏈路層等更低層次。WebSocket 則是一個典型的應用層協議
。總的來說:Socket 是傳輸控制層協議
,WebSocket 是應用層協議
。
參考:
- 《Volley使用及其原理解析》
- 《也許,這樣理解HTTPS更容易》
另外
有什么技術問題歡迎加我交流 qilebeaf
本人10多年大廠軟件開發經驗,精通Android,Java,Python,前端等開發,空余時間承接軟件開發設計、課程設計指導、解決疑難bug、AI大模型搭建,AI繪圖應用等。
歡迎砸單# Android 高級面試-6:性能優化
1、內存優化
1.1 OOM
問題:OOM 的幾種常見情形?
數據太大
:比如加載圖片太大,原始的圖片沒有經過采樣,完全加載到內存中導致內存爆掉。內存泄漏
內存抖動
:內存抖動是指內存頻繁地分配和回收,而頻繁的 GC
會導致卡頓,嚴重時還會導致 OOM。一個很經典的案例是 String 拼接時創建大量小的對象。此時由于大量小對象頻繁創建,導致內存不連續,無法分配大塊內存,系統直接就返回 OOM 了。
問題:OOM 是否可以 Try Catch ?
Catch 是可以 Catch 到的,但是這樣不符合規范,Error 說明程序中發生了錯誤,我們應該使用引用四種引用、增加內存或者減少內存占用來解決這個問題。
1.2 內存泄漏
問題:常見的內存泄漏的情形,以及內存泄漏應該如何分析?
單例
引用了 Activity 的 Context,可以使用Context.getApplicationContext()
獲取整個應用的 Context 來使用;靜態變量
持有 Activity 的引用,原因和上面的情況一樣,比如為了避免反復創建一個內部實例的時候使用靜態的變量;非靜態內部類
導致內存泄露,典型的有:- Handler:Handler 默認持有外部 Activity 的引用,發送給它的 Message 持有 Handler 的引用,Message 會被放入 MQ 中,因此可能會造成泄漏。解決方式是使用弱引用來持有外部 Activity 的引用。另一種方式是在 Activity 的
onDestroy()
方法中調用mHandler.removeCallbacksAndMessages(null)
從 MQ 中移除消息。 后者更好一些!因為它移除了 Message. - 另一種情形是使用非靜態的 Thread 或者 AsyncTask,因為它們持有 Activity 的引用,解決方式是使用
靜態內部類+弱引用
。
- Handler:Handler 默認持有外部 Activity 的引用,發送給它的 Message 持有 Handler 的引用,Message 會被放入 MQ 中,因此可能會造成泄漏。解決方式是使用弱引用來持有外部 Activity 的引用。另一種方式是在 Activity 的
廣播
:未取消注冊廣播。在 Activity 中注冊廣播,如果在 Activity 銷毀后不取消注冊,那么這個剛播會一直存在系統中,同上面所說的非靜態內部類一樣持有 Activity 引用,導致內存泄露。資源
:未關閉或釋放導致內存泄露。使用 IO、File 流或者 Sqlite、Cursor 等資源時要及時關閉。這些資源在進行讀寫操作時通常都使用了緩沖,如果及時不關閉,這些緩沖對象就會一直被占用而得不到釋放,以致發生內存泄露。屬性動畫
:在 Activity 中啟動了屬性動畫(ObjectAnimator),但是在銷毀的時候,沒有調用 cancle 方法,雖然我們看不到動畫了,但是這個動畫依然會不斷地播放下去,動畫引用所在的控件,所在的控件引用 Activity,這就造成 Activity 無法正常釋放。因此同樣要在 Activity 銷毀的時候 cancel 掉屬性動畫,避免發生內存泄漏。WebView
:WebView 在加載網頁后會長期占用內存而不能被釋放,因此我們在 Activity 銷毀后要調用它的 destory() 方法來銷毀它以釋放內存。
1.3 內存優化相關的工具
檢查內存泄漏
:Square 公司開源的用于檢測內存泄漏的庫,LeakCanary.Memory Monitor
:AS 自帶的工具,可以用來主動觸發 GC,獲取堆內存快照文件以便進一步進行分析(通過叫做 Allocation Tracker 的工具獲取快照)。(屬于開發階段使用的工具,開發時應該多使用它來檢查內存占用。)Device Monitor
:包含多種分析工具:線程,堆,網絡,文件等(位于 sdk 下面的 tools 文件夾中)。可以通過這里的 Heap 選項卡的 Cause GC 按鈕主動觸發 GC,通過內存回收的狀態判斷是否發生了內存泄漏。MAT
:首先通過 DDMS 的 Devices 選項卡下面的 Dump HPROF File 生成 hrpof 文件,然后用 SDK 的 hprof-conv 將該文件轉成標準 hprof 文件,導入 MAT 中進行分析。
3、ANR
問題:ANR 的原因
問題:ANR 怎么分析解決
滿足下面的一種情況系統就會彈出 ANR 提示
輸入事件 (按鍵和觸摸事件) 5s
內沒被處理;- BroadcastReceiver 的事件 ( onRecieve() 方法) 在規定時間內沒處理完 (
前臺廣播為 10s,后臺廣播為 60s
); - Service
前臺 20s 后臺 200s
未完成啟動; - ContentProvider 的
publish() 在 10s
內沒進行完。
最終彈出 ANR 對話框的位置是與 AMS 同目錄的類 AppErrors 的 handleShowAnrUi()
方法。最初拋出 ANR 是在 InputDispatcher.cpp 中。后回在上述方法調用 AMS 的 inputDispatchingTimedOut() 方法繼續處理,并最終在 inputDispatchingTimedOut() 方法中將事件傳遞給 AppErrors。
解決方式:
- 使用 adb 導出 ANR 日志并進行分析,發生 ANR的時候系統會記錄 ANR 的信息并將其存儲到 /data/anr/traces.txt 文件中(在比較新的系統中會被存儲都 /data/anr/anr_* 文件中)。或者在開發者模式中選擇將日志導出到 sdcard 之后再從 sdcard 將日志發送到電腦端進行查看
- 使用 DDMS 的
traceview
進行分析:到 SDK 安裝目錄的 tools 目錄下面使用 monitor.bat 打開 DDMS。使用 TraceView 來通過耗時方法調用的信息定位耗時操作的位置。 - 使用開源項目
ANR-WatchDog
來檢測 ANR:創建一個檢測線程,該線程不斷往 UI 線程 post 一個任務,然后睡眠固定時間,等該線程又一次起來后檢測之前 post 的任務是否運行了,假設任務未被運行,則生成 ANRError,并終止進程。
常見的 ANR 場景:
I/O 阻塞
網絡阻塞
多線程死鎖
由于響應式編程等導致的方法死循環
由于某個業務邏輯執行的時間太長
避免 ANR 的方法:
- UI 線程盡量只做跟 UI 相關的工作;
- 耗時的工作 (比如數據庫操作,I/O,網絡操作等),采用
單獨的工作線程
處理; - 用
Handler
來處理 UI 線程和工作線程的交互; - 使用
RxJava
等來處理異步消息。
4、性能調優工具
5、優化經驗
5.1 優化經驗
雖然一直強調優化,但是許多優化應該是在開發階段就完成的,程序邏輯的設計可能會影響程序的性能。如果開發完畢之后再去考慮對程序的邏輯進行優化,那么阻力會比較大。因此,編程的時候應該養成好的編碼習慣,同時注意收集性能優化的經驗,在開發的時候進行避免。
代碼質量檢查工具:
- 使用
SonarLint
來對代碼進行靜態檢查,使代碼更加符合規范; - 使用
阿里的 IDEA 插件
對 Java 的代碼質量進行檢查;
在 Android4.4 以上的系統上,對于 Bitmap 的解碼,decodeStream() 的效率要高于 decodeFile() 和 decodeResource(), 而且高的不是一點。所以解碼 Bitmap 要使用 decodeStream(),同時傳給 decodeStream() 的文件流是 BufferedInputStream:
val bis = BufferedInputStream(FileInputStream(filePath))
val bitmap = BitmapFactory.decodeStream(bis,null,ops)
Java 相關的優化:
-
靜態優于抽象
:如果你并不需要訪問一個對系那個中的某些字段,只是想調用它的某些方法來去完成一項通用的功能,那么可以將這個方法設置成靜態方法,調用速度提升 15%-20%,同時也不用為了調用這個方法去專門創建對象了,也不用擔心調用這個方法后是否會改變對象的狀態(靜態方法無法訪問非靜態字段)。 -
多使用系統封裝好的 API
:系統提供不了的 Api 完成不了我們需要的功能才應該自己去寫,因為使用系統的 Api 很多時候比我們自己寫的代碼要快得多,它們的很多功能都是通過底層的匯編模式執行的。舉個例子,實現數組拷貝的功能,使用循環的方式來對數組中的每一個元素一一進行賦值當然可行,但是直接使用系統中提供的System.arraycopy()
方法會讓執行效率快 9 倍以上。 -
避免在內部調用 Getters/Setters 方法
:面向對象中封裝的思想是不要把類內部的字段暴露給外部,而是提供特定的方法來允許外部操作相應類的內部字段。但在 Android 中,字段搜尋比方法調用效率高得多,我們直接訪問某個字段可能要比通過 getters 方法來去訪問這個字段快 3 到 7 倍。但是編寫代碼還是要按照面向對象思維的,我們應該在能優化的地方進行優化,比如避免在內部調用 getters/setters 方法。 -
使用 static final 修飾常量
:因為常量會在 dex 文件的初始化器當中進行初始化。當我們調用 intVal 時可以直接指向 42 的值,而調用 strVal 會用一種相對輕量級的字符串常量方式,而不是字段搜尋的方式。這種優化方式只對基本數據類型以及 String 類型的常量有效,對于其他數據類型的常量無效。 -
合理使用數據結構
:比如android.util
下面的Pair<F, S>
,在希望某個方法返回的數據恰好是兩個的時候可以使用。顯然,這種返回方式比返回數組或者列表含義清晰得多。延申一下:有時候合理使用數據結構或者使用自定義數據結構,能夠起到化腐朽為神奇的作用
。 -
多線程
:不要開太多線程,如果小任務很多建議使用線程池或者 AsyncTask,建議直接使用 RxJava 來實現多線程,可讀性和性能更好。 -
合理選擇數據結構
:根據具體應用場景選擇 LinkedList 和 ArrayList,比如 Adapter 中查找比增刪要多,因此建議選擇 ArrayList. -
合理設置 buffer
:在讀一個文件我們一般會設置一個 buffer。即先把文件讀到 buffer 中,然后再讀取 buffer 的數據。所以: 真正對文件的次數 = 文件大小 / buffer大小 。 所以如果你的 buffer 比較小的話,那么讀取文件的次數會非常多。當然在寫文件時 buffer 是一樣道理的。很多同學會喜歡設置 1KB 的 buffer,比如 byte buffer[] = new byte[1024]。如果要讀取的文件有 20KB, 那么根據這個 buffer 的大小,這個文件要被讀取 20 次才能讀完。 -
ListView 復用,
getView()
里盡量復用 conertView,同時因為getView()
會頻繁調用,要避免頻繁地生成對象。 -
謹慎使用多進程
,現在很多App都不是單進程,為了保活,或者提高穩定性都會進行一些進程拆分,而實際上即使是空進程也會占用內存(1M左右),對于使用完的進程,服務都要及時進行回收。 -
盡量使用系統資源,系統組件,圖片甚至控件的 id.
-
數據相關
:序列化數據使用 protobuf 可以比 xml 省 30% 內存,慎用 shareprefercnce,因為對于同一個 sp,會將整個 xml 文件載入內存,有時候為了讀一個配置,就會將幾百 k 的數據讀進內存,數據庫字段盡量精簡,只讀取所需字段。 -
dex優化,代碼優化,謹慎使用外部庫
,有人覺得代碼多少于內存沒有關系,實際會有那么點關系,現在稍微大一點的項目動輒就是百萬行代碼以上,多 dex 也是常態,不僅占用 rom 空間,實際上運行的時候需要加載 dex 也是會占用內存的(幾 M ),有時候為了使用一些庫里的某個功能函數就引入了整個龐大的庫,此時可以考慮抽取必要部分,開啟 proguard 優化代碼,使用 Facebook redex 使用優化 dex (好像有不少坑)。
常用的程序性能測試方法
-
時間測試
:方式很簡單只要在代碼的上面和下面定義一個long型的變量,并賦值給當前的毫秒數即可。比如long sMillis = System.currentTimeMillis(); // ...代碼塊 long eMillis = System.currentTimeMillis();
然后兩者相減即可得到程序的運行時間。
-
內存消耗測試
:獲取代碼塊前后的內存,然后相減即可得到這段代碼當中的內存消耗。獲取當前內存的方式是long total = Runtime.getRuntime().totalMemory(); // 獲取系統中內存總數 long free = Runtime.getRuntime().freeMemory(); // 獲取剩余的內存總數 long used = total - free; // 使用的內存數
在使用的時候只要在代碼塊的兩端調用 Runtime.getRuntime().freeMemory()
然后再相減即可得到使用的內存總數。
5.2 布局優化
- 在選擇使用 Android 中的布局方式的時候應該遵循:盡量少使用性能比較低的容器控件,比如 RelativeLayout,但如果使用 RelativeLayout 可以降低布局的層次的時候可以考慮使用。
- 使用
<include>
標簽復用布局:多個地方共用的布局可以使用<include>
標簽在各個布局中復用; - 可以通過使用
<merge>
來降低布局的層次。<merge>
標簽通常與<include>
標簽一起使用,<merge>
作為可以復用的布局的根控件。然后使用<include>
標簽引用該布局。 - 使用
<ViewStub>
標簽動態加載布局:<ViewStub>
標簽可以用來在程序運行的時候決定加載哪個布局,而不是一次性全部加載。 - 性能分析:使用
Android Lint
來分析布局; - 性能分析:避免過度繪制,在手機的開發者選項中的繪圖選項中選擇顯示布局邊界來查看布局
- 性能分析:
Hierarchy View
,可以通過 Hierarchy View 來獲取當前的 View 的層次圖 - 使用
ConstaintLayout
:用來降低布局層次; - 性能分析:使用
systrace
分析 UI 性能; - onDraw() 方法會被頻繁調用,因此不應該在其中做耗時邏輯和聲明對象
5.3 內存優化
-
防止內存泄漏
:見內存泄漏; -
使用優化過的集合
; -
使用優化過的數據集合
:如SparseArray
、SparseBooleanArray
等來替換 HashMap。因為 HashMap 的鍵必須是對象,而對象比數值類型需要多占用非常多的空間。 -
少使用枚舉
:枚舉可以合理組織數據結構,但是枚舉是對象,比普通的數值類型需要多使用很多空間。 -
當內存緊張時釋放內存
:onTrimMemory()
方法還有很多種其他類型的回調,可以在手機內存降低的時候及時通知我們,我們應該根據回調中傳入的級別來去決定如何釋放應用程序的資源。 -
讀取一個 Bitmap 圖片的時候,不要去加載不需要的分辨率。可以壓縮圖片等操作,使用性能穩定的圖片加載框架,比如 Glide.
-
謹慎使用抽象編程
:在 Android 使用抽象編程會帶來額外的內存開支,因為抽象的編程方法需要編寫額外的代碼,雖然這些代碼根本執行不到,但是也要映射到內存中,不僅占用了更多的內存,在執行效率上也會有所降低。所以需要合理的使用抽象編程。 -
盡量避免使用依賴注入框架
:使用依賴注入框架貌似看上去把 findViewById() 這一類的繁瑣操作去掉了,但是這些框架為了要搜尋代碼中的注解,通常都需要經歷較長的初始化過程,并且將一些你用不到的對象也一并加載到內存中。這些用不到的對象會一直站用著內存空間,可能很久之后才會得到釋放,所以可能多敲幾行代碼是更好的選擇。 -
使用多個進程
:謹慎使用,多數應用程序不該在多個進程中運行的,一旦使用不當,它甚至會增加額外的內存而不是幫我們節省內存。這個技巧比較適用于哪些需要在后臺去完成一項獨立的任務,和前臺是完全可以區分開的場景。比如音樂播放,關閉軟件,已經完全由 Service 來控制音樂播放了,系統仍然會將許多 UI 方面的內存進行保留。在這種場景下就非常適合使用兩個進程,一個用于 UI 展示,另一個用于在后臺持續的播放音樂。關于實現多進程,只需要在 Manifast 文件的應用程序組件聲明一個android:process
屬性就可以了。進程名可以自定義,但是之前要加個冒號,表示該進程是一個當前應用程序的私有進程。 -
分析內存的使用情況
:系統不可能將所有的內存都分配給我們的應用程序,每個程序都會有可使用的內存上限,被稱為堆大小。不同的手機堆大小不同,如下代碼可以獲得堆大小int heapSize = AMS.getMemoryClass()
結果以 MB 為單位進行返回,我們開發時應用程序的內存不能超過這個限制,否則會出現 OOM。 -
節制的使用 Service
:如果應用程序需要使用 Service 來執行后臺任務的話,只有當任務正在執行的時候才應該讓 Service 運行起來。當啟動一個 Service 時,系統會傾向于將這個 Service 所依賴的進程進行保留,系統可以在 LRUcache 當中緩存的進程數量也會減少,導致切換程序的時候耗費更多性能。我們可以使用 IntentService,當后臺任務執行結束后會自動停止,避免了 Service 的內存泄漏。 -
字符串優化:Android 性能優化之String篇
5.4 異常崩潰 & 穩定性
問題:如何保持應用的穩定性
問題:App 啟動崩潰異常捕捉
- 使用熱補丁
- 自己寫代碼捕獲異常
- 使用異常收集工具
- 開發就是測試,自己的邏輯自己先測一邊
5.5 優化工具
問題:性能優化如何分析 systrace?
下面將簡單介紹幾個主流的輔助分析內存優化的工具,分別是
- MAT (Memory Analysis Tools)
- Heap Viewer
- Allocation Tracker
- Android Studio 的 Memory Monitor
- LeakCanary
https://www.jianshu.com/p/0df5ad0d2e6a
MAT (Memory Analysis Tools),作用:查看當前內存占用情況。通過分析 Java 進程的內存快照 HPROF 分析,快速計算出在內存中對象占用的大小,查看哪些對象不能被垃圾收集器回收 & 可通過視圖直觀地查看可能造成這種結果的對象
- MAT - Memory Analyzer Tool 使用進階
- MAT使用教程
Heap Viewer,定義:一個的 Java Heap 內存分析工具。作用:查看當前內存快照。可查看分別有哪些類型的數據在堆內存總以及各種類型數據的占比情況。
5.6 啟動優化
問題:能優化,怎么保證應用啟動不卡頓
問題:統計啟動時長,標準
- 方式 1:使用 ADB:獲取啟動速度的第一種方式是使用 ADB,使用下面的指令的時候在啟動應用的時候會使用 AMS 進行統計。但是缺點是統計時間不夠準確:
adb shell am start -n {包名}/{包名}.{活動名}
- 方式 2:代碼埋點:在 Application 的 attachBaseContext() 方法中記錄開始時間,第一個 Activity 的 onWindowFocusChanged() 中記錄結束時間。缺點是統計不完全,因為在 attachBaseContext() 之前還有許多操作。
- 方式 3:TraceView:在 AS 中打開 DDMS,或者到 SDK 安裝目錄的 tools 目錄下面使用 monitor.bat 打開 DDMS。通過 TraceView 主要可以得到兩種數據:單次執行耗時的方法以及執行次數多的方法。但 TraceView 性能耗損太大,不能比較正確反映真實情況。
- 方式 4:Systrace:Systrace 能夠追蹤關鍵系統調用的耗時情況,如系統的 IO 操作、內核工作隊列、CPU 負載、Surface 渲染、GC 事件以及 Android 各個子系統的運行狀況等。但是不支持應用程序代碼的耗時分析。
- 方式 5:Systrace + 插樁:類似于 AOP,通過切面為每個函數統計執行時間。這種方式的好處是能夠準確統計各個方法的耗時。
TraceMethod.i(); /* do something*/ TraceMethod.o();
- 方式 6:錄屏:錄屏方式收集到的時間,更接近于用戶的真實體感。可以在錄屏之后按幀來進行統計分析。
啟動優化
延遲初始化
:一些邏輯,如果沒必要在程序啟動的時候就立即初始化,那么可以將其推遲到需要的時候再初始化。比如,我們可以使用單例的方式來獲取類的實例,然后在獲取實例的時候再進行初始化操作。但是需要注意的是,懶加載要防止集中化,否則容易出現首頁顯示后用戶無法操作的情形。可以按照耗時和是否必要將業務劃分到四個維度:必要且耗時,必要不耗時,非必要但耗時,非必要不耗時。
然后對應不同的維度來決定是否有必要在程序啟動的時候立即初始化。防止主線程阻塞
:一般我們也不會把耗時操作放在主線程里面,畢竟現在有了 RxJava 之后,在程序中使用異步代價并不高。這種耗時操作包括,大量的計算、IO、數據庫查詢和網絡訪問等。另外,關于開啟線程池的問題下面的話總結得比較好,除了一般意義上線程池和使用普通線程的區別,還要考慮應用啟動這個時刻的特殊性,特定場景下單個時間點的表現 Thread 會比 ThreadPoolExecutor 好:同樣的創建對象,ThreadPoolExecutor 的開銷明顯比 Thread 大。布局優化
:如,之前我在使用 Fragment 和 ViewPager 搭配的時候,發現雖然 Fragment 可以被復用,但是如果通過 Adapter 為 ViewPager 的每個項目指定了標題,那么這些標題控件不會被復用。當 ViewPager 的條目比較多的時候,甚至會造成 ANR.使用啟動頁面防止白屏
:這種方法只是治標不治本的方法,就是在應用啟動的時候避免白屏,可以通過設置自定義主題來實現。
其他借鑒辦法
- 使用 BlockCanary 檢測卡頓:它的原理是對 Looper 中的 loop() 方法打處的日志進行處理,通過一個自定義的日志輸出 Printer 監聽方法執行的開始和結束。(更加詳細的源碼分析參考這篇文章:Android UI卡頓監測框架BlockCanary原理分析)
- GC 優化:減少垃圾回收的時間間隔,所以在啟動的過程中不要頻繁創建對象,特別是大對象,避免進行大量的字符串操作,特別是序列化跟反序列化過程。一些頻繁創建的對象,例如網絡庫和圖片庫中的 Byte 數組、Buffer 可以復用。如果一些模塊實在需要頻繁創建對象,可以考慮移到 Native 實現。
- 類重排:如果我們的代碼在打包的時候被放進了不同的 dex 里面,當啟動的時候,如果需要用到的類分散在各個 dex 里面,那么系統要花額外的時間到各個 dex 里加載類。因此,我們可以通過類重排調整類在 Dex 中的排列順序,把啟動時用到的類放進主 dex 里。目前可以使用 ReDex 的 Interdex 調整類在 Dex 中的排列順序。
- 資源文件重排:這種方案的原理時先通過測試找出程序啟動過程中需要加載的資源,然后再打包的時候通過修改 7z 壓縮工具將上述熱點資源放在一起。這樣,在系統進行資源加載的時候,這些資源將要用到的資源會一起被加載進程內存當中并緩存,減少了 IO 的次數,同時不需要從磁盤讀取文件,來提高應用啟動的速度。
5.7 網絡優化
- Network Monitor: Android Studio 內置的 Monitor工具中就有一個 Network Monitor;
- 抓包工具:Wireshark, Fiddler, Charlesr 等抓包工具,Android 上面的無 root 抓包工具;
- Stetho:Android 應用的調試工具。無需 Root 即可通過 Chrome,在 Chrome Developer Tools 中可視化查看應用布局,網絡請求,SQLite,preference 等。
- Gzip 壓縮:使用 Gzip 來壓縮 request 和 response, 減少傳輸數據量, 從而減少流量消耗.
- 數據交換格式:JSON 而不是 XML,另外 Protocol Buffer 是 Google 推出的一種數據交換格式.
- 圖片的 Size:使用 WebP 圖片,修改圖片大小;
- 弱網優化
- 界面先反饋, 請求延遲提交例如, 用戶點贊操作, 可以直接給出界面的點贊成功的反饋, 使用JobScheduler在網絡情況較好的時候打包請求.
- 利用緩存減少網絡傳輸;
- 針對弱網(移動網絡), 不自動加載圖片
- 比方說 Splash 閃屏廣告圖片, 我們可以在連接到 Wifi 時下載緩存到本地; 新聞類的 App 可以在充電, Wifi 狀態下做離線緩存
- IP 直連與 HttpDns:DNS 解析的失敗率占聯網失敗中很大一種,而且首次域名解析一般需要幾百毫秒。針對此,我們可以不用域名,才用 IP 直連省去 DNS 解析過程,節省這部分時間。HttpDNS 基于 Http 協議的域名解析,替代了基于 DNS 協議向運營商 Local DNS 發起解析請求的傳統方式,可以避免 Local DNS 造成的域名劫持和跨網訪問問題,解決域名解析異常帶來的困擾。
- 請求頻率優化:可以通過把網絡數據保存在本地來實現這個需求,緩存數據,并且把發出的請求添加到隊列中,當網絡恢復的時候再及時發出。
- 緩存:App 應該緩存從網絡上獲取的內容,在發起持續的請求之前,app 應該先顯示本地的緩存數據。這確保了 app 不管設備有沒有網絡連接或者是很慢或者是不可靠的網絡,都能夠為用戶提供服務。
5.8 電量優化
5.9 RV 優化
- 數據處理和視圖加載分離:從遠端拉取數據肯定是要放在異步的,在我們拉取下來數據之后可能就匆匆把數據丟給了 VH 處理,其實,數據的處理邏輯我們也應該放在異步處理,這樣 Adapter 在 notify change 后,ViewHolder 就可以簡單無壓力地做數據與視圖的綁定邏輯。比如:
mTextView.setText(Html.fromHtml(data).toString());
這里的 Html.fromHtml(data) 方法可能就是比較耗時的,存在多個 TextView 的話耗時會更為嚴重,而如果把這一步與網絡異步線程放在一起,站在用戶角度,最多就是網絡刷新時間稍長一點。 - 數據優化:頁拉取遠端數據,對拉取下來的遠端數據進行緩存,提升二次加載速度;對于新增或者刪除數據通過 DiffUtil 來進行局部刷新數據,而不是一味地全局刷新數據。
- 減少過渡繪制:減少布局層級,可以考慮使用自定義 View 來減少層級,或者更合理地設置布局來減少層級,不推薦在 RecyclerView 中使用 ConstraintLayout,有很多開發者已經反映了使用它效果更差。
- 減少 xml 文件 inflate 時間:xml 文件 inflate 出 ItemView 是通過耗時的 IO 操作,尤其當 Item 的復用幾率很低的情況下,隨著 Type 的增多,這種 inflate 帶來的損耗是相當大的,此時我們可以用代碼去生成布局,即 new View() 的方式。
- 如果 Item 高度是固定的話,可以使用 RecyclerView.setHasFixedSize(true); 來避免 requestLayout 浪費資源;
- 如果不要求動畫,可以通過
((SimpleItemAnimator) rv.getItemAnimator()).setSupportsChangeAnimations(false);
把默認動畫關閉來提升效率。 - 對 TextView 使用 String.toUpperCase 來替代
android:textAllCaps="true"
; - 通過重寫 RecyclerView.onViewRecycled(holder) 來回收資源。
- 通過 RecycleView.setItemViewCacheSize(size); 來加大 RecyclerView 的緩存,用空間換時間來提高滾動的流暢性。
- 如果多個 RecycledView 的 Adapter 是一樣的,比如嵌套的 RecyclerView 中存在一樣的 Adapter,可以通過設置 RecyclerView.setRecycledViewPool(pool); 來共用一個 RecycledViewPool。
- 對 ItemView 設置監聽器,不要對每個 Item 都調用 addXxListener,應該大家公用一個 XxListener,根據 ID 來進行不同的操作,優化了對象的頻繁創建帶來的資源消耗。
6.0 APK 優化
- 開啟混淆:哪些配置?
- 資源混淆:AndRes
- 只支持 armeabi-v7 架構的 so 庫
- 手動 Lint 檢查,手動刪除無用資源:刪除沒有必要的資源文件
- 使用 Tnypng 等圖片壓縮工具對圖片進行壓縮
- 大部分圖片使用 Webp 格式代替:可以給UI提要求,讓他們將圖片資源設置為 Webp 格式,這樣的話圖片資源會小很多。如果對圖片顏色通道要求不高,可以考慮轉 jpg,最好用 webp,因為效果更佳。
- 盡量不要在項目中使用幀動畫
- 使用 gradle 開啟
shrinkResources ture
:但有一個問題,就是圖片 id 沒有被引用的時候會被變成一個像素,所以需要在項目代碼中引用所有表情圖片的 id。 - 減小 dex 的大小:
- 盡量減少第三方庫的引用
- 避免使用枚舉
- 避免重復功能的第三方庫
- 其他
- 用 7zip 代替壓縮資源。
- 刪除翻譯資源,只保留中英文
- 嘗試將
andorid support
庫徹底踢出你的項目。 - 嘗試使用動態加載 so 庫文件,插件化開發。
- 將大資源文件放到服務端,啟動后自動下載使用。
6、相機優化
參考相機優化相關的內容。
Bitmap 優化
https://blog.csdn.net/carson_ho/article/details/79549382
-
使用完畢后 釋放圖片資源,優化方案:
- 在 Android2.3.3(API 10)前,調用 Bitmap.recycle()方法
- 在 Android2.3.3(API 10)后,采用軟引用(SoftReference)
-
根據分辨率適配 & 縮放圖片
-
按需 選擇合適的解碼方式
-
設置 圖片緩存
另外
有什么技術問題歡迎加我交流 qilebeaf
本人10多年大廠軟件開發經驗,精通Android,Java,Python,前端等開發,空余時間承接軟件開發設計、課程設計指導、解決疑難bug、AI大模型搭建,AI繪圖應用等。
歡迎砸單