垃圾回收須要完成兩件事:找到垃圾,回收垃圾。 找到垃圾通常的話有兩種方法:javascript
引用計數法: 當一個對象被引用時,它的引用計數器會加一,垃圾回收時會清理掉引用計數爲0的對象。但這種方法有一個問題,比方說有兩個對象 A 和 B,A 引用了 B,B 又引用了 A,除此以外沒有別的對象引用 A 和 B,那麼 A 和 B 在咱們看來已是垃圾對象,須要被回收,但它們的引用計數不爲 0,沒有達到回收的條件。正由於這個循環引用的問題,Java 並無採用引用計數法。前端
可達性分析法: 咱們把 Java 中對象引用的關係看作一張圖,從根級對象不可達的對象會被垃圾收集器清除。根級對象通常包括 Java 虛擬機棧中的對象、本地方法棧中的對象、方法區中的靜態對象和常量池中的常量。 回收垃圾的話有這麼四種方法:java
標記清除算法: 顧名思義分爲兩步,標記和清除。首先標記到須要回收的垃圾對象,而後回收掉這些垃圾對象。標記清除算法的缺點是清除垃圾對象後會形成內存的碎片化。git
複製算法: 複製算法是將存活的對象複製到另外一塊內存區域中,並作相應的內存整理工做。複製算法的優勢是能夠避免內存碎片化,缺點也顯而易見,它須要兩倍的內存。github
標記整理算法: 標記整理算法也是分兩步,先標記後整理。它會標記須要回收的垃圾對象,清除掉垃圾對象後會將存活的對象壓縮,避免了內存的碎片化。面試
分代算法: 分代算法將對象分爲新生代和老年代對象。那麼爲何作這樣的區分呢?主要是在Java運行中會產生大量對象,這些對象的生命週期會有很大的不一樣,有的生命週期很長,有的甚至使用一次以後就再也不使用。因此針對不一樣生命週期的對象採用不一樣的回收策略,這樣能夠提升GC的效率。算法
新生代對象分爲三個區域:Eden 區和兩個 Survivor 區。新建立的對象都放在 Eden區,當 Eden 區的內存達到閾值以後會觸發 Minor GC,這時會將存活的對象複製到一個 Survivor 區中,這些存活對象的生命存活計數會加一。這時 Eden 區會閒置,當再一次達到閾值觸發 Minor GC 時,會將Eden區和以前一個 Survivor 區中存活的對象複製到另外一個 Survivor 區中,採用的是我以前提到的複製算法,同時它們的生命存活計數也會加一。設計模式
這個過程會持續不少遍,直到對象的存活計數達到必定的閾值後會觸發一個叫作晉升的現象:新生代的這個對象會被放置到老年代中。 老年代中的對象都是通過屢次 GC 依然存活的生命週期很長的 Java 對象。當老年代的內存達到閾值後會觸發 Major GC,採用的是標記整理算法。數組
JVM 的內存區域能夠分爲兩類:線程私有和區域和線程共有的區域。 線程私有的區域:程序計數器、JVM 虛擬機棧、本地方法棧 線程共有的區域:堆、方法區、運行時常量池瀏覽器
其實除了程序計數器,其餘的部分都會發生 OOM。
Java 中類加載分爲 3 個步驟:加載、連接、初始化。
類加載器大體分爲3類:啓動類加載器、擴展類加載器、應用程序類加載器。
jre/lib
下的jar
文件。jre/lib/ext
下的jar
文件。classpath
下的文件。所謂的雙親委派模型就是當加載一個類時,會優先使用父類加載器加載,當父類加載器沒法加載時纔會使用子類加載器去加載。這麼作的目的是爲了不類的重複加載。
HashMap 的內部能夠看作數組+鏈表的複合結構。數組被分爲一個個的桶(bucket)。哈希值決定了鍵值對在數組中的尋址。具備相同哈希值的鍵值對會組成鏈表。須要注意的是當鏈表長度超過閾值(默認是8)的時候會觸發樹化,鏈表會變成樹形結構。
把握HashMap的原理須要關注4個方法:hash、put、get、resize。
hash方法。 將 key 的 hashCode 值的高位數據移位到低位進行異或運算。這麼作的緣由是有些 key 的 hashCode 值的差別集中在高位,而哈希尋址是忽略容量以上高位的,這種作法能夠有效避免哈希衝突。
put 方法。 put 方法主要有如下幾個步驟:
get方法。 get 方法主要有如下幾個步驟:
resize 方法。 resize 作了兩件事:
join 方法一般是保證線程間順序調度的一個方法,它是 Thread 類中的方法。比方說在線程 A 中執行線程 B.join()
,這時線程 A 會進入等待狀態,直到線程 B 執行完畢以後纔會喚醒,繼續執行A線程中的後續方法。
join 方法能夠傳時間參數,也能夠不傳參數,不傳參數實際上調用的是 join(0)
。它的原理實際上是使用了 wait 方法,join 的原理以下:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
複製代碼
生產者消費者模式要保證的是當緩衝區滿的時候生產者再也不生產對象,當緩衝區空時,消費者再也不消費對象。實現機制就是當緩衝區滿時讓生產者處於等待狀態,當緩衝區爲空時讓消費者處於等待狀態。當生產者生產了一個對象後會喚醒消費者,當消費者消費一個對象後會喚醒生產者。
三種種實現方式:wait 和 notify、await 和 signal、BlockingQueue。
//wait和notify
import java.util.LinkedList;
public class StorageWithWaitAndNotify {
private final int MAX_SIZE = 10;
private LinkedList<Object> list = new LinkedList<Object>();
public void produce() {
synchronized (list) {
while (list.size() == MAX_SIZE) {
System.out.println("倉庫已滿:生產暫停");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(new Object());
System.out.println("生產了一個新產品,現庫存爲:" + list.size());
list.notifyAll();
}
}
public void consume() {
synchronized (list) {
while (list.size() == 0) {
System.out.println("庫存爲0:消費暫停");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove();
System.out.println("消費了一個產品,現庫存爲:" + list.size());
list.notifyAll();
}
}
}
複製代碼
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class StorageWithAwaitAndSignal {
private final int MAX_SIZE = 10;
private ReentrantLock mLock = new ReentrantLock();
private Condition mEmpty = mLock.newCondition();
private Condition mFull = mLock.newCondition();
private LinkedList<Object> mList = new LinkedList<Object>();
public void produce() {
mLock.lock();
while (mList.size() == MAX_SIZE) {
System.out.println("緩衝區滿,暫停生產");
try {
mFull.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mList.add(new Object());
System.out.println("生產了一個新產品,現容量爲:" + mList.size());
mEmpty.signalAll();
mLock.unlock();
}
public void consume() {
mLock.lock();
while (mList.size() == 0) {
System.out.println("緩衝區爲空,暫停消費");
try {
mEmpty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mList.remove();
System.out.println("消費了一個產品,現容量爲:" + mList.size());
mFull.signalAll();
mLock.unlock();
}
}
複製代碼
import java.util.concurrent.LinkedBlockingQueue;
public class StorageWithBlockingQueue {
private final int MAX_SIZE = 10;
private LinkedBlockingQueue<Object> list = new LinkedBlockingQueue<Object>(MAX_SIZE);
public void produce() {
if (list.size() == MAX_SIZE) {
System.out.println("緩衝區已滿,暫停生產");
}
try {
list.put(new Object());
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生產了一個產品,現容量爲:" + list.size());
}
public void consume() {
if (list.size() == 0) {
System.out.println("緩衝區爲空,暫停消費");
}
try {
list.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消費了一個產品,現容量爲:" + list.size());
}
}
複製代碼
final 能夠修飾類、變量和方法。修飾類表明這個類不可被繼承。修飾變量表明此變量不可被改變。修飾方法表示此方法不可被重寫 (override)。
finally 是保證重點代碼必定會執行的一種機制。一般是使用 try-finally 或者 try-catch-finally 來進行文件流的關閉等操做。
finalize 是 Object 類中的一個方法,它的設計目的是保證對象在垃圾收集前完成特定資源的回收。finalize 機制如今已經不推薦使用,而且在 JDK 9已經被標記爲 deprecated。
Java 中常見的單例模式實現有這麼幾種:餓漢式、雙重判斷的懶漢式、靜態內部類實現的單例、枚舉實現的單例。 這裏着重講一下雙重判斷的懶漢式和靜態內部類實現的單例。
雙重判斷的懶漢式:
public class SingleTon {
//須要注意的是volatile
private static volatile SingleTon mInstance;
private SingleTon() {
}
public static SingleTon getInstance() {
if (mInstance == null) {
synchronized (SingleTon.class) {
if (mInstance == null) {
mInstance=new SingleTon();
}
}
}
return mInstance;
}
}
複製代碼
雙重判斷的懶漢式單例既知足了延遲初始化,又知足了線程安全。經過 synchronized 包裹代碼來實現線程安全,經過雙重判斷來提升程序執行的效率。這裏須要注意的是單例對象實例須要有 volatile 修飾,若是沒有 volatile 修飾,在多線程狀況下可能會出現問題。緣由是這樣的,mInstance=new SingleTon()
這一句代碼並非一個原子操做,它包含三個操做:
咱們知道 JVM 會發生指令重排,正常的執行順序是1-2-3
,但發生指令重排後可能會致使1-3-2
。咱們考慮這樣一種狀況,當線程 A 執行到1-3-2
的3步驟暫停了,這時候線程 B 調用了 getInstance,走到了最外層的if判斷上,因爲最外層的 if 判斷並無 synchronized 包裹,因此能夠執行到這一句,這時候因爲線程 A 已經執行了步驟3,此時 mInstance 已經不爲 null 了,因此線程B直接返回了 mInstance。但其實咱們知道,完整的初始化必須走完這三個步驟,因爲線程 A 只走了兩個步驟,因此必定會報錯的。
解決的辦法就是使用 volatile 修飾 mInstance,咱們知道 volatile 有兩個做用:保證可見性和禁止指令重排,在這裏關鍵在於禁止指令重排,禁止指令重排後保證了不會發生上述問題。
靜態內部類實現的單例:
class SingletonWithInnerClass {
private SingletonWithInnerClass() {
}
private static class SingletonHolder{
private static SingletonWithInnerClass INSTANCE=new SingletonWithInnerClass();
}
public SingletonWithInnerClass getInstance() {
return SingletonHolder.INSTANCE;
}
}
複製代碼
因爲外部類的加載並不會致使內部類當即加載,只有當調用 getInstance 的時候纔會加載內部類,因此實現了延遲初始化。因爲類只會被加載一次,而且類加載也是線程安全的,因此知足咱們全部的需求。靜態內部類實現的單例也是最爲推薦的一種方式。
Java中引用類型分爲四類:強引用、軟引用、弱引用、虛引用。
強引用: 強引用指的是經過 new 對象建立的引用,垃圾回收器即便是內存不足也不會回收強引用指向的對象。
軟引用: 軟引用是經過 SoftRefrence 實現的,它的生命週期比強引用短,在內存不足,拋出 OOM 以前,垃圾回收器會回收軟引用引用的對象。軟引用常見的使用場景是存儲一些內存敏感的緩存,當內存不足時會被回收。
弱引用: 弱引用是經過 WeakRefrence 實現的,它的生命週期比軟引用還短,GC 只要掃描到弱引用的對象就會回收。弱引用常見的使用場景也是存儲一些內存敏感的緩存。
虛引用: 虛引用是經過 FanttomRefrence 實現的,它的生命週期最短,隨時可能被回收。若是一個對象只被虛引用引用,咱們沒法經過虛引用來訪問這個對象的任何屬性和方法。它的做用僅僅是保證對象在 finalize 後,作某些事情。虛引用常見的使用場景是跟蹤對象被垃圾回收的活動,當一個虛引用關聯的對象被垃圾回收器回收以前會收到一條系統通知。
Exception 和 Error 都繼承於 Throwable,在 Java 中,只有 Throwable 類型的對象才能被 throw 或者 catch,它是異常處理機制的基本組成類型。
Exception 和 Error 體現了 Java 對不一樣異常狀況的分類。Exception 是程序正常運行中,能夠預料的意外狀況,可能而且應該被捕獲,進行相應的處理。
Error 是指在正常狀況下,不大可能出現的狀況,絕大部分 Error 都會使程序處於非正常、不可恢復的狀態。既然是非正常,因此不便於也不須要捕獲,常見的 OutOfMemoryError 就是 Error 的子類。
Exception 又分爲 checked Exception 和 unchecked Exception。
通常提到 volatile,就不得不提到內存模型相關的概念。咱們都知道,在程序運行中,每條指令都是由 CPU 執行的,而指令的執行過程當中,勢必涉及到數據的讀取和寫入。程序運行中的數據都存放在主存中,這樣會有一個問題,因爲 CPU 的執行速度是要遠高於主存的讀寫速度,因此直接從主存中讀寫數據會下降 CPU 的效率。爲了解決這個問題,就有了高速緩存的概念,在每一個 CPU 中都有高速緩存,它會事先從主存中讀取數據,在 CPU 運算以後在合適的時候刷新到主存中。
這樣的運行模式在單線程中是沒有任何問題的,但在多線程中,會致使緩存一致性的問題。舉個簡單的例子:i=i+1
,在兩個線程中執行這句代碼,假設i的初始值爲0。咱們指望兩個線程運行後獲得2,那麼有這樣的一種狀況,兩個線程都從主存中讀取i到各自的高速緩存中,這時候兩個線程中的i都爲0。在線程1執行完畢獲得i=1
,將之刷新到主存後,線程2開始執行,因爲線程2中的i是高速緩存中的0,因此在執行完線程2以後刷新到主存的i仍舊是1。
因此這就致使了對共享變量的緩存一致性的問題,那麼爲了解決這個問題,提出了緩存一致性協議:當 CPU 在寫數據時,若是發現操做的是共享變量,它會通知其餘 CPU 將它們內部的這個共享變量置爲無效狀態,當其餘 CPU 讀取緩存中的共享變量時,發現這個變量是無效的,它會重新從主存中讀取最新的值。
在Java的多線程開發中,有三個重要概念:原子性、可見性、有序性。
volatile的原理是在生成的彙編代碼中多了一個lock前綴指令,這個前綴指令至關於一個內存屏障,這個內存屏障有3個做用:
http 是超文本傳輸協議,而 https 能夠簡單理解爲安全的 http 協議。https 經過在 http 協議下添加了一層 ssl 協議對數據進行加密從而保證了安全。https 的做用主要有兩點:創建安全的信息傳輸通道,保證數據傳輸安全;確認網站的真實性。
提到 https 的話首先要說到加密算法,加密算法分爲兩類:對稱加密和非對稱加密。
對稱加密: 加密和解密用的都是相同的祕鑰,優勢是速度快,缺點是安全性低。常見的對稱加密算法有 DES、AES 等等。
非對稱加密: 非對稱加密有一個祕鑰對,分爲公鑰和私鑰。通常來講,私鑰本身持有,公鑰能夠公開給對方,優勢是安全性比對稱加密高,缺點是數據傳輸效率比對稱加密低。採用公鑰加密的信息只有對應的私鑰能夠解密。常見的非對稱加密包括RSA等。
在正式的使用場景中通常都是對稱加密和非對稱加密結合使用,使用非對稱加密完成祕鑰的傳遞,而後使用對稱祕鑰進行數據加密和解密。兩者結合既保證了安全性,又提升了數據傳輸效率。
AIDL 、廣播、文件、socket、管道
Android 中經常使用的性能優化工具包括這些:Android Studio 自帶的 Android Profiler、LeakCanary、BlockCanary
Android 自帶的 Android Profiler 其實就很好用,Android Profiler 能夠檢測三個方面的性能問題:CPU、MEMORY、NETWORK。
LeakCanary 是一個第三方的檢測內存泄漏的庫,咱們的項目集成以後 LeakCanary 會自動檢測應用運行期間的內存泄漏,並將之輸出給咱們。
BlockCanary 也是一個第三方檢測UI卡頓的庫,項目集成後Block也會自動檢測應用運行期間的UI卡頓,並將之輸出給咱們。
jar/apk/dex
,能夠從 SD卡中加載未安裝的 apkAndroid中動畫大體分爲3類:幀動畫、補間動畫(Tween Animation)、屬性動畫(Property Animation)。
View.animate()
便可獲得 ViewPropertyAnimator,以後進行相應的動畫操做便可。後者適合用於爲咱們的自定義控件添加動畫,固然首先咱們應該在自定義 View 中添加相應的 getXXX()
和 setXXX()
相應屬性的 getter 和 setter 方法,這裏須要注意的是在 setter 方法內改變了自定義 View 中的屬性後要調用 invalidate()
來刷新View的繪製。以後調用 ObjectAnimator.of
屬性類型()返回一個 ObjectAnimator,調用 start()
方法啓動動畫便可。補間動畫與屬性動畫的區別:
說到 Handler,就不得不提與之密切相關的這幾個類:Message、MessageQueue,Looper。
Message。 Message 中有兩個成員變量值得關注:target 和 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 中的性能優化在我看來分爲如下幾個方面:內存優化、佈局優化、網絡優化、安裝包優化。
內存優化: 下一個問題就是。
佈局優化: 佈局優化的本質就是減小 View 的層級。常見的佈局優化方案以下
\< include \>
標籤\< ViewStub \>
標籤來加載不經常使用的佈局\< Merge \>
標籤來減小布局的嵌套層次網絡優化: 常見的網絡優化方案以下
安裝包優化: 安裝包優化的核心就是減小 apk 的體積,常見的方案以下
Android的內存優化在我看來分爲兩點:避免內存泄漏、擴大內存,其實就是開源節流。
其實內存泄漏的本質就是較長生命週期的對象引用了較短生命週期的對象。
handler.removeCallbacksAndMessages
來取消延時事件。disposable.dispose()
來取消操做。爲何要擴大咱們的內存呢?有時候咱們實際開發中不可避免的要使用不少第三方商業的 SDK,這些 SDK 其實有好有壞,大廠的 SDK 可能內存泄漏會少一些,但一些小廠的 SDK 質量也就不太靠譜一些。那應對這種咱們沒法改變的狀況,最好的辦法就是擴大內存。
擴大內存一般有兩種方法:一個是在清單文件中的 Application 下添加largeHeap="true"
這個屬性,另外一個就是同一個應用開啓多個進程來擴大一個應用的總內存空間。第二種方法其實就很常見了,比方說我使用過個推的 S DK,個推的 Service 其實就是處在另一個單獨的進程中。
Android 中的內存優化總的來講就是開源和節流,開源就是擴大內存,節流就是避免內存泄漏。
在Linux中,爲了不一個進程對其餘進程的干擾,進程之間是相互獨立的。在一個進程中其實還分爲用戶空間和內核空間。這裏的隔離分爲兩個部分,進程間的隔離和進程內的隔離。
既然進程間存在隔離,那其實也是存在着交互。進程間通訊就是 IPC,用戶空間和內核空間的通訊就是系統調用。
Linux 爲了保證獨立性和安全性,進程之間不能直接相互訪問,Android 是基於 Linux 的,因此也是須要解決進程間通訊的問題。
其實 Linux 進程間通訊有不少方式,好比管道、socket 等等。爲何 Android 進程間通訊採用了Binder而不是 Linux
已有的方式,主要是有這麼兩點考慮:性能和安全
性能。 在移動設備上對性能要求是比較嚴苛的。Linux傳統的進程間通訊好比管道、socket等等進程間通訊是須要複製兩次數據,而Binder則只須要一次。因此Binder在性能上是優於傳統進程通訊的。
安全。 傳統的 Linux 進程通訊是不包含通訊雙方的身份驗證的,這樣會致使一些安全性問題。而Binder機制自帶身份驗證,從而有效的提升了安全性。
Binder 是基於 CS 架構的,有四個主要組成部分。
Binder 機制主要的流程是這樣的:
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緩存到內存中,運行速度快,可是內存容量小。 本地緩存:將圖片緩存到文件中,速度較慢,但容量較大。 網絡緩存:從網絡獲取圖片,速度受網絡影響。
若是咱們設計一個圖片加載框架,流程必定是這樣的:
上面是一些基本的概念,若是是具體的代碼實現的話,大概須要這麼幾個方面的文件:
displayImage(url,imageView)
,另外一個是從網絡獲取圖片downloadImage(url,imageView)
。在展現圖片方法中首先要經過 ImageView.setTag(url)
,將 url 和 imageView 進行綁定,這是爲了不在列表中加載網絡圖片時會因爲ImageView的複用致使的圖片錯位的 bug。以後會從 MemeryAndDiskCache 中獲取緩存,若是存在,直接加載;若是不存在,則調用從網絡獲取圖片這個方法。從網絡獲取圖片方法不少,這裏我通常都會使用 OkHttp+Retrofit
。當從網絡中獲取到圖片以後,首先判斷一下imageView.getTag()
與圖片的 url 是否一致,若是一致則加載圖片,若是不一致則不加載圖片,經過這樣的方式避免了列表中異步加載圖片的錯位。同時在獲取到圖片以後會經過 MemeryAndDiskCache 來緩存圖片。在咱們的手指觸摸到屏幕的時候,事件實際上是經過 Activity -> ViewGroup -> View
這樣的流程到達最後響應咱們觸摸事件的 View。
說到事件分發,必不可少的是這幾個方法:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent。
接下來就按照Activity -> ViewGroup -> View
的流程來大體說一下事件分發機制。
咱們的手指觸摸到屏幕的時候,會觸發一個 Action_Down 類型的事件,當前頁面的 Activity 會首先作出響應,也就是說會走到 Activity 的 dispatchTouchEvent()
方法內。在這個方法內部簡單來講是這麼一個邏輯:
getWindow.superDispatchTouchEvent()。
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 都是不攔截的;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()
中,是這麼一個邏輯:
onTouchEvent()
方法。onTouchEvent()
方法中。因此 onTouch 的順序是在 onTouchEvent 以前的。視圖繪製的起點在 ViewRootImpl 類的 performTraversals()
方法,在這個方法內實際上是按照順序依次調用了 mView.measure()、mView.layout()、mView.draw()
View的繪製流程分爲3步:測量、佈局、繪製,分別對應3個方法 measure、layout、draw。
測量階段。 measure 方法會被父 View 調用,在measure 方法中作一些優化和準備工做後會調用 onMeasure 方法進行實際的自我測量。onMeasure方法在View和ViewGroup作的事情是不同的:
佈局階段。 layout 方法會被父View調用,layout 方法會保存父 View 傳進來的尺寸和位置,並調用 onLayout 進行實際的內部佈局。onLayout 在 View 和 ViewGroup 中作的事情也是不同的:
繪製階段。 draw 方法會作一些調度工做,而後會調用 onDraw 方法進行 View 的自我繪製。draw 方法的調度流程大體是這樣的:
drawBackground(Canvas)
方法。onDraw(Canvas)
方法。dispatchDraw(Canvas)
方法。onDrawForeground(Canvas)
。在 Android 中,Android 與js 的交互分爲兩個方面:Android 調用 js 裏的方法、js 調用 Android 中的方法。
Android調js。 Android 調 js 有兩種方法:
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。
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()
方法壓縮數組,以後會找到合適的索引,將索引以後的鍵值對後移,插入新的鍵值對,這個過程當中可能會觸發數組的擴容。咱們知道內存中的 Bitmap 大小的計算公式是:長所佔像素 * 寬所佔像素 * 每一個像素所佔內存。想避免 OOM 有兩種方法:等比例縮小長寬、減小每一個像素所佔的內存。
decodeFile()、decodeStream()、decodeByteArray()、decodeResource()
。這些方法中都有一個 Options 類型的參數,這個 Options 是 BitmapFactory 的內部類,存儲着 BItmap 的一些信息。Options 中有一個屬性:inSampleSize。咱們經過修改 inSampleSize 能夠縮小圖片的長寬,從而減小 BItma p 所佔內存。須要注意的是這個 inSampleSize 大小須要是 2 的冪次方,若是小於 1,代碼會強制讓inSampleSize爲1。ARGB_8888
,表明每一個像素所佔尺寸。咱們能夠經過將之修改成 RGB_565
或者 ARGB_4444
來減小一半內存。加載高清大圖,好比清明上河圖,首先屏幕是顯示不下的,並且考慮到內存狀況,也不可能一次性所有加載到內存。這時候就須要局部加載了,Android中有一個負責局部加載的類:BitmapRegionDecoder。使用方法很簡單,經過BitmapRegionDecoder.newInstance()建立對象,以後調用decodeRegion(Rect rect, BitmapFactory.Options options)便可。第一個參數rect是要顯示的區域,第二個參數是BitmapFactory中的內部類Options。
因爲源碼分析篇幅太大,因此這裏之貼出個人源碼分析的連接(掘金)。
Retrofit源碼分析1 Retrofit源碼分析2 Retrofit源碼分析3
大體是這麼一個流程: register:
subscribe(subscriber,subscriberMethod)
方法,在 subscribe 方法內:
subscriptionsByEventType.get(eventType)
獲取 Subscription 集合
typesBySubscriber.get(subscriber)
獲取事件類型集合
subscribe(subscriber,subscriberMethod)
到此終結stickyEvents.get(eventType)
獲取事件併發送post:
subscriptionsByEventType.get(eventClass)
獲取 Subscription 類型集合invokeSubscriber(subscription, event)
處理事件,本質是 method.invoke()
反射unregister:
工做中解決了什麼難題,作了什麼有成就感的項目(這個問題必定會問到,因此確定要作準備)
這個問題其實仍是靠平時的積累,對我來講的話,最有成就感的就是開發了KCommon這個項目,它大大提高了個人開發效率。
瞭解掘金秋招求職徵文活動更多信息👉秋招求職時,寫文就有好禮相送 | 掘金技術徵文