Android面試(附答案)

Java面試題

GC機(jī)制

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

  • 引用計(jì)數(shù)法

當(dāng)一個(gè)對(duì)象被引用時(shí),它的引用計(jì)數(shù)器會(huì)加一,垃圾回收時(shí)會(huì)清理掉引用計(jì)數(shù)為0的對(duì)象。但這種方法有一個(gè)問題,比方說有兩個(gè)對(duì)象A和B,A引用了B,B又引用了A,除此之外沒有別的對(duì)象引用A和B,那么A和B在我們看來已經(jīng)是垃圾對(duì)象,需要被回收,但它們的引用計(jì)數(shù)不為0,沒有達(dá)到回收的條件。正因?yàn)檫@個(gè)循環(huán)引用的問題,Java并沒有采用引用計(jì)數(shù)法。

  • 可達(dá)性分析法

我們把Java中對(duì)象引用的關(guān)系看做一張圖,從根級(jí)對(duì)象不可達(dá)的對(duì)象會(huì)被垃圾收集器清除。根級(jí)對(duì)象一般包括Java虛擬機(jī)棧中的對(duì)象、本地方法棧中的對(duì)象、方法區(qū)中的靜態(tài)對(duì)象和常量池中的常量。
回收垃圾的話有這么四種方法:

  • 標(biāo)記清除算法

顧名思義分為兩步,標(biāo)記和清除。首先標(biāo)記到需要回收的垃圾對(duì)象,然后回收掉這些垃圾對(duì)象。標(biāo)記清除算法的缺點(diǎn)是清除垃圾對(duì)象后會(huì)造成內(nèi)存的碎片化。

  • 復(fù)制算法

復(fù)制算法是將存活的對(duì)象復(fù)制到另一塊內(nèi)存區(qū)域中,并做相應(yīng)的內(nèi)存整理工作。復(fù)制算法的優(yōu)點(diǎn)是可以避免內(nèi)存碎片化,缺點(diǎn)也顯而易見,它需要兩倍的內(nèi)存。

  • 標(biāo)記整理算法

標(biāo)記整理算法也是分兩步,先標(biāo)記后整理。它會(huì)標(biāo)記需要回收的垃圾對(duì)象,清除掉垃圾對(duì)象后會(huì)將存活的對(duì)象壓縮,避免了內(nèi)存的碎片化。

  • 分代算法

分代算法將對(duì)象分為新生代和老年代對(duì)象。那么為什么做這樣的區(qū)分呢?主要是在Java運(yùn)行中會(huì)產(chǎn)生大量對(duì)象,這些對(duì)象的生命周期會(huì)有很大的不同,有的生命周期很長,有的甚至使用一次之后就不再使用。所以針對(duì)不同生命周期的對(duì)象采用不同的回收策略,這樣可以提高GC的效率。
新生代對(duì)象分為三個(gè)區(qū)域:Eden區(qū)和兩個(gè)Survivor區(qū)。新創(chuàng)建的對(duì)象都放在Eden區(qū),當(dāng)Eden區(qū)的內(nèi)存達(dá)到閾值之后會(huì)觸發(fā)Minor GC,這時(shí)會(huì)將存活的對(duì)象復(fù)制到一個(gè)Survivor區(qū)中,這些存活對(duì)象的生命存活計(jì)數(shù)會(huì)加一。這時(shí)Eden區(qū)會(huì)閑置,當(dāng)再一次達(dá)到閾值觸發(fā)Minor GC時(shí),會(huì)將Eden區(qū)和之前一個(gè)Survivor區(qū)中存活的對(duì)象復(fù)制到另一個(gè)Survivor區(qū)中,采用的是我之前提到的復(fù)制算法,同時(shí)它們的生命存活計(jì)數(shù)也會(huì)加一。這個(gè)過程會(huì)持續(xù)很多遍,直到對(duì)象的存活計(jì)數(shù)達(dá)到一定的閾值后會(huì)觸發(fā)一個(gè)叫做晉升的現(xiàn)象:新生代的這個(gè)對(duì)象會(huì)被放置到老年代中。
老年代中的對(duì)象都是經(jīng)過多次GC依然存活的生命周期很長的Java對(duì)象。當(dāng)老年代的內(nèi)存達(dá)到閾值后會(huì)觸發(fā)Major GC,采用的是標(biāo)記整理算法。

JVM內(nèi)存區(qū)域的劃分,哪些區(qū)域會(huì)發(fā)生OOM

JVM的內(nèi)存區(qū)域可以分為兩類:線程私有和區(qū)域和線程共有的區(qū)域。
線程私有的區(qū)域:程序計(jì)數(shù)器、JVM虛擬機(jī)棧、本地方法棧
線程共有的區(qū)域:堆、方法區(qū)、運(yùn)行時(shí)常量池

  • 程序計(jì)數(shù)器。每個(gè)線程有有一個(gè)私有的程序計(jì)數(shù)器,任何時(shí)間一個(gè)線程都只會(huì)有一個(gè)方法正在執(zhí)行,也就是所謂的當(dāng)前方法。程序計(jì)數(shù)器存放的就是這個(gè)當(dāng)前方法的JVM指令地址。
  • JVM虛擬機(jī)棧。創(chuàng)建線程的時(shí)候會(huì)創(chuàng)建線程內(nèi)的虛擬機(jī)棧,棧中存放著一個(gè)個(gè)的棧幀,對(duì)應(yīng)著一個(gè)個(gè)方法的調(diào)用。JVM虛擬機(jī)棧有兩種操作,分別是壓棧和出站。棧幀中存放著局部變量表、方法返回值和方法的正?;虍惓M顺龅亩x等等。
  • 本地方法棧。跟JVM虛擬機(jī)棧比較類似,只不過它支持的是Native方法。
  • 堆。堆是內(nèi)存管理的核心區(qū)域,用來存放對(duì)象實(shí)例。幾乎所有創(chuàng)建的對(duì)象實(shí)例都會(huì)直接分配到堆上。所以堆也是垃圾回收的主要區(qū)域,垃圾收集器會(huì)對(duì)堆有著更細(xì)的劃分,最常見的就是把堆劃分為新生代和老年代。
  • 方法區(qū)。方法區(qū)主要存放類的結(jié)構(gòu)信息,比如靜態(tài)屬性和方法等等。
  • 運(yùn)行時(shí)常量池。運(yùn)行時(shí)常量池位于方法區(qū)中,主要存放各種常量信息。

其實(shí)除了程序計(jì)數(shù)器,其他的部分都會(huì)發(fā)生OOM。

  • 堆。通常發(fā)生的OOM都會(huì)發(fā)生在堆中,最常見的可能導(dǎo)致OOM的原因就是內(nèi)存泄漏。
  • JVM虛擬機(jī)棧和本地方法棧。當(dāng)我們寫一個(gè)遞歸方法,這個(gè)遞歸方法沒有循環(huán)終止條件,最終會(huì)導(dǎo)致StackOverflow的錯(cuò)誤。當(dāng)然,如果??臻g擴(kuò)展失敗,也是會(huì)發(fā)生OOM的。
  • 方法區(qū)。方法區(qū)現(xiàn)在基本上不太會(huì)發(fā)生OOM,但在早期內(nèi)存中加載的類信息過多的情況下也是會(huì)發(fā)生OOM的。

類加載過程,雙親委派模型

Java中類加載分為3個(gè)步驟:加載、鏈接、初始化。
加載。加載是將字節(jié)碼數(shù)據(jù)從不同的數(shù)據(jù)源讀取到JVM內(nèi)存,并映射為JVM認(rèn)可的數(shù)據(jù)結(jié)構(gòu),也就是Class對(duì)象的過程。數(shù)據(jù)源可以是Jar文件、Class文件等等。如果數(shù)據(jù)的格式并不是ClassFile的結(jié)構(gòu),則會(huì)報(bào)ClassFormatError。
鏈接。鏈接是類加載的核心部分,這一步分為3個(gè)步驟:驗(yàn)證、準(zhǔn)備、解析。

  • 驗(yàn)證。驗(yàn)證是保證JVM安全的重要步驟。JVM需要校驗(yàn)字節(jié)信息是否符合規(guī)范,避免惡意信息和不規(guī)范數(shù)據(jù)危害JVM運(yùn)行安全。如果驗(yàn)證出錯(cuò),則會(huì)報(bào)VerifyError。
  • 準(zhǔn)備。這一步會(huì)創(chuàng)建靜態(tài)變量,并為靜態(tài)變量開辟內(nèi)存空間。
  • 解析。這一步會(huì)將符號(hào)引用替換為直接引用。

初始化。初始化會(huì)為靜態(tài)變量賦值,并執(zhí)行靜態(tài)代碼塊中的邏輯。

雙親委派模型。
類加載器大致分為3類:啟動(dòng)類加載器、擴(kuò)展類加載器、應(yīng)用程序類加載器。
啟動(dòng)類加載器主要加載 jre/lib下的jar文件。
擴(kuò)展類加載器主要加載 jre/lib/ext 下的jar文件。
應(yīng)用程序類加載器主要加載 classpath下的文件。

所謂的雙親委派模型就是當(dāng)加載一個(gè)類時(shí),會(huì)優(yōu)先使用父類加載器加載,當(dāng)父類加載器無法加載時(shí)才會(huì)使用子類加載器去加載。這么做的目的是為了避免類的重復(fù)加載。

Java中的集合類

HashMap的原理

HashMap的內(nèi)部可以看做數(shù)組+鏈表的復(fù)合結(jié)構(gòu)。數(shù)組被分為一個(gè)個(gè)的桶(bucket)。哈希值決定了鍵值對(duì)在數(shù)組中的尋址。具有相同哈希值的鍵值對(duì)會(huì)組成鏈表。需要注意的是當(dāng)鏈表長度超過閾值(默認(rèn)是8)的時(shí)候會(huì)觸發(fā)樹化,鏈表會(huì)變成樹形結(jié)構(gòu)。

