Android 面試題(附答案) | 掘金技術徵文

BlackFlagBin發表於2018-09-21

寫在開頭

由於杭州的房價實在太高,所以我可恥的跑路到了西安。幾個月前在西安買了房,所以最近總結了一些還算全面的 Android面試題。還好成功的通過了西安努比亞的面試,雖然不是阿里、網易這種級別的公司,但對我一個畢業兩年的 Android 開發來說,算是成功的從小公司跳到大公司。

Java面試題

GC機制

垃圾回收需要完成兩件事:找到垃圾,回收垃圾。 找到垃圾一般的話有兩種方法:

  • 引用計數法: 當一個物件被引用時,它的引用計數器會加一,垃圾回收時會清理掉引用計數為0的物件。但這種方法有一個問題,比方說有兩個物件 A 和 B,A 引用了 B,B 又引用了 A,除此之外沒有別的物件引用 A 和 B,那麼 A 和 B 在我們看來已經是垃圾物件,需要被回收,但它們的引用計數不為 0,沒有達到回收的條件。正因為這個迴圈引用的問題,Java 並沒有採用引用計數法。

  • 可達性分析法: 我們把 Java 中物件引用的關係看做一張圖,從根級物件不可達的物件會被垃圾收集器清除。根級物件一般包括 Java 虛擬機器棧中的物件、本地方法棧中的物件、方法區中的靜態物件和常量池中的常量。 回收垃圾的話有這麼四種方法:

  • 標記清除演算法: 顧名思義分為兩步,標記和清除。首先標記到需要回收的垃圾物件,然後回收掉這些垃圾物件。標記清除演算法的缺點是清除垃圾物件後會造成記憶體的碎片化。

  • 複製演算法: 複製演算法是將存活的物件複製到另一塊記憶體區域中,並做相應的記憶體整理工作。複製演算法的優點是可以避免記憶體碎片化,缺點也顯而易見,它需要兩倍的記憶體。

  • 標記整理演算法: 標記整理演算法也是分兩步,先標記後整理。它會標記需要回收的垃圾物件,清除掉垃圾物件後會將存活的物件壓縮,避免了記憶體的碎片化。

  • 分代演算法: 分代演算法將物件分為新生代和老年代物件。那麼為什麼做這樣的區分呢?主要是在Java執行中會產生大量物件,這些物件的生命週期會有很大的不同,有的生命週期很長,有的甚至使用一次之後就不再使用。所以針對不同生命週期的物件採用不同的回收策略,這樣可以提高GC的效率。

新生代物件分為三個區域:Eden 區和兩個 Survivor 區。新建立的物件都放在 Eden區,當 Eden 區的記憶體達到閾值之後會觸發 Minor GC,這時會將存活的物件複製到一個 Survivor 區中,這些存活物件的生命存活計數會加一。這時 Eden 區會閒置,當再一次達到閾值觸發 Minor GC 時,會將Eden區和之前一個 Survivor 區中存活的物件複製到另一個 Survivor 區中,採用的是我之前提到的複製演算法,同時它們的生命存活計數也會加一。

這個過程會持續很多遍,直到物件的存活計數達到一定的閾值後會觸發一個叫做晉升的現象:新生代的這個物件會被放置到老年代中。 老年代中的物件都是經過多次 GC 依然存活的生命週期很長的 Java 物件。當老年代的記憶體達到閾值後會觸發 Major GC,採用的是標記整理演算法。

JVM記憶體區域的劃分,哪些區域會發生 OOM

JVM 的記憶體區域可以分為兩類:執行緒私有和區域和執行緒共有的區域。 執行緒私有的區域:程式計數器、JVM 虛擬機器棧、本地方法棧 執行緒共有的區域:堆、方法區、執行時常量池

  • 程式計數器。 每個執行緒有有一個私有的程式計數器,任何時間一個執行緒都只會有一個方法正在執行,也就是所謂的當前方法。程式計數器存放的就是這個當前方法的JVM指令地址。
  • JVM虛擬機器棧。 建立執行緒的時候會建立執行緒內的虛擬機器棧,棧中存放著一個個的棧幀,對應著一個個方法的呼叫。JVM 虛擬機器棧有兩種操作,分別是壓棧和出站。棧幀中存放著區域性變數表、方法返回值和方法的正常或異常退出的定義等等。
  • 本地方法棧。 跟 JVM 虛擬機器棧比較類似,只不過它支援的是 Native 方法。
  • 堆。 堆是記憶體管理的核心區域,用來存放物件例項。幾乎所有建立的物件例項都會直接分配到堆上。所以堆也是垃圾回收的主要區域,垃圾收集器會對堆有著更細的劃分,最常見的就是把堆劃分為新生代和老年代。
  • 方法區。方法區主要存放類的結構資訊,比如靜態屬性和方法等等。
  • 執行時常量池。執行時常量池位於方法區中,主要存放各種常量資訊。

