Android面試(附答案)

寫在開頭

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

Java面試題

GC機制

垃圾回收須要完成兩件事:找到垃圾,回收垃圾。
找到垃圾通常的話有兩種方法:前端

  • 引用計數法

當一個對象被引用時,它的引用計數器會加一,垃圾回收時會清理掉引用計數爲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內存區域的劃分,哪些區域會發生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,而後根據radom一、radom二、pre-master secret經過必定的算法得出一個對稱加密的祕鑰,做爲後面交互過程當中使用對稱祕鑰。同時客戶端也會使用radom一、radom二、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"這個屬性,另外一個就是同一個應用開啓多個進程來擴大一個應用的總內存空間。第二種方法其實就很常見了,比方說我使用過個推的SDK,個推的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經過Binder驅動返回服務端的代理對象。
  • 客戶端拿到服務端的代理對象後便可進行進程間通訊。

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方法會調用全部子View的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覆蓋DELETE便可,在這裏能夠避免數組元素的移動,從而提升了效率。若是value不爲DELETE,會判斷mGarbage,若是爲true,則會調用gc()方法壓縮數組,以後會找到合適的索引,將索引以後的鍵值對後移,插入新的鍵值對,這個過程當中可能會觸發數組的擴容。

圖片加載如何避免OOM

咱們知道內存中的Bitmap大小的計算公式是:長所佔像素 * 寬所佔像素 * 每一個像素所佔內存。想避免OOM有兩種方法:等比例縮小長寬、減小每一個像素所佔的內存。

  • 等比縮小長寬。咱們知道Bitmap的建立是經過BitmapFactory的工廠方法,decodeFile()、decodeStream()、decodeByteArray()、decodeResource()。這些方法中都有一個Options類型的參數,這個Options是BitmapFactory的內部類,存儲着BItmap的一些信息。Options中有一個屬性:inSampleSize。咱們經過修改inSampleSize能夠縮小圖片的長寬,從而減小BItmap所佔內存。須要注意的是這個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這個項目,它大大提高了個人開發效率。

相關文章
相關標籤/搜索