把握HashMap的原理需要關(guān)注4個(gè)方法:hash、put、get、resize。

hash方法。將key的hashCode值的高位數(shù)據(jù)移位到低位進(jìn)行異或運(yùn)算。這么做的原因是有些key的hashCode值的差異集中在高位,而哈希尋址是忽略容量以上高位的,這種做法可以有效避免哈希沖突。

put方法。put方法主要有以下幾個(gè)步驟:

  • 通過hash方法獲取hash值,根據(jù)hash值尋址。
  • 如果未發(fā)生碰撞,直接放到桶中。
  • 如果發(fā)生碰撞,則以鏈表形式放在桶后。
  • 當(dāng)鏈表長度大于閾值后會(huì)觸發(fā)樹化,將鏈表轉(zhuǎn)換為紅黑樹。
  • 如果數(shù)組長度達(dá)到閾值,會(huì)調(diào)用resize方法擴(kuò)展容量。

get方法。get方法主要有以下幾個(gè)步驟:

  • 通過hash方法獲取hash值,根據(jù)hash值尋址。
  • 如果與尋址到桶的key相等,直接返回對(duì)應(yīng)的value。
  • 如果發(fā)生沖突,分兩種情況。如果是樹,則調(diào)用getTreeNode獲取value;如果是鏈表則通過循環(huán)遍歷查找對(duì)應(yīng)的value。

resize方法。resize做了兩件事:

  • 將原數(shù)組擴(kuò)展為原來的2倍
  • 重新計(jì)算index索引值,將原節(jié)點(diǎn)重新放到新的數(shù)組中。這一步可以將原先沖突的節(jié)點(diǎn)分散到新的桶中。

什么情況下Java會(huì)產(chǎn)生死鎖,如何定位、修復(fù),手寫死鎖

sleep和wait的區(qū)別

  • sleep方法是Thread類中的靜態(tài)方法,wait是Object類中的方法
  • sleep并不會(huì)釋放同步鎖,而wait會(huì)釋放同步鎖
  • sleep可以在任何地方使用,而wait只能在同步方法或者同步代碼塊中使用
  • sleep中必須傳入時(shí)間,而wait可以傳,也可以不傳,不傳時(shí)間的話只有notify或者notifyAll才能喚醒,傳時(shí)間的話在時(shí)間之后會(huì)自動(dòng)喚醒

join的用法

join方法通常是保證線程間順序調(diào)度的一個(gè)方法,它是Thread類中的方法。比方說在線程A中執(zhí)行線程B.join(),這時(shí)線程A會(huì)進(jìn)入等待狀態(tài),直到線程B執(zhí)行完畢之后才會(huì)喚醒,繼續(xù)執(zhí)行A線程中的后續(xù)方法。

join方法可以傳時(shí)間參數(shù),也可以不傳參數(shù),不傳參數(shù)實(shí)際上調(diào)用的是join(0)。它的原理其實(shí)是使用了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的區(qū)別

Java中的線程池

線程通信

Java中的并發(fā)集合

Java中生產(chǎn)者與消費(fèi)者模式