其實除了程式計數器,其他的部分都會發生 OOM。

  • 堆。 通常發生的 OOM 都會發生在堆中,最常見的可能導致 OOM 的原因就是記憶體洩漏。
  • JVM虛擬機器棧和本地方法棧。 當我們寫一個遞迴方法,這個遞迴方法沒有迴圈終止條件,最終會導致 StackOverflow 的錯誤。當然,如果棧空間擴充套件失敗,也是會發生 OOM 的。
  • 方法區。方法區現在基本上不太會發生 OOM,但在早期記憶體中載入的類資訊過多的情況下也是會發生 OOM 的。

類載入過程

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 索引值,將原節點重新放到新的陣列中。這一步可以將原先衝突的節點分散到新的桶中。

什麼情況下 Java 會產生死鎖,如何定位、修復,手寫死鎖

sleep 和 wait 的區別

  • sleep 方法是 Thread 類中的靜態方法,wait 是 Object 類中的方法
  • sleep 並不會釋放同步鎖,而 wait 會釋放同步鎖
  • sleep 可以在任何地方使用,而 wait 只能在同步方法或者同步程式碼塊中使用
  • sleep 中必須傳入時間,而 wait 可以傳,也可以不傳,不傳時間的話只有 notify 或者 notifyAll 才能喚醒,傳時間的話在時間之後會自動喚醒

join 的用法

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;
            }
        }
    }
複製程式碼

volatile和synchronize的區別

Java中的執行緒池

執行緒通訊

Java中的併發集合

Java中生產者與消費者模式

生產者消費者模式要保證的是當緩衝區滿的時候生產者不再生產物件,當緩衝區空時,消費者不再消費物件。實現機制就是當緩衝區滿時讓生產者處於等待狀態,當緩衝區為空時讓消費者處於等待狀態。當生產者生產了一個物件後會喚醒消費者,當消費者消費一個物件後會喚醒生產者。

三種種實現方式:wait 和 notify、await 和 signal、BlockingQueue。

  • wait 和 notify
//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();
        }
    }


}

複製程式碼
  • await 和 signal
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();
    }
}

複製程式碼
  • BlockingQueue
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、finally、finalize區別

final 可以修飾類、變數和方法。修飾類代表這個類不可被繼承。修飾變數代表此變數不可被改變。修飾方法表示此方法不可被重寫 (override)。

finally 是保證重點程式碼一定會執行的一種機制。通常是使用 try-finally 或者 try-catch-finally 來進行檔案流的關閉等操作。

finalize 是 Object 類中的一個方法,它的設計目的是保證物件在垃圾收集前完成特定資源的回收。finalize 機制現在已經不推薦使用,並且在 JDK 9已經被標記為 deprecated。

Java 中單例模式

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()這一句程式碼並不是一個原子操作,它包含三個操作:

  1. 給 mInstance 分配記憶體
  2. 呼叫 SingleTon 的構造方法初始化成員變數
  3. 將 mInstance 指向分配的記憶體空間(在這一步 mInstance 已經不為 null 了)

我們知道 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中引用型別的區別,具體的使用場景

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 也就是執行時異常,類似空指標異常、陣列越界等,通常是可以避免的邏輯錯誤,具體根據需求來判斷是否需要捕獲,並不會在編譯器強制要求。

volatile

一般提到 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,其實就是保證了可見性和有序性。 可見性我上面已經說過了,在多執行緒開發中是很有必要的。這個有序性還是得說一下,為了執行的效率,有時候會發生指令重排,這在單執行緒中指令重排之後的輸出與我們的程式碼邏輯輸出還是一致的。但在多執行緒中就可能發生問題,volatile在一定程度上可以避免指令重排。

volatile的原理是在生成的彙編程式碼中多了一個lock字首指令,這個字首指令相當於一個記憶體屏障,這個記憶體屏障有3個作用:

  • 確保指令重排的時候不會把屏障後的指令排在屏障前,確保不會把屏障前的指令排在屏障後。
  • 修改快取中的共享變數後立即重新整理到主存中。
  • 當執行寫操作時會導致其他CPU中的快取無效。

網路相關面試題

http 狀態碼

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、管道

廣播靜態註冊和動態註冊的區別

  1. 動態註冊廣播不是常駐型廣播,也就是說廣播跟隨 Activity 的生命週期。注意在 Activity 結束前,移除廣播接收器。 靜態註冊是常駐型,也就是說當應用程式關閉後,如果有資訊廣播來,程式也會被系統呼叫自動執行。
  2. 當廣播為有序廣播時:優先順序高的先接收(不分靜態和動態)。同優先順序的廣播接收器,動態優先於靜態
  3. 同優先順序的同類廣播接收器,靜態:先掃描的優先於後掃描的,動態:先註冊的優先於後註冊的。
  4. 當廣播為預設廣播時:無視優先順序,動態廣播接收器優先於靜態廣播接收器。同優先順序的同類廣播接收器,靜態:先掃描的優先於後掃描的,動態:先註冊的優先於後冊的。

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。為什麼刪除首部的元素,原因很明顯:我們最近經常訪問的元素都會放置到尾部,那首部的元素肯定就是 最近最少使用 的元素了,因此當記憶體不足時應當優先刪除這些元素。

