Android&Java面試題大全—金九銀十面試必備
宣告本文由作者:Man不經心授權轉載,轉載請聯絡原文作者 原文連結:https://www.jianshu.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原始碼分析:https://www.jianshu.com/p/b3486441d7df
閱讀更多
程式設計師如何寫一份更好的簡歷? 你這樣介紹專案,輕鬆搞定面試官|offer加分必備 NDK專案實戰—高仿360手機助手之解除安裝監聽 (Android)面試題級答案(精選版) 裸辭後,從Android轉戰Web前端的學習以及求職之路