生產(chǎn)者消費(fèi)者模式要保證的是當(dāng)緩沖區(qū)滿的時(shí)候生產(chǎn)者不再生產(chǎn)對(duì)象,當(dāng)緩沖區(qū)空時(shí),消費(fèi)者不再消費(fèi)對(duì)象。實(shí)現(xiàn)機(jī)制就是當(dāng)緩沖區(qū)滿時(shí)讓生產(chǎn)者處于等待狀態(tài),當(dāng)緩沖區(qū)為空時(shí)讓消費(fèi)者處于等待狀態(tài)。當(dāng)生產(chǎn)者生產(chǎn)了一個(gè)對(duì)象后會(huì)喚醒消費(fèi)者,當(dāng)消費(fèi)者消費(fèi)一個(gè)對(duì)象后會(huì)喚醒生產(chǎn)者。
三種種實(shí)現(xiàn)方式: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("倉庫已滿:生產(chǎn)暫停");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            list.add(new Object());
            System.out.println("生產(chǎn)了一個(gè)新產(chǎn)品,現(xiàn)庫存為:" + list.size());
            list.notifyAll();
        }
    }

    public void consume() {
        synchronized (list) {
            while (list.size() == 0) {
                System.out.println("庫存為0:消費(fèi)暫停");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            list.remove();
            System.out.println("消費(fèi)了一個(gè)產(chǎn)品,現(xiàn)庫存為:" + 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("緩沖區(qū)滿,暫停生產(chǎn)");
            try {
                mFull.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        mList.add(new Object());
        System.out.println("生產(chǎn)了一個(gè)新產(chǎn)品,現(xiàn)容量為:" + mList.size());
        mEmpty.signalAll();

        mLock.unlock();
    }

    public void consume() {
        mLock.lock();
        while (mList.size() == 0) {
            System.out.println("緩沖區(qū)為空,暫停消費(fèi)");
            try {
                mEmpty.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        mList.remove();
        System.out.println("消費(fèi)了一個(gè)產(chǎn)品,現(xiàn)容量為:" + 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("緩沖區(qū)已滿,暫停生產(chǎn)");
        }

        try {
            list.put(new Object());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("生產(chǎn)了一個(gè)產(chǎn)品,現(xiàn)容量為:" + list.size());
    }

    public void consume() {
        if (list.size() == 0) {
            System.out.println("緩沖區(qū)為空,暫停消費(fèi)");
        }

        try {
            list.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("消費(fèi)了一個(gè)產(chǎn)品,現(xiàn)容量為:" + list.size());
    }

}

final、finally、finalize區(qū)別

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

finally是保證重點(diǎn)代碼一定會(huì)執(zhí)行的一種機(jī)制。通常是使用try-finally或者try-catch-finally來進(jìn)行文件流的關(guān)閉等操作。

finalize是Object類中的一個(gè)方法,它的設(shè)計(jì)目的是保證對(duì)象在垃圾收集前完成特定資源的回收。finalize機(jī)制現(xiàn)在已經(jīng)不推薦使用,并且在JDK 9已經(jīng)被標(biāo)記為deprecated。

Java中單例模式

Java中常見的單例模式實(shí)現(xiàn)有這么幾種:餓漢式、雙重判斷的懶漢式、靜態(tài)內(nèi)部類實(shí)現(xiàn)的單例、枚舉實(shí)現(xiàn)的單例。
這里著重講一下雙重判斷的懶漢式和靜態(tài)內(nèi)部類實(shí)現(xiàn)的單例。

雙重判斷的懶漢式:

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包裹代碼來實(shí)現(xiàn)線程安全,通過雙重判斷來提高程序執(zhí)行的效率。這里需要注意的是單例對(duì)象實(shí)例需要有volatile修飾,如果沒有volatile修飾,在多線程情況下可能會(huì)出現(xiàn)問題。原因是這樣的,mInstance=new SingleTon() 這一句代碼并不是一個(gè)原子操作,它包含三個(gè)操作:

  1. 給mInstance分配內(nèi)存
  2. 調(diào)用SingleTon的構(gòu)造方法初始化成員變量
  3. 將mInstance指向分配的內(nèi)存空間(在這一步mInstance已經(jīng)不為null了)

我們知道JVM會(huì)發(fā)生指令重排,正常的執(zhí)行順序是1-2-3,但發(fā)生指令重排后可能會(huì)導(dǎo)致1-3-2。我們考慮這樣一種情況,當(dāng)線程A執(zhí)行到1-3-2的3步驟暫停了,這時(shí)候線程B調(diào)用了getInstance,走到了最外層的if判斷上,由于最外層的if判斷并沒有synchronized包裹,所以可以執(zhí)行到這一句,這時(shí)候由于線程A已經(jīng)執(zhí)行了步驟3,此時(shí)mInstance已經(jīng)不為null了,所以線程B直接返回了mInstance。但其實(shí)我們知道,完整的初始化必須走完這三個(gè)步驟,由于線程A只走了兩個(gè)步驟,所以一定會(huì)報(bào)錯(cuò)的。

解決的辦法就是使用volatile修飾mInstance,我們知道volatile有兩個(gè)作用:保證可見性和禁止指令重排,在這里關(guān)鍵在于禁止指令重排,禁止指令重排后保證了不會(huì)發(fā)生上述問題。

靜態(tài)內(nèi)部類實(shí)現(xiàn)的單例:

class SingletonWithInnerClass {

    private SingletonWithInnerClass() {

    }

    private static class SingletonHolder{
        private static SingletonWithInnerClass INSTANCE=new SingletonWithInnerClass();
    }

    public SingletonWithInnerClass getInstance() {
        return SingletonHolder.INSTANCE;
    }

}

由于外部類的加載并不會(huì)導(dǎo)致內(nèi)部類立即加載,只有當(dāng)調(diào)用getInstance的時(shí)候才會(huì)加載內(nèi)部類,所以實(shí)現(xiàn)了延遲初始化。由于類只會(huì)被加載一次,并且類加載也是線程安全的,所以滿足我們所有的需求。靜態(tài)內(nèi)部類實(shí)現(xiàn)的單例也是最為推薦的一種方式。

Java中引用類型的區(qū)別,具體的使用場(chǎng)景

Java中引用類型分為四類:強(qiáng)引用、軟引用、弱引用、虛引用。

強(qiáng)引用:強(qiáng)引用指的是通過new對(duì)象創(chuàng)建的引用,垃圾回收器即使是內(nèi)存不足也不會(huì)回收強(qiáng)引用指向的對(duì)象。

軟引用:軟引用是通過SoftRefrence實(shí)現(xiàn)的,它的生命周期比強(qiáng)引用短,在內(nèi)存不足,拋出OOM之前,垃圾回收器會(huì)回收軟引用引用的對(duì)象。軟引用常見的使用場(chǎng)景是存儲(chǔ)一些內(nèi)存敏感的緩存,當(dāng)內(nèi)存不足時(shí)會(huì)被回收。

弱引用:弱引用是通過WeakRefrence實(shí)現(xiàn)的,它的生命周期比軟引用還短,GC只要掃描到弱引用的對(duì)象就會(huì)回收。弱引用常見的使用場(chǎng)景也是存儲(chǔ)一些內(nèi)存敏感的緩存。

虛引用:虛引用是通過FanttomRefrence實(shí)現(xiàn)的,它的生命周期最短,隨時(shí)可能被回收。如果一個(gè)對(duì)象只被虛引用引用,我們無法通過虛引用來訪問這個(gè)對(duì)象的任何屬性和方法。它的作用僅僅是保證對(duì)象在finalize后,做某些事情。虛引用常見的使用場(chǎng)景是跟蹤對(duì)象被垃圾回收的活動(dòng),當(dāng)一個(gè)虛引用關(guān)聯(lián)的對(duì)象被垃圾回收器回收之前會(huì)收到一條系統(tǒng)通知。

Exception和Error的區(qū)別

Exception和Error都繼承于Throwable,在Java中,只有Throwable類型的對(duì)象才能被throw或者catch,它是異常處理機(jī)制的基本組成類型。

Exception和Error體現(xiàn)了Java對(duì)不同異常情況的分類。Exception是程序正常運(yùn)行中,可以預(yù)料的意外情況,可能并且應(yīng)該被捕獲,進(jìn)行相應(yīng)的處理。

Error是指在正常情況下,不大可能出現(xiàn)的情況,絕大部分Error都會(huì)使程序處于非正常、不可恢復(fù)的狀態(tài)。既然是非正常,所以不便于也不需要捕獲,常見的OutOfMemoryError就是Error的子類。

Exception又分為checked Exception和unchecked Exception。checked Exception在代碼里必須顯式的進(jìn)行捕獲,這是編譯器檢查的一部分。unchecked Exception也就是運(yùn)行時(shí)異常,類似空指針異常、數(shù)組越界等,通常是可以避免的邏輯錯(cuò)誤,具體根據(jù)需求來判斷是否需要捕獲,并不會(huì)在編譯器強(qiáng)制要求。

volatile

一般提到volatile,就不得不提到內(nèi)存模型相關(guān)的概念。我們都知道,在程序運(yùn)行中,每條指令都是由CPU執(zhí)行的,而指令的執(zhí)行過程中,勢(shì)必涉及到數(shù)據(jù)的讀取和寫入。程序運(yùn)行中的數(shù)據(jù)都存放在主存中,這樣會(huì)有一個(gè)問題,由于CPU的執(zhí)行速度是要遠(yuǎn)高于主存的讀寫速度,所以直接從主存中讀寫數(shù)據(jù)會(huì)降低CPU的效率。為了解決這個(gè)問題,就有了高速緩存的概念,在每個(gè)CPU中都有高速緩存,它會(huì)事先從主存中讀取數(shù)據(jù),在CPU運(yùn)算之后在合適的時(shí)候刷新到主存中。

這樣的運(yùn)行模式在單線程中是沒有任何問題的,但在多線程中,會(huì)導(dǎo)致緩存一致性的問題。舉個(gè)簡單的例子:i=i+1 ,在兩個(gè)線程中執(zhí)行這句代碼,假設(shè)i的初始值為0。我們期望兩個(gè)線程運(yùn)行后得到2,那么有這樣的一種情況,兩個(gè)線程都從主存中讀取i到各自的高速緩存中,這時(shí)候兩個(gè)線程中的i都為0。在線程1執(zhí)行完畢得到i=1,將之刷新到主存后,線程2開始執(zhí)行,由于線程2中的i是高速緩存中的0,所以在執(zhí)行完線程2之后刷新到主存的i仍舊是1。

所以這就導(dǎo)致了對(duì)共享變量的緩存一致性的問題,那么為了解決這個(gè)問題,提出了緩存一致性協(xié)議:當(dāng)CPU在寫數(shù)據(jù)時(shí),如果發(fā)現(xiàn)操作的是共享變量,它會(huì)通知其他CPU將它們內(nèi)部的這個(gè)共享變量置為無效狀態(tài),當(dāng)其他CPU讀取緩存中的共享變量時(shí),發(fā)現(xiàn)這個(gè)變量是無效的,它會(huì)從新從主存中讀取最新的值。

在Java的多線程開發(fā)中,有三個(gè)重要概念:原子性、可見性、有序性。
原子性:一個(gè)或多個(gè)操作要么都不執(zhí)行,要么都執(zhí)行。
可見性:一個(gè)線程中對(duì)共享變量(類中的成員變量或靜態(tài)變量)的修改,在其他線程立即可見。
有序性:程序執(zhí)行的順序按照代碼的順序執(zhí)行。
把一個(gè)變量聲明為volatile,其實(shí)就是保證了可見性和有序性。
可見性我上面已經(jīng)說過了,在多線程開發(fā)中是很有必要的。這個(gè)有序性還是得說一下,為了執(zhí)行的效率,有時(shí)候會(huì)發(fā)生指令重排,這在單線程中指令重排之后的輸出與我們的代碼邏輯輸出還是一致的。但在多線程中就可能發(fā)生問題,volatile在一定程度上可以避免指令重排。

volatile的原理是在生成的匯編代碼中多了一個(gè)lock前綴指令,這個(gè)前綴指令相當(dāng)于一個(gè)內(nèi)存屏障,這個(gè)內(nèi)存屏障有3個(gè)作用:

  • 確保指令重排的時(shí)候不會(huì)把屏障后的指令排在屏障前,確保不會(huì)把屏障前的指令排在屏障后。
  • 修改緩存中的共享變量后立即刷新到主存中。
  • 當(dāng)執(zhí)行寫操作時(shí)會(huì)導(dǎo)致其他CPU中的緩存無效。

網(wǎng)絡(luò)相關(guān)面試題

http 狀態(tài)碼

http 與 https 的區(qū)別?https 是如何工作的?

http是超文本傳輸協(xié)議,而https可以簡單理解為安全的http協(xié)議。https通過在http協(xié)議下添加了一層ssl協(xié)議對(duì)數(shù)據(jù)進(jìn)行加密從而保證了安全。https的作用主要有兩點(diǎn):建立安全的信息傳輸通道,保證數(shù)據(jù)傳輸安全;確認(rèn)網(wǎng)站的真實(shí)性。

http與https的區(qū)別主要如下:

  • https需要到CA申請(qǐng)證書,很少免費(fèi),因而需要一定的費(fèi)用
  • http是明文傳輸,安全性低;而https在http的基礎(chǔ)上通過ssl加密,安全性高
  • 二者的默認(rèn)端口不一樣,http使用的默認(rèn)端口是80;https使用的默認(rèn)端口是443

https的工作流程

提到https的話首先要說到加密算法,加密算法分為兩類:對(duì)稱加密和非對(duì)稱加密。

對(duì)稱加密:加密和解密用的都是相同的秘鑰,優(yōu)點(diǎn)是速度快,缺點(diǎn)是安全性低。常見的對(duì)稱加密算法有DES、AES等等。

非對(duì)稱加密:非對(duì)稱加密有一個(gè)秘鑰對(duì),分為公鑰和私鑰。一般來說,私鑰自己持有,公鑰可以公開給對(duì)方,優(yōu)點(diǎn)是安全性比對(duì)稱加密高,缺點(diǎn)是數(shù)據(jù)傳輸效率比對(duì)稱加密低。采用公鑰加密的信息只有對(duì)應(yīng)的私鑰可以解密。常見的非對(duì)稱加密包括RSA等。

在正式的使用場(chǎng)景中一般都是對(duì)稱加密和非對(duì)稱加密結(jié)合使用,使用非對(duì)稱加密完成秘鑰的傳遞,然后使用對(duì)稱秘鑰進(jìn)行數(shù)據(jù)加密和解密。二者結(jié)合既保證了安全性,又提高了數(shù)據(jù)傳輸效率。

https的具體流程如下:

  1. 客戶端(通常是瀏覽器)先向服務(wù)器發(fā)出加密通信的請(qǐng)求
    • 支持的協(xié)議版本,比如TLS 1.0版
    • 一個(gè)客戶端生成的隨機(jī)數(shù) random1,稍后用于生成"對(duì)話密鑰"
    • 支持的加密方法,比如RSA公鑰加密
    • 支持的壓縮方法
  2. 服務(wù)器收到請(qǐng)求,然后響應(yīng)
    • 確認(rèn)使用的加密通信協(xié)議版本,比如TLS 1.0版本。如果瀏覽器與服務(wù)器支持的版本不一致,服務(wù)器關(guān)閉加密通信
    • 一個(gè)服務(wù)器生成的隨機(jī)數(shù)random2,稍后用于生成"對(duì)話密鑰"
    • 確認(rèn)使用的加密方法,比如RSA公鑰加密
    • 服務(wù)器證書
  3. 客戶端收到證書之后會(huì)首先會(huì)進(jìn)行驗(yàn)證
    • 首先驗(yàn)證證書的安全性
    • 驗(yàn)證通過之后,客戶端會(huì)生成一個(gè)隨機(jī)數(shù)pre-master secret,然后使用證書中的公鑰進(jìn)行加密,然后傳遞給服務(wù)器端
  4. 服務(wù)器收到使用公鑰加密的內(nèi)容,在服務(wù)器端使用私鑰解密之后獲得隨機(jī)數(shù)pre-master secret,然后根據(jù)radom1、radom2、pre-master secret通過一定的算法得出一個(gè)對(duì)稱加密的秘鑰,作為后面交互過程中使用對(duì)稱秘鑰。同時(shí)客戶端也會(huì)使用radom1、radom2、pre-master secret,和同樣的算法生成對(duì)稱秘鑰。
  5. 然后再后續(xù)的交互中就使用上一步生成的對(duì)稱秘鑰對(duì)傳輸?shù)膬?nèi)容進(jìn)行加密和解密。

TCP三次握手流程

Android面試題

進(jìn)程間通信的方式有哪幾種

AIDL 、廣播、文件、socket、管道

廣播靜態(tài)注冊(cè)和動(dòng)態(tài)注冊(cè)的區(qū)別

  1. 動(dòng)態(tài)注冊(cè)廣播不是常駐型廣播,也就是說廣播跟隨Activity的生命周期。注意在Activity結(jié)束前,移除廣播接收器。 靜態(tài)注冊(cè)是常駐型,也就是說當(dāng)應(yīng)用程序關(guān)閉后,如果有信息廣播來,程序也會(huì)被系統(tǒng)調(diào)用自動(dòng)運(yùn)行。
  2. 當(dāng)廣播為有序廣播時(shí):優(yōu)先級(jí)高的先接收(不分靜態(tài)和動(dòng)態(tài))。同優(yōu)先級(jí)的廣播接收器,動(dòng)態(tài)優(yōu)先于靜態(tài)
  3. 同優(yōu)先級(jí)的同類廣播接收器,靜態(tài):先掃描的優(yōu)先于后掃描的,動(dòng)態(tài):先注冊(cè)的優(yōu)先于后注冊(cè)的。
  4. 當(dāng)廣播為默認(rèn)廣播時(shí):無視優(yōu)先級(jí),動(dòng)態(tài)廣播接收器優(yōu)先于靜態(tài)廣播接收器。同優(yōu)先級(jí)的同類廣播接收器,靜態(tài):先掃描的優(yōu)先于后掃描的,動(dòng)態(tài):先注冊(cè)的優(yōu)先于后冊(cè)的。

Android性能優(yōu)化工具使用(這個(gè)問題建議配合Android中的性能優(yōu)化)

Android中常用的性能優(yōu)化工具包括這些:Android Studio自帶的Android Profiler、LeakCanary、BlockCanary

Android自帶的Android Profiler其實(shí)就很好用,Android Profiler可以檢測(cè)三個(gè)方面的性能問題:CPU、MEMORY、NETWORK。

LeakCanary是一個(gè)第三方的檢測(cè)內(nèi)存泄漏的庫,我們的項(xiàng)目集成之后LeakCanary會(huì)自動(dòng)檢測(cè)應(yīng)用運(yùn)行期間的內(nèi)存泄漏,并將之輸出給我們。

BlockCanary也是一個(gè)第三方檢測(cè)UI卡頓的庫,項(xiàng)目集成后Block也會(huì)自動(dòng)檢測(cè)應(yīng)用運(yùn)行期間的UI卡頓,并將之輸出給我們。

Android中的類加載器

PathClassLoader,只能加載系統(tǒng)中已經(jīng)安裝過的apk
DexClassLoader,可以加載jar/apk/dex,可以從SD卡中加載未安裝的apk

BootClassLoader(Java的BootStrap ClassLoader)
用于加載Android Framework層class文件。
PathClassLoader(Java的App ClassLoader)
用于加載已經(jīng)安裝到系統(tǒng)中的apk中的class文件。
DexClassLoader(Java的Custom ClassLoader)
用于加載指定目錄中的class文件。
BaseDexClassLoader
是PathClassLoader和DexClassLoader的父類。
因?yàn)樽裱p親委派模型,Android中的ClassLoader具有兩個(gè)特點(diǎn):

類加載共享
當(dāng)一個(gè)class文件被任何一個(gè)ClassLoader加載過,就不會(huì)再被其他ClassLoader加載。
類加載隔離
不同ClassLoader加載的class文件肯定不是一個(gè)。舉個(gè)栗子,一些系統(tǒng)層級(jí)的class文件在系統(tǒng)初始化的時(shí)候被加載,比如java.net.String,這個(gè)是在應(yīng)用啟動(dòng)前就被系統(tǒng)加載好的。如果在一個(gè)應(yīng)用里能簡單地用一個(gè)自定義的String類把這個(gè)String類替換掉的話,將有嚴(yán)重的安全問題。

Android中的動(dòng)畫有哪幾類,它們的特點(diǎn)和區(qū)別是什么

Android中動(dòng)畫大致分為3類:幀動(dòng)畫、補(bǔ)間動(dòng)畫(View Animation)、屬性動(dòng)畫(Object Animation)。

  • 幀動(dòng)畫:通過xml配置一組圖片,動(dòng)態(tài)播放。很少會(huì)使用。
  • 補(bǔ)間動(dòng)畫(View Animation):大致分為旋轉(zhuǎn)、透明、縮放、位移四類操作。很少會(huì)使用。
  • 屬性動(dòng)畫(Object Animation):屬性動(dòng)畫是現(xiàn)在使用的最多的一種動(dòng)畫,它比補(bǔ)間動(dòng)畫更加強(qiáng)大。屬性動(dòng)畫大致分為兩種使用類型,分別是ViewPropertyAnimator和ObjectAnimator。前者適合一些通用的動(dòng)畫,比如旋轉(zhuǎn)、位移、縮放和透明,使用方式也很簡單通過View.animate()即可得到ViewPropertyAnimator,之后進(jìn)行相應(yīng)的動(dòng)畫操作即可。后者適合用于為我們的自定義控件添加動(dòng)畫,當(dāng)然首先我們應(yīng)該在自定義View中添加相應(yīng)的getXXX()和setXXX()相應(yīng)屬性的getter和setter方法,這里需要注意的是在setter方法內(nèi)改變了自定義View中的屬性后要調(diào)用invalidate()來刷新View的繪制。之后調(diào)用ObjectAnimator.of屬性類型()返回一個(gè)ObjectAnimator,調(diào)用start()方法啟動(dòng)動(dòng)畫即可。

補(bǔ)間動(dòng)畫與屬性動(dòng)畫的區(qū)別:

  • 補(bǔ)間動(dòng)畫是父容器不斷的繪制view,看起來像移動(dòng)了效果,其實(shí)view沒有變化,還在原地。
  • 是通過不斷改變view內(nèi)部的屬性值,真正的改變view。

Handler機(jī)制

說到Handler,就不得不提與之密切相關(guān)的這幾個(gè)類:Message、MessageQueue,Looper。

  • Message。Message中有兩個(gè)成員變量值得關(guān)注:target和callback。target其實(shí)就是發(fā)送消息的Handler對(duì)象,callback是當(dāng)調(diào)用handler.post(runnable)時(shí)傳入的Runnable類型的任務(wù)。post事件的本質(zhì)也是創(chuàng)建了一個(gè)Message,將我們傳入的這個(gè)runnable賦值給創(chuàng)建的Message的callback這個(gè)成員變量。
  • MessageQueue。消息隊(duì)列很明顯是存放消息的隊(duì)列,值得關(guān)注的是MessageQueue中的next()方法,它會(huì)返回下一個(gè)待處理的消息。
  • Looper。Looper消息輪詢器其實(shí)是連接Handler和消息隊(duì)列的核心。首先我們都知道,如果想要在一個(gè)線程中創(chuàng)建一個(gè)Handler,首先要通過Looper.prepare()創(chuàng)建Looper,之后還得調(diào)用Looper.loop()開啟輪詢。我們著重看一下這兩個(gè)方法。

prepare()。這個(gè)方法做了兩件事:首先通過ThreadLocal.get()獲取當(dāng)前線程中的Looper,如果不為空,則會(huì)拋出一個(gè)RunTimeException,意思是一個(gè)線程不能創(chuàng)建2個(gè)Looper。如果為null則執(zhí)行下一步。第二步是創(chuàng)建了一個(gè)Looper,并通過ThreadLocal.set(looper)。將我們創(chuàng)建的Looper與當(dāng)前線程綁定。這里需要提一下的是消息隊(duì)列的創(chuàng)建其實(shí)就發(fā)生在Looper的構(gòu)造方法中。

loop()。這個(gè)方法開啟了整個(gè)事件機(jī)制的輪詢。它的本質(zhì)是開啟了一個(gè)死循環(huán),不斷的通過MessageQueue的next()方法獲取消息。拿到消息后會(huì)調(diào)用msg.target.dispatchMessage()來做處理。其實(shí)我們?cè)谡f到Message的時(shí)候提到過,msg.target其實(shí)就是發(fā)送這個(gè)消息的handler。這句代碼的本質(zhì)就是調(diào)用handler的dispatchMessage()。

  • Handler。上面做了這么多鋪墊,終于到了最重要的部分。Handler的分析著重在兩個(gè)部分:發(fā)送消息和處理消息。

發(fā)送消息。其實(shí)發(fā)送消息除了sendMessage之外還有sendMessageDelayed和post以及postDelayed等等不同的方式。但它們的本質(zhì)都是調(diào)用了sendMessageAtTime。在sendMessageAtTime這個(gè)方法中調(diào)用了enqueueMessage。在enqueueMessage這個(gè)方法中做了兩件事:通過msg.target = this實(shí)現(xiàn)了消息與當(dāng)前handler的綁定。然后通過queue.enqueueMessage實(shí)現(xiàn)了消息入隊(duì)。

處理消息。消息處理的核心其實(shí)就是dispatchMessage()這個(gè)方法。這個(gè)方法里面的邏輯很簡單,先判斷msg.callback是否為null,如果不為空則執(zhí)行這個(gè)runnable。如果為空則會(huì)執(zhí)行我們的handleMessage方法。

Android性能優(yōu)化

Android中的性能優(yōu)化在我看來分為以下幾個(gè)方面:內(nèi)存優(yōu)化、布局優(yōu)化、網(wǎng)絡(luò)優(yōu)化、安裝包優(yōu)化。

內(nèi)存優(yōu)化:下一個(gè)問題就是。

布局優(yōu)化:布局優(yōu)化的本質(zhì)就是減少View的層級(jí)。常見的布局優(yōu)化方案如下

  • 在LinearLayout和RelativeLayout都可以完成布局的情況下優(yōu)先選擇RelativeLayout,可以減少View的層級(jí)
  • 將常用的布局組件抽取出來使用 < include > 標(biāo)簽
  • 通過 < ViewStub > 標(biāo)簽來加載不常用的布局
  • 使用 < Merge > 標(biāo)簽來減少布局的嵌套層次

網(wǎng)絡(luò)優(yōu)化:常見的網(wǎng)絡(luò)優(yōu)化方案如下

  • 盡量減少網(wǎng)絡(luò)請(qǐng)求,能夠合并的就盡量合并
  • 避免DNS解析,根據(jù)域名查詢可能會(huì)耗費(fèi)上百毫秒的時(shí)間,也可能存在DNS劫持的風(fēng)險(xiǎn)。可以根據(jù)業(yè)務(wù)需求采用增加動(dòng)態(tài)更新IP的方式,或者在IP方式訪問失敗時(shí)切換到域名訪問方式。
  • 大量數(shù)據(jù)的加載采用分頁的方式
  • 網(wǎng)絡(luò)數(shù)據(jù)傳輸采用GZIP壓縮
  • 加入網(wǎng)絡(luò)數(shù)據(jù)的緩存,避免頻繁請(qǐng)求網(wǎng)絡(luò)
  • 上傳圖片時(shí),在必要的時(shí)候壓縮圖片

安裝包優(yōu)化:安裝包優(yōu)化的核心就是減少apk的體積,常見的方案如下

  • 使用混淆,可以在一定程度上減少apk體積,但實(shí)際效果微乎其微
  • 減少應(yīng)用中不必要的資源文件,比如圖片,在不影響APP效果的情況下盡量壓縮圖片,有一定的效果
  • 在使用了SO庫的時(shí)候優(yōu)先保留v7版本的SO庫,刪掉其他版本的SO庫。原因是在2018年,v7版本的SO庫可以滿足市面上絕大多數(shù)的要求,可能八九年前的手機(jī)滿足不了,但我們也沒必要去適配老掉牙的手機(jī)。實(shí)際開發(fā)中減少apk體積的效果是十分顯著的,如果你使用了很多SO庫,比方說一個(gè)版本的SO庫一共10M,那么只保留v7版本,刪掉armeabi和v8版本的SO庫,一共可以減少20M的體積。

Android內(nèi)存優(yōu)化

Android的內(nèi)存優(yōu)化在我看來分為兩點(diǎn):避免內(nèi)存泄漏、擴(kuò)大內(nèi)存,其實(shí)就是開源節(jié)流。

其實(shí)內(nèi)存泄漏的本質(zhì)就是較長生命周期的對(duì)象引用了較短生命周期的對(duì)象。

常見的內(nèi)存泄漏:

  • 單例模式導(dǎo)致的內(nèi)存泄漏。最常見的例子就是創(chuàng)建這個(gè)單例對(duì)象需要傳入一個(gè)Context,這時(shí)候傳入了一個(gè)Activity類型的Context,由于單例對(duì)象的靜態(tài)屬性,導(dǎo)致它的生命周期是從單例類加載到應(yīng)用程序結(jié)束為止,所以即使已經(jīng)finish掉了傳入的Activity,由于我們的單例對(duì)象依然持有Activity的引用,所以導(dǎo)致了內(nèi)存泄漏。解決辦法也很簡單,不要使用Activity類型的Context,使用Application類型的Context可以避免內(nèi)存泄漏。
  • 靜態(tài)變量導(dǎo)致的內(nèi)存泄漏。靜態(tài)變量是放在方法區(qū)中的,它的生命周期是從類加載到程序結(jié)束,可以看到靜態(tài)變量生命周期是非常久的。最常見的因靜態(tài)變量導(dǎo)致內(nèi)存泄漏的例子是我們?cè)贏ctivity中創(chuàng)建了一個(gè)靜態(tài)變量,而這個(gè)靜態(tài)變量的創(chuàng)建需要傳入Activity的引用this。在這種情況下即使Activity調(diào)用了finish也會(huì)導(dǎo)致內(nèi)存泄漏。原因就是因?yàn)檫@個(gè)靜態(tài)變量的生命周期幾乎和整個(gè)應(yīng)用程序的生命周期一致,它一直持有Activity的引用,從而導(dǎo)致了內(nèi)存泄漏。
  • 非靜態(tài)內(nèi)部類導(dǎo)致的內(nèi)存泄漏。非靜態(tài)內(nèi)部類導(dǎo)致內(nèi)存泄漏的原因是非靜態(tài)內(nèi)部類持有外部類的引用,最常見的例子就是在Activity中使用Handler和Thread了。使用非靜態(tài)內(nèi)部類創(chuàng)建的Handler和Thread在執(zhí)行延時(shí)操作的時(shí)候會(huì)一直持有當(dāng)前Activity的引用,如果在執(zhí)行延時(shí)操作的時(shí)候就結(jié)束Activity,這樣就會(huì)導(dǎo)致內(nèi)存泄漏。解決辦法有兩種:第一種是使用靜態(tài)內(nèi)部類,在靜態(tài)內(nèi)部類中使用弱引用調(diào)用Activity。第二種方法是在Activity的onDestroy中調(diào)用handler.removeCallbacksAndMessages來取消延時(shí)事件。
  • 使用資源未及時(shí)關(guān)閉導(dǎo)致的內(nèi)存泄漏。常見的例子有:操作各種數(shù)據(jù)流未及時(shí)關(guān)閉,操作Bitmap未及時(shí)recycle等等。
  • 使用第三方庫未能及時(shí)解綁。有的三方庫提供了注冊(cè)和解綁的功能,最常見的就是EventBus了,我們都知道使用EventBus要在onCreate中注冊(cè),在onDestroy中解綁。如果沒有解綁的話,EventBus其實(shí)是一個(gè)單例模式,他會(huì)一直持有Activity的引用,導(dǎo)致內(nèi)存泄漏。同樣常見的還有RxJava,在使用Timer操作符做了一些延時(shí)操作后也要注意在onDestroy方法中調(diào)用disposable.dispose()來取消操作。
  • 屬性動(dòng)畫導(dǎo)致的內(nèi)存泄漏。常見的例子就是在屬性動(dòng)畫執(zhí)行的過程中退出了Activity,這時(shí)View對(duì)象依然持有Activity的引用從而導(dǎo)致了內(nèi)存泄漏。解決辦法就是在onDestroy中調(diào)用動(dòng)畫的cancel方法取消屬性動(dòng)畫。
  • WebView導(dǎo)致的內(nèi)存泄漏。WebView比較特殊,即使是調(diào)用了它的destroy方法,依然會(huì)導(dǎo)致內(nèi)存泄漏。其實(shí)避免WebView導(dǎo)致內(nèi)存泄漏的最好方法就是讓W(xué)ebView所在的Activity處于另一個(gè)進(jìn)程中,當(dāng)這個(gè)Activity結(jié)束時(shí)殺死當(dāng)前WebView所處的進(jìn)程即可,我記得阿里釘釘?shù)腤ebView就是另外開啟的一個(gè)進(jìn)程,應(yīng)該也是采用這種方法避免內(nèi)存泄漏。