DiskLruCache原理

設計一個圖片的非同步載入框架

設計一個圖片載入框架,肯定要用到圖片載入的三級快取的思想。三級快取分為記憶體快取、本地快取和網路快取。

記憶體快取:將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 原始碼中常見的設計模式以及自己在開發中常用的設計模式

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。

Android三方庫的原始碼分析

由於原始碼分析篇幅太大,所以這裡之貼出我的原始碼分析的連結(掘金)。

OkHttp

OkHttp原始碼分析

Retrofit

Retrofit原始碼分析1 Retrofit原始碼分析2 Retrofit原始碼分析3

RxJava

RxJava原始碼分析

Glide

Glide原始碼分析

EventBus

EventBus原始碼分析

大致是這麼一個流程: register:

  • 獲取訂閱者的 Class 物件
  • 使用反射查詢訂閱者中的事件處理方法集合
  • 遍歷事件處理方法集合,呼叫 subscribe(subscriber,subscriberMethod) 方法,在 subscribe 方法內:
    • 通過 subscriberMethod 獲取處理的事件型別 eventType
    • 將訂閱者 subscriber 和方法 subscriberMethod 綁在一起形成一個 Subscription 物件
    • 通過 subscriptionsByEventType.get(eventType) 獲取 Subscription 集合
      • 如果 Subscription 集合為空則建立一個新的集合,這一步目的是延遲集合的初始化
      • 拿到 Subscription 集合後遍歷這個集合,通過比較事件處理的優先順序,將新的 Subscription 物件加入合適的位置
    • 通過typesBySubscriber.get(subscriber)獲取事件型別集合
      • 如果事件型別集合為空則建立一個新的集合,這一步目的是延遲集合的初始化
      • 拿到事件型別集合後將新的事件型別加入到集合中
    • 判斷當前事件型別是否是 sticky
    • 如果當前事件型別不是 sticky(粘性事件),subscribe(subscriber,subscriberMethod)到此終結
    • 如果是 sticky,判斷 EventBus 中的一個事件繼承性的屬性,預設是 true
      • 如果事件繼承性為 true,遍歷這個 Map 型別的 stickEvents,通過 isAssignableFrom 方法判斷當前事件是否是遍歷事件的父類,如果是則傳送事件
      • 如果事件繼承性為 false,通過 stickyEvents.get(eventType)獲取事件併傳送

post:

  • postSticky
    • 將事件加入到 stickyEvents 這個 Map 型別的集合中
    • 呼叫 post 方法
  • post
    • 將事件加入當前執行緒的事件佇列中
    • 通過 while 迴圈不斷從事件佇列中取出事件並呼叫 postSingleEvent 方法傳送事件
    • 在 postSingleEvent 中,判斷事件繼承性,預設為true
      • 事件繼承性為true,找到當前事件所有的父型別並呼叫 postSingleEventForEventType 方法傳送事件
      • 事件繼承性為 false,只傳送當前事件型別的事件
        • 在 postSingleEventForEventType 中,通過 subscriptionsByEventType.get(eventClass) 獲取 Subscription 型別集合
        • 遍歷這個集合,呼叫 postToSubscription 傳送事件
          • 在 postToSubscription 中分為四種情況
            • POSTING,呼叫 invokeSubscriber(subscription, event) 處理事件,本質是 method.invoke() 反射
            • MAIN,如果在主執行緒直接 invokeSubscriber 處理;反之通過 handler 切換到主執行緒呼叫 invokeSubscriber 處理事件
            • BACKGROUND,如果不在主執行緒直接 invokeSubscriber 處理事件;反之開啟一條執行緒,線上程中呼叫 invokeSubscriber 處理事件
            • ASYNC,開啟一條執行緒,線上程中呼叫 invokeSubscriber 處理事件

unregister:

  • 刪除 subscriptionsByEventType 中與訂閱者相關的所有 subscription
  • 刪除 typesBySubscriber 中與訂閱者相關的所有型別

資料結構與演算法

手寫快排

手寫歸併排序

手寫堆以及堆排序

說一下排序演算法的區別(時間複雜度和空間複雜度)

工作中解決了什麼難題,做了什麼有成就感的專案(這個問題一定會問到,所以肯定要做準備)

這個問題其實還是靠平時的積累,對我來說的話,最有成就感的就是開發了KCommon這個專案,它大大提升了我的開發效率。

瞭解掘金秋招求職徵文活動更多資訊?秋招求職時,寫文就有好禮相送 | 掘金技術徵文

Android 面試題(附答案) | 掘金技術徵文

相關文章