聲明本文由作者:Man不經心授權轉載,轉載請聯系原文作者
原文鏈接:http://www.lxweimin.com/p/375ad14096b3,
類加載過程
Java 中類加載分為 3 個步驟:加載、鏈接、初始化。
加載。 加載是將字節碼數據從不同的數據源讀取到JVM內存,并映射為 JVM 認可的數據結構,也就是 Class 對象的過程。數據源可以是 Jar 文件、Class 文件等等。如果數據的格式并不是 ClassFile 的結構,則會報 ClassFormatError。
鏈接。 鏈接是類加載的核心部分,這一步分為 3 個步驟:驗證、準備、解析。
驗證。 驗證是保證JVM安全的重要步驟。JVM需要校驗字節信息是否符合規范,避免惡意信息和不規范數據危害JVM運行安全。如果驗證出錯,則會報VerifyError。
準備。 這一步會創建靜態變量,并為靜態變量開辟內存空間。
解析。 這一步會將符號引用替換為直接引用。初始化。 初始化會為靜態變量賦值,并執行靜態代碼塊中的邏輯。
雙親委派模型
類加載器大致分為3類:啟動類加載器、擴展類加載器、應用程序類加載器。
- 啟動類加載器主要加載 jre/lib下的jar文件。
- 擴展類加載器主要加載 jre/lib/ext 下的jar文件。
- 應用程序類加載器主要加載 classpath 下的文件。
所謂的雙親委派模型就是當加載一個類時,會優先使用父類加載器加載,當父類加載器無法加載時才會使用子類加載器去加載。這么做的目的是為了避免類的重復加載。
Java 中的集合類
HashMap 的原理
HashMap 的內部可以看做數組+鏈表的復合結構。數組被分為一個個的桶(bucket)。哈希值決定了鍵值對在數組中的尋址。具有相同哈希值的鍵值對會組成鏈表。需要注意的是當鏈表長度超過閾值(默認是8)的時候會觸發樹化,鏈表會變成樹形結構。
把握HashMap的原理需要關注4個方法:hash、put、get、resize。
hash方法。 將 key 的 hashCode 值的高位數據移位到低位進行異或運算。這么做的原因是有些 key 的 hashCode 值的差異集中在高位,而哈希尋址是忽略容量以上高位的,這種做法可以有效避免哈希沖突。
-
put 方法。 put 方法主要有以下幾個步驟:
- 通過 hash 方法獲取 hash 值,根據 hash 值尋址。
- 如果未發生碰撞,直接放到桶中。
- 如果發生碰撞,則以鏈表形式放在桶后。
- 當鏈表長度大于閾值后會觸發樹化,將鏈表轉換為紅黑樹。
- 如果數組長度達到閾值,會調用 resize 方法擴展容量。
-
get方法。 get 方法主要有以下幾個步驟:
- 通過 hash 方法獲取 hash 值,根據 hash 值尋址。
- 如果與尋址到桶的 key 相等,直接返回對應的 value。
- 如果發生沖突,分兩種情況。如果是樹,則調用 getTreeNode 獲取 value;如果是鏈表則通過循環遍歷查找對應的 value。
-
resize 方法。 resize 做了兩件事:
- 將原數組擴展為原來的 2 倍
- 重新計算 index 索引值,將原節點重新放到新的數組中。這一步可以將原先沖突的節點分散到新的桶中。
sleep 和 wait 的區別
- sleep 方法是 Thread 類中的靜態方法,wait 是 Object 類中的方法
- sleep 并不會釋放同步鎖,而 wait 會釋放同步鎖
- sleep 可以在任何地方使用,而 wait 只能在同步方法或者同步代碼塊中使用
- sleep 中必須傳入時間,而 wait 可以傳,也可以不傳,不傳時間的話只有 notify 或者 notifyAll - 才能喚醒,傳時間的話在時間之后會自動喚醒
volatile和synchronize的區別
final、finally、finalize區別
- final 可以修飾類、變量和方法。修飾類代表這個類不可被繼承。修飾變量代表此變量不可被改變。修飾方法表示此方法不可被重寫 (override)。
- finally 是保證重點代碼一定會執行的一種機制。通常是使用 try-finally 或者 try-catch-finally 來進行文件流的關閉等操作。
- finalize 是 Object 類中的一個方法,它的設計目的是保證對象在垃圾收集前完成特定資源的回收。finalize 機制現在已經不推薦使用,并且在 JDK 9已經被標記為 deprecated。
Java中引用類型的區別,具體的使用場景
Java中引用類型分為四類:強引用、軟引用、弱引用、虛引用。
- 強引用: 強引用指的是通過 new 對象創建的引用,垃圾回收器即使是內存不足也不會回收強引用指向的對象。
- 軟引用: 軟引用是通過 SoftRefrence 實現的,它的生命周期比強引用短,在內存不足,拋出 OOM 之前,垃圾回收器會回收軟引用引用的對象。軟引用常見的使用場景是存儲一些內存敏感的緩存,當內存不足時會被回收。
- 弱引用: 弱引用是通過 WeakRefrence 實現的,它的生命周期比軟引用還短,GC 只要掃描到弱引用的對象就會回收。弱引用常見的使用場景也是存儲一些內存敏感的緩存。
- 虛引用: 虛引用是通過 FanttomRefrence 實現的,它的生命周期最短,隨時可能被回收。如果一個對象只被虛引用引用,我們無法通過虛引用來訪問這個對象的任何屬性和方法。它的作用僅僅是保證對象在 finalize 后,做某些事情。虛引用常見的使用場景是跟蹤對象被垃圾回收的活動,當一個虛引用關聯的對象被垃圾回收器回收之前會收到一條系統通知。
Exception 和 Error的區別
- Exception 和 Error 都繼承于 Throwable,在 Java 中,只有 Throwable 類型的對象才能被 throw 或者 catch,它是異常處理機制的基本組成類型.
- Exception 和 Error 體現了 Java 對不同異常情況的分類。Exception 是程序正常運行中,可以預料的意外情況,可能并且應該被捕獲,進行相應的處理。
- Error 是指在正常情況下,不大可能出現的情況,絕大部分 Error 都會使程序處于非正常、不可恢復的狀態。既然是非正常,所以不便于也不需要捕獲,常見的 OutOfMemoryError 就是 Error 的子類。
- Exception 又分為 checked Exception 和 unchecked Exception。
- checked Exception 在代碼里必須顯式的進行捕獲,這是編譯器檢查的一部分。
- unchecked Exception 也就是運行時異常,類似空指針異常、數組越界等,通常是可以避免的邏輯錯誤,具體根據需求來判斷是否需要捕獲,并不會在編譯器強制要求。
--------------------網絡相關面試題-------------------
http 與 https 的區別?https 是如何工作的?
http 是超文本傳輸協議,而 https 可以簡單理解為安全的 http 協議。https 通過在 http 協議下添加了一層 ssl 協議對數據進行加密從而保證了安全。https 的作用主要有兩點:建立安全的信息傳輸通道,保證數據傳輸安全;確認網站的真實性。
http 與 https 的區別主要如下:
- https 需要到 CA 申請證書,很少免費,因而需要一定的費用
- http 是明文傳輸,安全性低;而 https 在 http 的基礎上通過 ssl 加密,安全性高
- 二者的默認端口不一樣,http 使用的默認端口是80;https使用的默認端口是 443
https 的工作流程
提到 https 的話首先要說到加密算法,加密算法分為兩類:對稱加密和非對稱加密。
- 對稱加密: 加密和解密用的都是相同的秘鑰,優點是速度快,缺點是安全性低。常見的對稱加密算法有 DES、AES 等等。
- 非對稱加密: 非對稱加密有一個秘鑰對,分為公鑰和私鑰。一般來說,私鑰自己持有,公鑰可以公開給對方,優點是安全性比對稱加密高,缺點是數據傳輸效率比對稱加密低。采用公鑰加密的信息只有對應的私鑰可以解密。常見的非對稱加密包括RSA等。
在正式的使用場景中一般都是對稱加密和非對稱加密結合使用,使用非對稱加密完成秘鑰的傳遞,然后使用對稱秘鑰進行數據加密和解密。二者結合既保證了安全性,又提高了數據傳輸效率。
https 的具體流程如下:
1.客戶端(通常是瀏覽器)先向服務器發出加密通信的請求
- 支持的協議版本,比如 TLS 1.0版
- 一個客戶端生成的隨機數 random1,稍后用于生成"對話密鑰"
- 支持的加密方法,比如 RSA 公鑰加密
- 支持的壓縮方法
2.服務器收到請求,然后響應 - 確認使用的加密通信協議版本,比如 TLS 1.0版本。如果瀏覽器與服務器支持的版本不一致,服務器關閉加密通信
- 一個服務器生成的隨機數 random2,稍后用于生成"對話密鑰"
- 確認使用的加密方法,比如 RSA 公鑰加密
- 服務器證書
3.客戶端收到證書之后會首先會進行驗證 - 首先驗證證書的安全性
- 驗證通過之后,客戶端會生成一個隨機數 pre-master secret,然后使用證書中的公鑰進行加密,然后傳遞給服務器端
4.服務器收到使用公鑰加密的內容,在服務器端使用私鑰解密之后獲得隨機數 pre-master secret,然后根據 radom1、radom2、pre-master secret 通過一定的算法得出一個對稱加密的秘鑰,作為后面交互過程中使用對稱秘鑰。同時客戶端也會使用 radom1、radom2、pre-master secret,和同樣的算法生成對稱秘鑰。
5.然后再后續的交互中就使用上一步生成的對稱秘鑰對傳輸的內容進行加密和解密。
TCP三次握手流程
-----------------Android面試題-------------------
進程間通信的方式有哪幾種
AIDL 、廣播、文件、socket、管道
廣播靜態注冊和動態注冊的區別
- 動態注冊廣播不是常駐型廣播,也就是說廣播跟隨 Activity 的生命周期。注意在 Activity 結束前,移除廣播接收器。 靜態注冊是常駐型,也就是說當應用程序關閉后,如果有信息廣播來,程序也會被系統調用自動運行。
- 當廣播為有序廣播時:優先級高的先接收(不分靜態和動態)。同優先級的廣播接收器,動態優先于靜態
- 同優先級的同類廣播接收器,靜態:先掃描的優先于后掃描的,動態:先注冊的優先于后注冊的。
- 當廣播為默認廣播時:無視優先級,動態廣播接收器優先于靜態廣播接收器。同優先級的同類廣播接收器,靜態:先掃描的優先于后掃描的,動態:先注冊的優先于后冊的。
Android 性能優化工具使用(這個問題建議配合Android中的性能優化)
- Android 中常用的性能優化工具包括這些:Android Studio 自帶的 Android Profiler、LeakCanary、BlockCanary
- Android 自帶的 Android Profiler 其實就很好用,Android Profiler 可以檢測三個方面的性能問題:CPU、MEMORY、NETWORK。
- LeakCanary 是一個第三方的檢測內存泄漏的庫,我們的項目集成之后 LeakCanary 會自動檢測應用運行期間的內存泄漏,并將之輸出給我們。
- BlockCanary 也是一個第三方檢測UI卡頓的庫,項目集成后Block也會自動檢測應用運行期間的UI卡頓,并將之輸出給我們。
Android中的類加載器
PathClassLoader,只能加載系統中已經安裝過的 apk
DexClassLoader,可以加載 jar/apk/dex,可以從 SD卡中加載未安裝的 apk
Android中的動畫有哪幾類,它們的特點和區別是什么
Android中動畫大致分為3類:幀動畫、補間動畫(View Animation)、屬性動畫(Object Animation)。
- 幀動畫:通過xml配置一組圖片,動態播放。很少會使用。
- 補間動畫(View Animation):大致分為旋轉、透明、縮放、位移四類操作。很少會使用。
- 屬性動畫(Object Animation):屬性動畫是現在使用的最多的一種動畫,它比補間動畫更加強大。屬性動畫大致分為兩種使用類型,分別是 ViewPropertyAnimator 和 ObjectAnimator。前者適合一些通用的動畫,比如旋轉、位移、縮放和透明,使用方式也很簡單通過 View.animate() 即可得到 ViewPropertyAnimator,之后進行相應的動畫操作即可。后者適合用于為我們的自定義控件添加動畫,當然首先我們應該在自定義 View 中添加相應的 getXXX() 和 setXXX() 相應屬性的 getter 和 setter 方法,這里需要注意的是在 setter 方法內改變了自定義 View 中的屬性后要調用 invalidate() 來刷新View的繪制。之后調用 ObjectAnimator.of 屬性類型()返回一個 ObjectAnimator,調用 start() 方法啟動動畫即可。
補間動畫與屬性動畫的區別:
- 補間動畫是父容器不斷的繪制 view,看起來像移動了效果,其實 view 沒有變化,還在原地。
- 是通過不斷改變 view 內部的屬性值,真正的改變 view。
Handler 機制
說到 Handler,就不得不提與之密切相關的這幾個類:Message、MessageQueue,Looper。
- Message。 Message 中有兩個成員變量值得關注:target 和 callback。
- target 其實就是發送消息的 Handler 對象
- callback 是當調用 handler.post(runnable) 時傳入的 Runnable 類型的任務。post 事件的本質也是創建了一個 Message,將我們傳入的這個 runnable 賦值給創建的Message的 callback 這個成員變量。
- MessageQueue。 消息隊列很明顯是存放消息的隊列,值得關注的是 MessageQueue 中的 next() 方法,它會返回下一個待處理的消息。
- Looper。 Looper 消息輪詢器其實是連接 Handler 和消息隊列的核心。首先我們都知道,如果想要在一個線程中創建一個 Handler,首先要通過 Looper.prepare() 創建 Looper,之后還得調用 Looper.loop()開啟輪詢。我們著重看一下這兩個方法。
- prepare()。 這個方法做了兩件事:首先通過ThreadLocal.get()獲取當前線程中的Looper,如果不為空,則會拋出一個RunTimeException,意思是一個線程不能創建2個Looper。如果為null則執行下一步。第二步是創建了一個Looper,并通過 ThreadLocal.set(looper)。將我們創建的Looper與當前線程綁定。這里需要提一下的是消息隊列的創建其實就發生在Looper的構造方法中。
- loop()。 這個方法開啟了整個事件機制的輪詢。它的本質是開啟了一個死循環,不斷的通過 MessageQueue的next()方法獲取消息。拿到消息后會調用 msg.target.dispatchMessage()來做處理。其實我們在說到 Message 的時候提到過,msg.target 其實就是發送這個消息的 handler。這句代碼的本質就是調用 handler的dispatchMessage()。
- Handler。 上面做了這么多鋪墊,終于到了最重要的部分。Handler 的分析著重在兩個部分:發送消息和處理消息。
- 發送消息。其實發送消息除了 sendMessage 之外還有 sendMessageDelayed 和 post 以及 postDelayed 等等不同的方式。但它們的本質都是調用了 sendMessageAtTime。在 sendMessageAtTime 這個方法中調用了 enqueueMessage。在 enqueueMessage 這個方法中做了兩件事:通過 msg.target = this 實現了消息與當前 handler 的綁定。然后通過 queue.enqueueMessage 實現了消息入隊。
- 處理消息。 消息處理的核心其實就是dispatchMessage()這個方法。這個方法里面的邏輯很簡單,先判斷 msg.callback 是否為 null,如果不為空則執行這個 runnable。如果為空則會執行我們的handleMessage方法。
Android 性能優化
Android 中的性能優化在我看來分為以下幾個方面:內存優化、布局優化、網絡優化、安裝包優化。
- 內存優化: 下一個問題就是。
- 布局優化: 布局優化的本質就是減少 View 的層級。常見的布局優化方案如下
- 在 LinearLayout 和 RelativeLayout 都可以完成布局的情況下優先選擇 RelativeLayout,可以減少 View 的層級
- 將常用的布局組件抽取出來使用 < include >標簽
- 通過 < ViewStub >標簽來加載不常用的布局
- 使用 < Merge >標簽來減少布局的嵌套層次
- 網絡優化: 常見的網絡優化方案如下
- 盡量減少網絡請求,能夠合并的就盡量合并
- 避免 DNS 解析,根據域名查詢可能會耗費上百毫秒的時間,也可能存在DNS劫持的風險。可以根據業務需求采用增加動態更新 IP 的方式,或者在 IP 方式訪問失敗時切換到域名訪問方式。
- 大量數據的加載采用分頁的方式
- 網絡數據傳輸采用 GZIP 壓縮
- 加入網絡數據的緩存,避免頻繁請求網絡
- 上傳圖片時,在必要的時候壓縮圖片
- 安裝包優化: 安裝包優化的核心就是減少 apk 的體積,常見的方案如
- 使用混淆,可以在一定程度上減少 apk 體積,但實際效果微乎其微
- 減少應用中不必要的資源文件,比如圖片,在不影響 APP 效果的情況下盡量壓縮圖片,有一定的效果
- 在使用了 SO 庫的時候優先保留 v7 版本的 SO 庫,刪掉其他版本的SO庫。原因是在 2018 年,v7 版本的 SO 庫可以滿足市面上絕大多數的要求,可能八九年前的手機滿足不了,但我們也沒必要去適配老掉牙的手機。實際開發中減少 apk 體積的效果是十分顯著的,如果你使用了很多 SO 庫,比方說一個版本的SO庫一共 10M,那么只保留 v7 版本,刪掉 armeabi 和 v8 版本的 SO 庫,一共可以減少 20M 的體積。
Android 內存優化
Android的內存優化在我看來分為兩點:避免內存泄漏、擴大內存,其實就是開源節流。
其實內存泄漏的本質就是較長生命周期的對象引用了較短生命周期的對象。
常見的內存泄漏
- 單例模式導致的內存泄漏。 最常見的例子就是創建這個單例對象需要傳入一個 Context,這時候傳入了一個 Activity 類型的 Context,由于單例對象的靜態屬性,導致它的生命周期是從單例類加載到應用程序結束為止,所以即使已經 finish 掉了傳入的 Activity,由于我們的單例對象依然持有 Activity 的引用,所以導致了內存泄漏。解決辦法也很簡單,不要使用 Activity 類型的 Context,使用 Application 類型的 Context 可以避免內存泄漏。
- 靜態變量導致的內存泄漏。 靜態變量是放在方法區中的,它的生命周期是從類加載到程序結束,可以看到靜態變量生命周期是非常久的。最常見的因靜態變量導致內存泄漏的例子是我們在 Activity 中創建了一個靜態變量,而這個靜態變量的創建需要傳入 Activity 的引用 this。在這種情況下即使 Activity 調用了 finish 也會導致內存泄漏。原因就是因為這個靜態變量的生命周期幾乎和整個應用程序的生命周期一致,它一直持有 Activity 的引用,從而導致了內存泄漏。
- 非靜態內部類導致的內存泄漏。 非靜態內部類導致內存泄漏的原因是非靜態內部類持有外部類的引用,最常見的例子就是在 Activity 中使用 Handler 和 Thread 了。使用非靜態內部類創建的 Handler 和 Thread 在執行延時操作的時候會一直持有當前Activity的引用,如果在執行延時操作的時候就結束 Activity,這樣就會導致內存泄漏。解決辦法有兩種:第一種是使用靜態內部類,在靜態內部類中使用弱引用調用Activity。第二種方法是在 Activity 的 onDestroy 中調用 handler.removeCallbacksAndMessages 來取消延時事件。
- 使用資源未及時關閉導致的內存泄漏。 常見的例子有:操作各種數據流未及時關閉,操作 Bitmap 未及時 recycle 等等。
- 使用第三方庫未能及時解綁。 有的三方庫提供了注冊和解綁的功能,最常見的就 EventBus 了,我們都知道使用 EventBus 要在 onCreate 中注冊,在 onDestroy 中解綁。如果沒有解綁的話,EventBus 其實是一個單例模式,他會一直持有 Activity 的引用,導致內存泄漏。同樣常見的還有 RxJava,在使用 Timer 操作符做了一些延時操作后也要注意在 onDestroy 方法中調用 disposable.dispose()來取消操作。
-
屬性動畫導致的內存泄漏。 常見的例子就是在屬性動畫執行的過程中退出了 Activity,這時 View 對象依然持有 Activity 的引用從而導致了內存泄漏。解決辦法就是在 onDestroy 中調用動畫的 cancel 方法取消屬性動畫。
WebView 導致的內存泄漏。WebView 比較特殊,即使是調用了它的 destroy 方法,依然會導致內存泄漏。其實避免WebView導致內存泄漏的最好方法就是讓WebView所在的Activity處于另一個進程中,當這個 Activity 結束時殺死當前 WebView 所處的進程即可,我記得阿里釘釘的 WebView 就是另外開啟的一個進程,應該也是采用這種方法避免內存泄漏。
擴大內存
為什么要擴大我們的內存呢?有時候我們實際開發中不可避免的要使用很多第三方商業的 SDK,這些 SDK 其實有好有壞,大廠的 SDK 可能內存泄漏會少一些,但一些小廠的 SDK 質量也就不太靠譜一些。那應對這種我們無法改變的情況,最好的辦法就是擴大內存。
擴大內存通常有兩種方法:一個是在清單文件中的 Application 下添加largeHeap="true"這個屬性,另一個就是同一個應用開啟多個進程來擴大一個應用的總內存空間。第二種方法其實就很常見了,比方說我使用過個推的 S DK,個推的 Service 其實就是處在另外一個單獨的進程中。
Android 中的內存優化總的來說就是開源和節流,開源就是擴大內存,節流就是避免內存泄漏。
Binder 機制
在Linux中,為了避免一個進程對其他進程的干擾,進程之間是相互獨立的。在一個進程中其實還分為用戶空間和內核空間。這里的隔離分為兩個部分,進程間的隔離和進程內的隔離。
既然進程間存在隔離,那其實也是存在著交互。進程間通信就是 IPC,用戶空間和內核空間的通信就是系統調用。
Linux 為了保證獨立性和安全性,進程之間不能直接相互訪問,Android 是基于 Linux 的,所以也是需要解決進程間通信的問題。
其實 Linux 進程間通信有很多方式,比如管道、socket 等等。為什么 Android 進程間通信采用了Binder而不是 Linux
已有的方式,主要是有這么兩點考慮:性能和安全
- 性能。 在移動設備上對性能要求是比較嚴苛的。Linux傳統的進程間通信比如管道、socket等等進程間通信是需要復制兩次數據,而Binder則只需要一次。所以Binder在性能上是優于傳統進程通信的。
- 安全。 傳統的 Linux 進程通信是不包含通信雙方的身份驗證的,這樣會導致一些安全性問題。而Binder機制自帶身份驗證,從而有效的提高了安全性。
Binder 是基于 CS 架構的,有四個主要組成部分。
- Client。 客戶端進程。
- Server。 服務端進程。
- ServiceManager。 提供注冊、查詢和返回代理服務對象的功能。
- Binder 驅動。 主要負責建立進程間的 Binder 連接,進程間的數據交互等等底層操作。
Binder 機制主要的流程是這樣的:
- 服務端通過Binder驅動在 ServiceManager 中注冊我們的服務。
- 客戶端通過Binder驅動查詢在 ServiceManager 中注冊的服務。
- ServiceManager 通過 inder 驅動返回服務端的代理對象。
- 客戶端拿到服務端的代理對象后即可進行進程間通信。
LruCache的原理
LruCache 的核心原理就是對 LinkedHashMap 的有效利用,它的內部存在一個 LinkedHashMap 成員變量。值得我們關注的有四個方法:構造方法、get、put、trimToSize。
- 構造方法: 在 LruCache 的構造方法中做了兩件事,設置了 maxSize、創建了一個 LinkedHashMap。這里值得注意的是 LruCache 將 LinkedHashMap的accessOrder 設置為了 true,accessOrder 就是遍歷這個LinkedHashMap 的輸出順序。true 代表按照訪問順序輸出,false代表按添加順序輸出,因為通常都是按照添加順序輸出,所以 accessOrder 這個屬性默認是 false,但我們的 LruCache 需要按訪問順序輸出,所以顯式的將 accessOrder 設置為 true。
- get方法: 本質上是調用 LinkedHashMap 的 get 方法,由于我們將 accessOrder 設置為了 true,所以每調用一次get方法,就會將我們訪問的當前元素放置到這個LinkedHashMap的尾部。
- put方法: 本質上也是調用了 LinkedHashMap 的 put 方法,由于 LinkedHashMap 的特性,每調用一次 put 方法,也會將新加入的元素放置到 LinkedHashMap 的尾部。添加之后會調用 trimToSize 方法來保證添加后的內存不超過 maxSize。
- trimToSize方法: trimToSize 方法的內部其實是開啟了一個 while(true)的死循環,不斷的從 LinkedHashMap 的首部刪除元素,直到刪除之后的內存小于 maxSize 之后使用 break 跳出循環。
其實到這里我們可以總結一下,為什么這個算法叫 最近最少使用 算法呢?原理很簡單,我們的每次 put 或者get都可以看做一次訪問,由于 LinkedHashMap 的特性,會將每次訪問到的元素放置到尾部。當我們的內存達到閾值后,會觸發 trimToSize 方法來刪除 LinkedHashMap 首部的元素,直到當前內存小于 maxSize。為什么刪除首部的元素,原因很明顯:我們最近經常訪問的元素都會放置到尾部,那首部的元素肯定就是 最近最少使用 的元素了,因此當內存不足時應當優先刪除這些元素。
設計一個圖片的異步加載框架
設計一個圖片加載框架,肯定要用到圖片加載的三級緩存的思想。三級緩存分為內存緩存、本地緩存和網絡緩存。
內存緩存 :將Bitmap緩存到內存中,運行速度快,但是內存容量小。
本地緩存 :將圖片緩存到文件中,速度較慢,但容量較大。
網絡緩存 :從網絡獲取圖片,速度受網絡影響。
如果我們設計一個圖片加載框架,流程一定是這樣的:
- 拿到圖片url后首先從內存中查找BItmap,如果找到直接加載。
- 內存中沒有找到,會從本地緩存中查找,如果本地緩存可以找到,則直接加載。
- 內存和本地都沒有找到,這時會從網絡下載圖片,下載到后會加載圖片,并且將下載到的圖片放到內存緩存和本地緩存中。
上面是一些基本的概念,如果是具體的代碼實現的話,大概需要這么幾個方面的文件:
- 首先需要確定我們的內存緩存,這里一般用的都是 LruCache。
- 確定本地緩存,通常用的是 DiskLruCache,這里需要注意的是圖片緩存的文件名一般是 url 被 MD5 加密后的字符串,為了避免文件名直接暴露圖片的 url。
- 內存緩存和本地緩存確定之后,需要我們創建一個新的類 MemeryAndDiskCache,當然,名字隨便起,這個類包含了之前提到的 LruCache 和 DiskLruCache。在 MemeryAndDiskCache 這個類中我們定義兩個方法,一個是 getBitmap,另一個是 putBitmap,對應著圖片的獲取和緩存,內部的邏輯也很簡單。getBitmap中按內存、本地的優先級去取 BItmap,putBitmap 中先緩存內存,之后緩存到本地。
- 在緩存策略類確定好之后,我們創建一個 ImageLoader 類,這個類必須包含兩個方法,一個是展示圖片 displayImage(url,imageView),另一個是從網絡獲取圖片downloadImage(url,imageView)。在展示圖片方法中首先要通過 ImageView.setTag(url),將 url 和 imageView 進行綁定,這是為了避免在列表中加載網絡圖片時會由于ImageView的復用導致的圖片錯位的 bug。之后會從 MemeryAndDiskCache 中獲取緩存,如果存在,直接加載;如果不存在,則調用從網絡獲取圖片這個方法。從網絡獲取圖片方法很多,這里我一般都會使用 OkHttp+Retrofit。當從網絡中獲取到圖片之后,首先判斷一下imageView.getTag()與圖片的 url 是否一致,如果一致則加載圖片,如果不一致則不加載圖片,通過這樣的方式避免了列表中異步加載圖片的錯位。同時在獲取到圖片之后會通過 MemeryAndDiskCache 來緩存圖片。
Android中的事件分發機制
在我們的手指觸摸到屏幕的時候,事件其實是通過 Activity -> ViewGroup -> View 這樣的流程到達最后響應我們觸摸事件的 View。
說到事件分發,必不可少的是這幾個方法:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent。接下來就按照Activity -> ViewGroup -> View 的流程來大致說一下事件分發機制。
我們的手指觸摸到屏幕的時候,會觸發一個 Action_Down 類型的事件,當前頁面的 Activity 會首先做出響應,也就是說會走到 Activity 的 dispatchTouchEvent() 方法內。在這個方法內部簡單來說是這么一個邏輯:
- 調用 getWindow.superDispatchTouchEvent()。
- 如果上一步返回 true,直接返回 true;否則就 return 自己的 onTouchEvent()。
這個邏輯很好理解,getWindow().superDispatchTouchEvent() 如果返回 true 代表當前事件已經被處理,無需調用自己的 onTouchEvent;否則代表事件并沒有被處理,需要 Activity 自己處理,也就是調用自己的 onTouchEvent。
getWindow()方法返回了一個 Window 類型的對象,這個我們都知道,在 Android 中,PhoneWindow 是Window 的唯一實現類。所以這句本質上是調用了``PhoneWindow中的superDispatchTouchEvent()。`
而在 PhoneWindow 的這個方法中實際調用了mDecor.superDispatchTouchEvent(event)。這個 mDecor 就是 DecorView,它是 FrameLayout 的一個子類,在 DecorView 中的 superDispatchTouchEvent() 中調用的是 super.dispatchTouchEvent()。到這里就很明顯了,DecorView 是一個 FrameLayout 的子類,FrameLayout 是一個 ViewGroup 的子類,本質上調用的還是 ViewGroup的dispatchTouchEvent()。
分析到這里,我們的事件已經從 Activity 傳遞到了 ViewGroup,接下來我們來分析下 ViewGroup 中的這幾個事件處理方法。
在 ViewGroup 中的 dispatchTouchEvent()中的邏輯大致如下:
- 通過 onInterceptTouchEvent() 判斷當前 ViewGroup 是否攔截事件,默認的 ViewGroup 都是不攔截的;
- 如果攔截,則 return 自己的 onTouchEvent();
- 如果不攔截,則根據 child.dispatchTouchEvent()的返回值判斷。如果返回 true,則 return true;否則 return 自己的 onTouchEvent(),在這里實現了未處理事件的向上傳遞。
通常情況下 ViewGroup 的 onInterceptTouchEvent()都返回 false,也就是不攔截。這里需要注意的是事件序列,比如 Down 事件、Move 事件......Up事件,從 Down 到 Up 是一個完整的事件序列,對應著手指從按下到抬起這一系列的事件,如果 ViewGroup 攔截了 Down 事件,那么后續事件都會交給這個 ViewGroup的onTouchEvent。如果 ViewGroup 攔截的不是 Down 事件,那么會給之前處理這個 Down 事件的 View 發送一個 Action_Cancel 類型的事件,通知子 View 這個后續的事件序列已經被 ViewGroup 接管了,子 View 恢復之前的狀態即可。
這里舉一個常見的例子:在一個 Recyclerview 鐘有很多的 Button,我們首先按下了一個 button,然后滑動一段距離再松開,這時候 Recyclerview 會跟著滑動,并不會觸發這個 button 的點擊事件。這個例子中,當我們按下 button 時,這個 button 接收到了 Action_Down 事件,正常情況下后續的事件序列應該由這個 button處理。但我們滑動了一段距離,這時 Recyclerview 察覺到這是一個滑動操作,攔截了這個事件序列,走了自身的 onTouchEvent()方法,反映在屏幕上就是列表的滑動。而這時 button 仍然處于按下的狀態,所以在攔截的時候需要發送一個 Action_Cancel 來通知 button 恢復之前狀態。
事件分發最終會走到 View 的 dispatchTouchEvent()中。在 View 的 dispatchTouchEvent() 中沒有 onInterceptTouchEvent(),這也很容易理解,View 不是 ViewGroup,不會包含其他子 View,所以也不存在攔截不攔截這一說。忽略一些細節,View 的 dispatchTouchEvent()中直接 return 了自己的 onTouchEvent()。如果 onTouchEvent()返回 true 代表事件被處理,否則未處理的事件會向上傳遞,直到有 View 處理了事件或者一直沒有處理,最終到達了 Activity 的 onTouchEvent() 終止。
這里經常有人問 onTouch 和 onTouchEvent 的區別。首先,這兩個方法都在 View 的 dispatchTouchEvent()中,是這么一個邏輯:
- 如果 touchListener 不為 null,并且這個 View 是 enable 的,而且 onTouch 返回的是 true,滿足這三個條件時會直接 return true,不會走 onTouchEvent()方法。
- 上面只要有一個條件不滿足,就會走到 onTouchEvent()方法中。所以 onTouch 的順序是在 onTouchEvent 之前的。
View的繪制流程
視圖繪制的起點在 ViewRootImpl 類的 performTraversals()方法,在這個方法內其實是按照順序依次調用了 mView.measure()、mView.layout()、mView.draw()
View的繪制流程分為3步:測量、布局、繪制,分別對應3個方法 measure、layout、draw。
-
測量階段。 measure 方法會被父 View 調用,在measure 方法中做一些優化和準備工作后會調用 onMeasure 方法進行實際的自我測量。onMeasure方法在View和ViewGroup做的事情是不一樣的:
- View。 View 中的 onMeasure 方法會計算自己的尺寸并通過 setMeasureDimension 保存。
- ViewGroup。 ViewGroup 中的 onMeasure 方法會調用所有子 iew的measure 方法進行自我測量并保存。然后通過子View的尺寸和位置計算出自己的尺寸并保存。
-
布局階段。 layout 方法會被父View調用,layout 方法會保存父 View 傳進來的尺寸和位置,并調用 onLayout 進行實際的內部布局。onLayout 在 View 和 ViewGroup 中做的事情也是不一樣的:
- View。 因為 View 是沒有子 View 的,所以View的onLayout里面什么都不做。
- ViewGroup。 ViewGroup 中的 onLayout 方法會調用所有子 View 的 layout 方法,把尺寸和位置傳給他們,讓他們完成自我的內部布局。
-
繪制階段。 draw 方法會做一些調度工作,然后會調用 onDraw 方法進行 View 的自我繪制。draw 方法的調度流程大致是這樣的:
- 繪制背景。 對應 drawBackground(Canvas)方法。
- 繪制主體。 對應 onDraw(Canvas)方法。
- 繪制子View。 對應 dispatchDraw(Canvas)方法。
- 繪制滑動相關和前景。 對應 onDrawForeground(Canvas)。
Android與 js 是如何交互的
在 Android 中,Android 與js 的交互分為兩個方面:Android 調用 js 里的方法、js 調用 Android 中的方法。
-
Android調js。 Android 調 js 有兩種方法:
- WebView.loadUrl("javascript:js中的方法名")。 這種方法的優點是很簡潔,缺點是沒有返回值,如果需要拿到js方法的返回值則需要js調用Android中的方法來拿到這個返回值。
- WebView.evaluateJavaScript("javascript:js中的方法名",ValueCallback)。 這種方法比 loadUrl 好的是可以通過 ValueCallback 這個回調拿到 js方法的返回值。缺點是這個方法 Android4.4 才有,兼容性較差。不過放在 2018 年來說,市面上絕大多數 App 都要求最低版本是 4.4 了,所以我認為這個兼容性問題不大。
-
js 調 Android。 js 調 Android有三種方法:
- WebView.addJavascriptInterface()。 這是官方解決 js 調用 Android 方法的方案,需要注意的是要在供 js 調用的 Android 方法上加上 @JavascriptInterface 注解,以避免安全漏洞。這種方案的缺點是 Android4.2 以前會有安全漏洞,不過在 4.2 以后已經修復了。同樣,在 2018 年來說,兼容性問題不大。
- 重寫 WebViewClient的shouldOverrideUrlLoading()方法來攔截url, 拿到 url 后進行解析,如果符合雙方的規定,即可調用 Android 方法。優點是避免了 Android4.2 以前的安全漏洞,缺點也很明顯,無法直接拿到調用 Android 方法的返回值,只能通過 Android 調用 js 方法來獲取返回值。
- 重寫 WebChromClient 的 onJsPrompt() 方法,同前一個方式一樣,拿到 url 之后先進行解析,如果符合雙方規定,即可調用Android方法。最后如果需要返回值,通過 result.confirm("Android方法返回值") 即可將 Android 的返回值返回給 js。方法的優點是沒有漏洞,也沒有兼容性限制,同時還可以方便的獲取 Android 方法的返回值。其實這里需要注意的是在 WebChromeClient 中除 了 onJsPrompt 之外還有 onJsAlert 和 onJsConfirm 方法。那么為什么不選擇另兩個方法呢?原因在于 onJsAlert 是沒有返回值的,而 onJsConfirm 只有 true 和 false 兩個返回值,同時在前端開發中 prompt 方法基本不會被調用,所以才會采用 onJsPrompt。
Activity 啟動過程
SparseArray 原理
SparseArray,通常來講是 Android 中用來替代 HashMap 的一個數據結構。
準確來講,是用來替換key為 Integer 類型,value為Object 類型的HashMap。需要注意的是 SparseArray 僅僅實現了 Cloneable 接口,所以不能用Map來聲明。
從內部結構來講,SparseArray 內部由兩個數組組成,一個是 int[]類型的 mKeys,用來存放所有的鍵;另一個是 Object[]類型的 mValues,用來存放所有的值。
最常見的是拿 SparseArray 跟HashMap 來做對比,由于 SparseArray 內部組成是兩個數組,所以占用內存比 HashMap 要小。我們都知道,增刪改查等操作都首先需要找到相應的鍵值對,而 SparseArray 內部是通過二分查找來尋址的,效率很明顯要低于 HashMap 的常數級別的時間復雜度。提到二分查找,這里還需要提一下的是二分查找的前提是數組已經是排好序的,沒錯,SparseArray 中就是按照key進行升序排列的。
綜合起來來說,SparseArray 所占空間優于 HashMap,而效率低于 HashMap,是典型的時間換空間,適合較小容量的存儲。
從源碼角度來說,我認為需要注意的是 SparseArray的remove()、put()和 gc()方法。
- remove()。 SparseArray 的 remove() 方法并不是直接刪除之后再壓縮數組,而是將要刪除的 value 設置為 DELETE 這個 SparseArray 的靜態屬性,這個 DELETE 其實就是一個 Object 對象,同時會將 SparseArray 中的 mGarbage 這個屬性設置為 true,這個屬性是便于在合適的時候調用自身的 gc()方法壓縮數組來避免浪費空間。這樣可以提高效率,如果將來要添加的key等于刪除的key,那么會將要添加的 value 覆蓋 DELETE。
- gc()。 SparseArray 中的 gc() 方法跟 JVM 的 GC 其實完全沒有任何關系。``gc()` 方法的內部實際上就是一個for循環,將 value 不為 DELETE 的鍵值對往前移動覆蓋value 為DELETE的鍵值對來實現數組的壓縮,同時將 mGarbage 置為 false,避免內存的浪費。
- put()。 put 方法是這么一個邏輯,如果通過二分查找 在 mKeys 數組中找到了 key,那么直接覆蓋 value 即可。如果沒有找到,會拿到與數組中與要添加的 key 最接近的 key 索引,如果這個索引對應的 value 為 DELETE,則直接把新的 value 覆蓋 DELET 即可,在這里可以避免數組元素的移動,從而提高了效率。如果 value 不為 DELETE,會判斷 mGarbage,如果為 true,則會調用 gc()方法壓縮數組,之后會找到合適的索引,將索引之后的鍵值對后移,插入新的鍵值對,這個過程中可能會觸發數組的擴容。
圖片加載如何避免 OOM
我們知道內存中的 Bitmap 大小的計算公式是:長所占像素 * 寬所占像素 * 每個像素所占內存。想避免 OOM 有兩種方法:等比例縮小長寬、減少每個像素所占的內存。
- 等比縮小長寬。我們知道 Bitmap 的創建是通過 BitmapFactory 的工廠方法,decodeFile()、decodeStream()、decodeByteArray()、decodeResource()。這些方法中都有一個 Options 類型的參數,這個 Options 是 BitmapFactory 的內部類,存儲著 BItmap 的一些信息。Options 中有一個屬性:inSampleSize。我們通過修改 inSampleSize 可以縮小圖片的長寬,從而減少 BItma p 所占內存。需要注意的是這個 inSampleSize 大小需要是 2 的冪次方,如果小于 1,代碼會強制讓inSampleSize為1。
- 減少像素所占內存。Options 中有一個屬性 inPreferredConfig,默認是 ARGB_8888,代表每個像素所占尺寸。我們可以通過將之修改為 RGB_565 或者 ARGB_4444 來減少一半內存。
大圖加載
加載高清大圖,比如清明上河圖,首先屏幕是顯示不下的,而且考慮到內存情況,也不可能一次性全部加載到內存。這時候就需要局部加載了,Android中有一個負責局部加載的類:BitmapRegionDecoder。使用方法很簡單,通過BitmapRegionDecoder.newInstance()創建對象,之后調用decodeRegion(Rect rect, BitmapFactory.Options options)即可。第一個參數rect是要顯示的區域,第二個參數是BitmapFactory中的內部類Options。
OkHttp
OkHttp源碼分析:帶你一起探究OKhttp源碼,面對Http源碼分析不在畏懼
Retrofit
Retrofit源碼分析:帶你一起探究Retrofit 源碼,讓你不再畏懼Retrofit的面試提問
RxJava:
RxJava源碼分析:帶你一起探究Rxjava源碼,學會Rxjava竟如此簡單
Glide
Glide源碼分析:Glide高級詳解—緩存與解碼復用
EventBus
EventBus源碼分析:http://www.lxweimin.com/p/b3486441d7df