擴(kuò)大內(nèi)存,為什么要擴(kuò)大我們的內(nèi)存呢?有時(shí)候我們實(shí)際開發(fā)中不可避免的要使用很多第三方商業(yè)的SDK,這些SDK其實(shí)有好有壞,大廠的SDK可能內(nèi)存泄漏會(huì)少一些,但一些小廠的SDK質(zhì)量也就不太靠譜一些。那應(yīng)對(duì)這種我們無法改變的情況,最好的辦法就是擴(kuò)大內(nèi)存。

擴(kuò)大內(nèi)存通常有兩種方法:一個(gè)是在清單文件中的Application下添加largeHeap="true"這個(gè)屬性,另一個(gè)就是同一個(gè)應(yīng)用開啟多個(gè)進(jìn)程來擴(kuò)大一個(gè)應(yīng)用的總內(nèi)存空間。第二種方法其實(shí)就很常見了,比方說我使用過個(gè)推的SDK,個(gè)推的Service其實(shí)就是處在另外一個(gè)單獨(dú)的進(jìn)程中。

Android中的內(nèi)存優(yōu)化總的來說就是開源和節(jié)流,開源就是擴(kuò)大內(nèi)存,節(jié)流就是避免內(nèi)存泄漏。

Binder機(jī)制

在Linux中,為了避免一個(gè)進(jìn)程對(duì)其他進(jìn)程的干擾,進(jìn)程之間是相互獨(dú)立的。在一個(gè)進(jìn)程中其實(shí)還分為用戶空間和內(nèi)核空間。這里的隔離分為兩個(gè)部分,進(jìn)程間的隔離和進(jìn)程內(nèi)的隔離。

既然進(jìn)程間存在隔離,那其實(shí)也是存在著交互。進(jìn)程間通信就是IPC,用戶空間和內(nèi)核空間的通信就是系統(tǒng)調(diào)用。

Linux為了保證獨(dú)立性和安全性,進(jìn)程之間不能直接相互訪問,Android是基于Linux的,所以也是需要解決進(jìn)程間通信的問題。

其實(shí)Linux進(jìn)程間通信有很多方式,比如管道、socket等等。為什么Android進(jìn)程間通信采用了Binder而不是Linux已有的方式,主要是有這么兩點(diǎn)考慮:性能和安全

性能。在移動(dòng)設(shè)備上對(duì)性能要求是比較嚴(yán)苛的。Linux傳統(tǒng)的進(jìn)程間通信比如管道、socket等等進(jìn)程間通信是需要復(fù)制兩次數(shù)據(jù),而Binder則只需要一次。所以Binder在性能上是優(yōu)于傳統(tǒng)進(jìn)程通信的。

安全。傳統(tǒng)的Linux進(jìn)程通信是不包含通信雙方的身份驗(yàn)證的,這樣會(huì)導(dǎo)致一些安全性問題。而Binder機(jī)制自帶身份驗(yàn)證,從而有效的提高了安全性。

Binder是基于CS架構(gòu)的,有四個(gè)主要組成部分。

  • Client??蛻舳诉M(jìn)程。
  • Server。服務(wù)端進(jìn)程。
  • ServiceManager。提供注冊(cè)、查詢和返回代理服務(wù)對(duì)象的功能。
  • Binder驅(qū)動(dòng)。主要負(fù)責(zé)建立進(jìn)程間的Binder連接,進(jìn)程間的數(shù)據(jù)交互等等底層操作。

Binder機(jī)制主要的流程是這樣的:

  • 服務(wù)端通過Binder驅(qū)動(dòng)在ServiceManager中注冊(cè)我們的服務(wù)。
  • 客戶端通過Binder驅(qū)動(dòng)查詢?cè)赟erviceManager中注冊(cè)的服務(wù)。
  • ServiceManager通過Binder驅(qū)動(dòng)返回服務(wù)端的代理對(duì)象。
  • 客戶端拿到服務(wù)端的代理對(duì)象后即可進(jìn)行進(jìn)程間通信。

LruCache的原理

LruCache的核心原理就是對(duì)LinkedHashMap的有效利用,它的內(nèi)部存在一個(gè)LinkedHashMap成員變量。值得我們關(guān)注的有四個(gè)方法:構(gòu)造方法、get、put、trimToSize。

構(gòu)造方法:在LruCache的構(gòu)造方法中做了兩件事,設(shè)置了maxSize、創(chuàng)建了一個(gè)LinkedHashMap。這里值得注意的是LruCache將LinkedHashMap的accessOrder設(shè)置為了true,accessOrder就是遍歷這個(gè)LinkedHashMap的輸出順序。true代表按照訪問順序輸出,false代表按添加順序輸出,因?yàn)橥ǔ6际前凑仗砑禹樞蜉敵觯詀ccessOrder這個(gè)屬性默認(rèn)是false,但我們的LruCache需要按訪問順序輸出,所以顯式的將accessOrder設(shè)置為true。

get方法:本質(zhì)上是調(diào)用LinkedHashMap的get方法,由于我們將accessOrder設(shè)置為了true,所以每調(diào)用一次get方法,就會(huì)將我們?cè)L問的當(dāng)前元素放置到這個(gè)LinkedHashMap的尾部。

put方法:本質(zhì)上也是調(diào)用了LinkedHashMap的put方法,由于LinkedHashMap的特性,每調(diào)用一次put方法,也會(huì)將新加入的元素放置到LinkedHashMap的尾部。添加之后會(huì)調(diào)用trimToSize方法來保證添加后的內(nèi)存不超過maxSize。

trimToSize方法:trimToSize方法的內(nèi)部其實(shí)是開啟了一個(gè)while(true)的死循環(huán),不斷的從LinkedHashMap的首部刪除元素,直到刪除之后的內(nèi)存小于maxSize之后使用break跳出循環(huán)。

其實(shí)到這里我們可以總結(jié)一下,為什么這個(gè)算法叫 最近最少使用 算法呢?原理很簡單,我們的每次put或者get都可以看做一次訪問,由于LinkedHashMap的特性,會(huì)將每次訪問到的元素放置到尾部。當(dāng)我們的內(nèi)存達(dá)到閾值后,會(huì)觸發(fā)trimToSize方法來刪除LinkedHashMap首部的元素,直到當(dāng)前內(nèi)存小于maxSize。為什么刪除首部的元素,原因很明顯:我們最近經(jīng)常訪問的元素都會(huì)放置到尾部,那首部的元素肯定就是 最近最少使用 的元素了,因此當(dāng)內(nèi)存不足時(shí)應(yīng)當(dāng)優(yōu)先刪除這些元素。

DiskLruCache原理

設(shè)計(jì)一個(gè)圖片的異步加載框架

設(shè)計(jì)一個(gè)圖片加載框架,肯定要用到圖片加載的三級(jí)緩存的思想。三級(jí)緩存分為內(nèi)存緩存、本地緩存和網(wǎng)絡(luò)緩存。

內(nèi)存緩存:將Bitmap緩存到內(nèi)存中,運(yùn)行速度快,但是內(nèi)存容量小。
本地緩存:將圖片緩存到文件中,速度較慢,但容量較大。
網(wǎng)絡(luò)緩存:從網(wǎng)絡(luò)獲取圖片,速度受網(wǎng)絡(luò)影響。

如果我們?cè)O(shè)計(jì)一個(gè)圖片加載框架,流程一定是這樣的:

  • 拿到圖片url后首先從內(nèi)存中查找BItmap,如果找到直接加載。
  • 內(nèi)存中沒有找到,會(huì)從本地緩存中查找,如果本地緩存可以找到,則直接加載。
  • 內(nèi)存和本地都沒有找到,這時(shí)會(huì)從網(wǎng)絡(luò)下載圖片,下載到后會(huì)加載圖片,并且將下載到的圖片放到內(nèi)存緩存和本地緩存中。

上面是一些基本的概念,如果是具體的代碼實(shí)現(xiàn)的話,大概需要這么幾個(gè)方面的文件:

  • 首先需要確定我們的內(nèi)存緩存,這里一般用的都是LruCache。
  • 確定本地緩存,通常用的是DiskLruCache,這里需要注意的是圖片緩存的文件名一般是url被MD5加密后的字符串,為了避免文件名直接暴露圖片的url。
  • 內(nèi)存緩存和本地緩存確定之后,需要我們創(chuàng)建一個(gè)新的類MemeryAndDiskCache,當(dāng)然,名字隨便起,這個(gè)類包含了之前提到的LruCache和DiskLruCache。在MemeryAndDiskCache這個(gè)類中我們定義兩個(gè)方法,一個(gè)是getBitmap,另一個(gè)是putBitmap,對(duì)應(yīng)著圖片的獲取和緩存,內(nèi)部的邏輯也很簡單。getBitmap中按內(nèi)存、本地的優(yōu)先級(jí)去取BItmap,putBitmap中先緩存內(nèi)存,之后緩存到本地。
  • 在緩存策略類確定好之后,我們創(chuàng)建一個(gè)ImageLoader類,這個(gè)類必須包含兩個(gè)方法,一個(gè)是展示圖片displayImage(url,imageView),另一個(gè)是從網(wǎng)絡(luò)獲取圖片downloadImage(url,imageView)。在展示圖片方法中首先要通過ImageView.setTag(url),將url和imageView進(jìn)行綁定,這是為了避免在列表中加載網(wǎng)絡(luò)圖片時(shí)會(huì)由于ImageView的復(fù)用導(dǎo)致的圖片錯(cuò)位的bug。之后會(huì)從MemeryAndDiskCache中獲取緩存,如果存在,直接加載;如果不存在,則調(diào)用從網(wǎng)絡(luò)獲取圖片這個(gè)方法。從網(wǎng)絡(luò)獲取圖片方法很多,這里我一般都會(huì)使用OkHttp+Retrofit。當(dāng)從網(wǎng)絡(luò)中獲取到圖片之后,首先判斷一下imageView.getTag()與圖片的url是否一致,如果一致則加載圖片,如果不一致則不加載圖片,通過這樣的方式避免了列表中異步加載圖片的錯(cuò)位。同時(shí)在獲取到圖片之后會(huì)通過MemeryAndDiskCache來緩存圖片。

Android中的事件分發(fā)機(jī)制

在我們的手指觸摸到屏幕的時(shí)候,事件其實(shí)是通過 Activity -> ViewGroup -> View 這樣的流程到達(dá)最后響應(yīng)我們觸摸事件的View。

說到事件分發(fā),必不可少的是這幾個(gè)方法:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent。接下來就按照 Activity -> ViewGroup -> View 的流程來大致說一下事件分發(fā)機(jī)制。

我們的手指觸摸到屏幕的時(shí)候,會(huì)觸發(fā)一個(gè)Action_Down類型的事件,當(dāng)前頁面的Activity會(huì)首先做出響應(yīng),也就是說會(huì)走到Activity的dispatchTouchEvent()方法內(nèi)。在這個(gè)方法內(nèi)部簡單來說是這么一個(gè)邏輯:

  • 調(diào)用getWindow.superDispatchTouchEvent()。
  • 如果上一步返回true,直接返回true;否則就return自己的onTouchEvent()。

這個(gè)邏輯很好理解,getWindow().superDispatchTouchEvent()如果返回true代表當(dāng)前事件已經(jīng)被處理,無需調(diào)用自己的onTouchEvent;否則代表事件并沒有被處理,需要Activity自己處理,也就是調(diào)用自己的onTouchEvent。

getWindow()方法返回了一個(gè)Window類型的對(duì)象,這個(gè)我們都知道,在Android中,PhoneWindow是Window的唯一實(shí)現(xiàn)類。所以這句本質(zhì)上是調(diào)用了PhoneWindow中的superDispatchTouchEvent()。

而在PhoneWindow的這個(gè)方法中實(shí)際調(diào)用了mDecor.superDispatchTouchEvent(event)。這個(gè)mDecor就是DecorView,它是FrameLayout的一個(gè)子類,在DecorView中的superDispatchTouchEvent()中調(diào)用的是super.dispatchTouchEvent()。到這里就很明顯了,DecorView是一個(gè)FrameLayout的子類,F(xiàn)rameLayout是一個(gè)ViewGroup的子類,本質(zhì)上調(diào)用的還是ViewGroup的dispatchTouchEvent()。

分析到這里,我們的事件已經(jīng)從Activity傳遞到了ViewGroup,接下來我們來分析下ViewGroup中的這幾個(gè)事件處理方法。

在ViewGroup中的dispatchTouchEvent()中的邏輯大致如下:

  • 通過onInterceptTouchEvent()判斷當(dāng)前ViewGroup是否攔截事件,默認(rèn)的ViewGroup都是不攔截的;
  • 如果攔截,則return自己的onTouchEvent();
  • 如果不攔截,則根據(jù) child.dispatchTouchEvent()的返回值判斷。如果返回true,則return true;否則return自己的onTouchEvent(),在這里實(shí)現(xiàn)了未處理事件的向上傳遞。

通常情況下ViewGroup的onInterceptTouchEvent()都返回false,也就是不攔截。這里需要注意的是事件序列,比如Down事件、Move事件......Up事件,從Down到Up是一個(gè)完整的事件序列,對(duì)應(yīng)著手指從按下到抬起這一系列的事件,如果ViewGroup攔截了Down事件,那么后續(xù)事件都會(huì)交給這個(gè)ViewGroup的onTouchEvent。如果ViewGroup攔截的不是Down事件,那么會(huì)給之前處理這個(gè)Down事件的View發(fā)送一個(gè)Action_Cancel類型的事件,通知子View這個(gè)后續(xù)的事件序列已經(jīng)被ViewGroup接管了,子View恢復(fù)之前的狀態(tài)即可。

這里舉一個(gè)常見的例子:在一個(gè)Recyclerview鐘有很多的Button,我們首先按下了一個(gè)button,然后滑動(dòng)一段距離再松開,這時(shí)候Recyclerview會(huì)跟著滑動(dòng),并不會(huì)觸發(fā)這個(gè)button的點(diǎn)擊事件。這個(gè)例子中,當(dāng)我們按下button時(shí),這個(gè)button接收到了Action_Down事件,正常情況下后續(xù)的事件序列應(yīng)該由這個(gè)button處理。但我們滑動(dòng)了一段距離,這時(shí)Recyclerview察覺到這是一個(gè)滑動(dòng)操作,攔截了這個(gè)事件序列,走了自身的onTouchEvent()方法,反映在屏幕上就是列表的滑動(dòng)。而這時(shí)button仍然處于按下的狀態(tài),所以在攔截的時(shí)候需要發(fā)送一個(gè)Action_Cancel來通知button恢復(fù)之前狀態(tài)。

事件分發(fā)最終會(huì)走到View的dispatchTouchEvent()中。在View的dispatchTouchEvent()中沒有onInterceptTouchEvent(),這也很容易理解,View不是ViewGroup,不會(huì)包含其他子View,所以也不存在攔截不攔截這一說。忽略一些細(xì)節(jié),View的dispatchTouchEvent()中直接return了自己的onTouchEvent()。如果onTouchEvent()返回true代表事件被處理,否則未處理的事件會(huì)向上傳遞,直到有View處理了事件或者一直沒有處理,最終到達(dá)了Activity的onTouchEvent()終止。

這里經(jīng)常有人問onTouch和onTouchEvent的區(qū)別。首先,這兩個(gè)方法都在View的dispatchTouchEvent()中,是這么一個(gè)邏輯:

  • 如果touchListener不為null,并且這個(gè)View是enable的,而且onTouch返回的是true,滿足這三個(gè)條件時(shí)會(huì)直接return true,不會(huì)走onTouchEvent()方法。
  • 上面只要有一個(gè)條件不滿足,就會(huì)走到onTouchEvent()方法中。所以onTouch的順序是在onTouchEvent之前的。

View的繪制流程

視圖繪制的起點(diǎn)在ViewRootImpl類的performTraversals()方法,在這個(gè)方法內(nèi)其實(shí)是按照順序依次調(diào)用了mView.measure()、mView.layout()、mView.draw()

View的繪制流程分為3步:測(cè)量、布局、繪制,分別對(duì)應(yīng)3個(gè)方法measure、layout、draw。

測(cè)量階段。measure方法會(huì)被父View調(diào)用,在measure方法中做一些優(yōu)化和準(zhǔn)備工作后會(huì)調(diào)用onMeasure方法進(jìn)行實(shí)際的自我測(cè)量。onMeasure方法在View和ViewGroup做的事情是不一樣的:

  • View。View中的onMeasure方法會(huì)計(jì)算自己的尺寸并通過setMeasureDimension保存。
  • ViewGroup。ViewGroup中的onMeasure方法會(huì)調(diào)用所有子View的measure方法進(jìn)行自我測(cè)量并保存。然后通過子View的尺寸和位置計(jì)算出自己的尺寸并保存。

布局階段。layout方法會(huì)被父View調(diào)用,layout方法會(huì)保存父View傳進(jìn)來的尺寸和位置,并調(diào)用onLayout進(jìn)行實(shí)際的內(nèi)部布局。onLayout在View和ViewGroup中做的事情也是不一樣的:

  • View。因?yàn)閂iew是沒有子View的,所以View的onLayout里面什么都不做。
  • ViewGroup。ViewGroup中的onLayout方法會(huì)調(diào)用所有子View的layout方法,把尺寸和位置傳給他們,讓他們完成自我的內(nèi)部布局。

繪制階段。draw方法會(huì)做一些調(diào)度工作,然后會(huì)調(diào)用onDraw方法進(jìn)行View的自我繪制。draw方法的調(diào)度流程大致是這樣的:

  • 繪制背景。對(duì)應(yīng)drawBackground(Canvas)方法。
  • 繪制主體。對(duì)應(yīng)onDraw(Canvas)方法。
  • 繪制子View。對(duì)應(yīng)dispatchDraw(Canvas)方法。
  • 繪制滑動(dòng)相關(guān)和前景。對(duì)應(yīng)onDrawForeground(Canvas)。

Android源碼中常見的設(shè)計(jì)模式以及自己在開發(fā)中常用的設(shè)計(jì)模式

Android與js是如何交互的

在Android中,Android與js的交互分為兩個(gè)方面:Android調(diào)用js里的方法、js調(diào)用Android中的方法。

Android調(diào)js。Android調(diào)js有兩種方法:

  • WebView.loadUrl("javascript:js中的方法名")。這種方法的優(yōu)點(diǎn)是很簡潔,缺點(diǎn)是沒有返回值,如果需要拿到j(luò)s方法的返回值則需要js調(diào)用Android中的方法來拿到這個(gè)返回值。
  • WebView.evaluateJavaScript("javascript:js中的方法名",ValueCallback)。這種方法比loadUrl好的是可以通過ValueCallback這個(gè)回調(diào)拿到j(luò)s方法的返回值。缺點(diǎn)是這個(gè)方法Android4.4才有,兼容性較差。不過放在2018年來說,市面上絕大多數(shù)App都要求最低版本是4.4了,所以我認(rèn)為這個(gè)兼容性問題不大。

js調(diào)Android。js調(diào)Android有三種方法:

  • WebView.addJavascriptInterface()。這是官方解決js調(diào)用Android方法的方案,需要注意的是要在供js調(diào)用的Android方法上加上 @JavascriptInterface 注解,以避免安全漏洞。這種方案的缺點(diǎn)是Android4.2以前會(huì)有安全漏洞,不過在4.2以后已經(jīng)修復(fù)了。同樣,在2018年來說,兼容性問題不大。
  • 重寫WebViewClient的shouldOverrideUrlLoading()方法來攔截url,拿到url后進(jìn)行解析,如果符合雙方的規(guī)定,即可調(diào)用Android方法。優(yōu)點(diǎn)是避免了Android4.2以前的安全漏洞,缺點(diǎn)也很明顯,無法直接拿到調(diào)用Android方法的返回值,只能通過Android調(diào)用js方法來獲取返回值。
  • 重寫WebChromClient的onJsPrompt()方法,同前一個(gè)方式一樣,拿到url之后先進(jìn)行解析,如果符合雙方規(guī)定,即可調(diào)用Android方法。最后如果需要返回值,通過result.confirm("Android方法返回值")即可將Android的返回值返回給js。方法的優(yōu)點(diǎn)是沒有漏洞,也沒有兼容性限制,同時(shí)還可以方便的獲取Android方法的返回值。其實(shí)這里需要注意的是在WebChromeClient中除了onJsPrompt之外還有onJsAlert和onJsConfirm方法。那么為什么不選擇另兩個(gè)方法呢?原因在于onJsAlert是沒有返回值的,而onJsConfirm只有true和false兩個(gè)返回值,同時(shí)在前端開發(fā)中prompt方法基本不會(huì)被調(diào)用,所以才會(huì)采用onJsPrompt。

熱修復(fù)原理

Activity啟動(dòng)過程

SparseArray原理

SparseArray,通常來講是Android中用來替代HashMap的一個(gè)數(shù)據(jù)結(jié)構(gòu)。
準(zhǔn)確來講,是用來替換key為Integer類型,value為Object類型的HashMap。需要注意的是SparseArray僅僅實(shí)現(xiàn)了Cloneable接口,所以不能用Map來聲明。
從內(nèi)部結(jié)構(gòu)來講,SparseArray內(nèi)部由兩個(gè)數(shù)組組成,一個(gè)是int[]類型的mKeys,用來存放所有的鍵;另一個(gè)是Object[]類型的mValues,用來存放所有的值。
最常見的是拿SparseArray跟HashMap來做對(duì)比,由于SparseArray內(nèi)部組成是兩個(gè)數(shù)組,所以占用內(nèi)存比HashMap要小。我們都知道,增刪改查等操作都首先需要找到相應(yīng)的鍵值對(duì),而SparseArray內(nèi)部是通過二分查找來尋址的,效率很明顯要低于HashMap的常數(shù)級(jí)別的時(shí)間復(fù)雜度。提到二分查找,這里還需要提一下的是二分查找的前提是數(shù)組已經(jīng)是排好序的,沒錯(cuò),SparseArray中就是按照key進(jìn)行升序排列的。
綜合起來來說,SparseArray所占空間優(yōu)于HashMap,而效率低于HashMap,是典型的時(shí)間換空間,適合較小容量的存儲(chǔ)。
從源碼角度來說,我認(rèn)為需要注意的是SparseArray的remove()、put()和gc()方法。

  • remove()。SparseArray的remove()方法并不是直接刪除之后再壓縮數(shù)組,而是將要?jiǎng)h除的value設(shè)置為DELETE這個(gè)SparseArray的靜態(tài)屬性,這個(gè)DELETE其實(shí)就是一個(gè)Object對(duì)象,同時(shí)會(huì)將SparseArray中的mGarbage這個(gè)屬性設(shè)置為true,這個(gè)屬性是便于在合適的時(shí)候調(diào)用自身的gc()方法壓縮數(shù)組來避免浪費(fèi)空間。這樣可以提高效率,如果將來要添加的key等于刪除的key,那么會(huì)將要添加的value覆蓋DELETE。
  • gc()。SparseArray中的gc()方法跟JVM的GC其實(shí)完全沒有任何關(guān)系。gc()方法的內(nèi)部實(shí)際上就是一個(gè)for循環(huán),將value不為DELETE的鍵值對(duì)往前移動(dòng)覆蓋value為DELETE的鍵值對(duì)來實(shí)現(xiàn)數(shù)組的壓縮,同時(shí)將mGarbage置為false,避免內(nèi)存的浪費(fèi)。
  • put()。put方法是這么一個(gè)邏輯,如果通過二分查找在mKeys數(shù)組中找到了key,那么直接覆蓋value即可。如果沒有找到,會(huì)拿到與數(shù)組中與要添加的key最接近的key索引,如果這個(gè)索引對(duì)應(yīng)的value為DELETE,則直接把新的value覆蓋DELETE即可,在這里可以避免數(shù)組元素的移動(dòng),從而提高了效率。如果value不為DELETE,會(huì)判斷mGarbage,如果為true,則會(huì)調(diào)用gc()方法壓縮數(shù)組,之后會(huì)找到合適的索引,將索引之后的鍵值對(duì)后移,插入新的鍵值對(duì),這個(gè)過程中可能會(huì)觸發(fā)數(shù)組的擴(kuò)容。

圖片加載如何避免OOM

我們知道內(nèi)存中的Bitmap大小的計(jì)算公式是:長所占像素 * 寬所占像素 * 每個(gè)像素所占內(nèi)存。想避免OOM有兩種方法:等比例縮小長寬、減少每個(gè)像素所占的內(nèi)存。

  • 等比縮小長寬。我們知道Bitmap的創(chuàng)建是通過BitmapFactory的工廠方法,decodeFile()、decodeStream()、decodeByteArray()、decodeResource()。這些方法中都有一個(gè)Options類型的參數(shù),這個(gè)Options是BitmapFactory的內(nèi)部類,存儲(chǔ)著BItmap的一些信息。Options中有一個(gè)屬性:inSampleSize。我們通過修改inSampleSize可以縮小圖片的長寬,從而減少BItmap所占內(nèi)存。需要注意的是這個(gè)inSampleSize大小需要是2的冪次方,如果小于1,代碼會(huì)強(qiáng)制讓inSampleSize為1。
  • 減少像素所占內(nèi)存。Options中有一個(gè)屬性inPreferredConfig,默認(rèn)是ARGB_8888,代表每個(gè)像素所占尺寸。我們可以通過將之修改為RGB_565或者ARGB_4444來減少一半內(nèi)存。

大圖加載

加載高清大圖,比如清明上河圖,首先屏幕是顯示不下的,而且考慮到內(nèi)存情況,也不可能一次性全部加載到內(nèi)存。這時(shí)候就需要局部加載了,Android中有一個(gè)負(fù)責(zé)局部加載的類:BitmapRegionDecoder。使用方法很簡單,通過BitmapRegionDecoder.newInstance()創(chuàng)建對(duì)象,之后調(diào)用decodeRegion(Rect rect, BitmapFactory.Options options)即可。第一個(gè)參數(shù)rect是要顯示的區(qū)域,第二個(gè)參數(shù)是BitmapFactory中的內(nèi)部類Options。

Android三方庫的源碼分析

由于源碼分析篇幅太大,所以這里之貼出我的源碼分析的鏈接(掘金)。

OkHttp

OkHttp源碼分析

Retrofit

Retrofit源碼分析1
Retrofit源碼分析2
Retrofit源碼分析3

RxJava

RxJava源碼分析

Glide

Glide源碼分析

EventBus

EventBus源碼分析

大致是這么一個(gè)流程:
register:

  • 獲取訂閱者的Class對(duì)象
  • 使用反射查找訂閱者中的事件處理方法集合
  • 遍歷事件處理方法集合,調(diào)用subscribe(subscriber,subscriberMethod)方法,在subscribe方法內(nèi):
    • 通過subscriberMethod獲取處理的事件類型eventType
    • 將訂閱者subscriber和方法subscriberMethod綁在一起形成一個(gè)Subscription對(duì)象
    • 通過subscriptionsByEventType.get(eventType)獲取Subscription集合
      • 如果Subscription集合為空則創(chuàng)建一個(gè)新的集合,這一步目的是延遲集合的初始化
      • 拿到Subscription集合后遍歷這個(gè)集合,通過比較事件處理的優(yōu)先級(jí),將新的Subscription對(duì)象加入合適的位置
    • 通過typesBySubscriber.get(subscriber)獲取事件類型集合
      • 如果事件類型集合為空則創(chuàng)建一個(gè)新的集合,這一步目的是延遲集合的初始化
      • 拿到事件類型集合后將新的事件類型加入到集合中
    • 判斷當(dāng)前事件類型是否是sticky
    • 如果當(dāng)前事件類型不是sticky(粘性事件),subscribe(subscriber,subscriberMethod)到此終結(jié)
    • 如果是sticky,判斷EventBus中的一個(gè)事件繼承性的屬性,默認(rèn)是true
      • 如果事件繼承性為true,遍歷這個(gè)Map類型的stickEvents,通過isAssignableFrom方法判斷當(dāng)前事件是否是遍歷事件的父類,如果是則發(fā)送事件
      • 如果事件繼承性為false,通過stickyEvents.get(eventType)獲取事件并發(fā)送

post:

  • postSticky
    • 將事件加入到stickyEvents這個(gè)Map類型的集合中
    • 調(diào)用post方法
  • post
    • 將事件加入當(dāng)前線程的事件隊(duì)列中
    • 通過while循環(huán)不斷從事件隊(duì)列中取出事件并調(diào)用postSingleEvent方法發(fā)送事件
    • 在postSingleEvent中,判斷事件繼承性,默認(rèn)為true
      • 事件繼承性為true,找到當(dāng)前事件所有的父類型并調(diào)用postSingleEventForEventType方法發(fā)送事件
      • 事件繼承性為false,只發(fā)送當(dāng)前事件類型的事件
        • 在postSingleEventForEventType中,通過subscriptionsByEventType.get(eventClass)獲取Subscription類型集合
        • 遍歷這個(gè)集合,調(diào)用postToSubscription發(fā)送事件
          • 在postToSubscription中分為四種情況
            • POSTING,調(diào)用invokeSubscriber(subscription, event)處理事件,本質(zhì)是method.invoke()反射
            • MAIN,如果在主線程直接invokeSubscriber處理;反之通過handler切換到主線程調(diào)用invokeSubscriber處理事件
            • BACKGROUND,如果不在主線程直接invokeSubscriber處理事件;反之開啟一條線程,在線程中調(diào)用invokeSubscriber處理事件
            • ASYNC,開啟一條線程,在線程中調(diào)用invokeSubscriber處理事件

unregister:

  • 刪除subscriptionsByEventType中與訂閱者相關(guān)的所有subscription
  • 刪除typesBySubscriber中與訂閱者相關(guān)的所有類型

數(shù)據(jù)結(jié)構(gòu)與算法

手寫快排

手寫歸并排序

手寫堆以及堆排序

說一下排序算法的區(qū)別(時(shí)間復(fù)雜度和空間復(fù)雜度)

工作中解決了什么難題,做了什么有成就感的項(xiàng)目(這個(gè)問題一定會(huì)問到,所以肯定要做準(zhǔn)備)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容