寫在開頭
由于杭州的房價實在太高丑掺,所以我可恥的跑路到了西安。幾個月前在西安買了房,所以最近總結(jié)了一些還算全面的Android面試題糠溜。還好成功的通過了西安努比亞的面試,雖然不是阿里直撤、網(wǎng)易這種級別的公司非竿,但對我一個畢業(yè)兩年的Android開發(fā)來說,算是成功的從小公司跳到大公司谋竖。
Java面試題
GC機制
垃圾回收需要完成兩件事:找到垃圾红柱,回收垃圾。
找到垃圾一般的話有兩種方法:
- 引用計數(shù)法
當一個對象被引用時蓖乘,它的引用計數(shù)器會加一锤悄,垃圾回收時會清理掉引用計數(shù)為0的對象。但這種方法有一個問題嘉抒,比方說有兩個對象A和B零聚,A引用了B,B又引用了A些侍,除此之外沒有別的對象引用A和B隶症,那么A和B在我們看來已經(jīng)是垃圾對象,需要被回收岗宣,但它們的引用計數(shù)不為0蚂会,沒有達到回收的條件。正因為這個循環(huán)引用的問題耗式,Java并沒有采用引用計數(shù)法胁住。 - 可達性分析法
我們把Java中對象引用的關系看做一張圖,從根級對象不可達的對象會被垃圾收集器清除刊咳。根級對象一般包括Java虛擬機棧中的對象彪见、本地方法棧中的對象、方法區(qū)中的靜態(tài)對象和常量池中的常量娱挨。
回收垃圾的話有這么四種方法: - 標記清除算法
顧名思義分為兩步企巢,標記和清除。首先標記到需要回收的垃圾對象让蕾,然后回收掉這些垃圾對象。標記清除算法的缺點是清除垃圾對象后會造成內(nèi)存的碎片化或听。 - 復制算法
復制算法是將存活的對象復制到另一塊內(nèi)存區(qū)域中探孝,并做相應的內(nèi)存整理工作。復制算法的優(yōu)點是可以避免內(nèi)存碎片化誉裆,缺點也顯而易見顿颅,它需要兩倍的內(nèi)存。 - 標記整理算法
標記整理算法也是分兩步足丢,先標記后整理粱腻。它會標記需要回收的垃圾對象庇配,清除掉垃圾對象后會將存活的對象壓縮,避免了內(nèi)存的碎片化绍些。 - 分代算法
分代算法將對象分為新生代和老年代對象捞慌。那么為什么做這樣的區(qū)分呢?主要是在Java運行中會產(chǎn)生大量對象柬批,這些對象的生命周期會有很大的不同啸澡,有的生命周期很長,有的甚至使用一次之后就不再使用氮帐。所以針對不同生命周期的對象采用不同的回收策略,這樣可以提高GC的效率。
新生代對象分為三個區(qū)域:Eden區(qū)和兩個Survivor區(qū)蜓洪。新創(chuàng)建的對象都放在Eden區(qū)东臀,當Eden區(qū)的內(nèi)存達到閾值之后會觸發(fā)Minor GC,這時會將存活的對象復制到一個Survivor區(qū)中参咙,這些存活對象的生命存活計數(shù)會加一龄广。這時Eden區(qū)會閑置,當再一次達到閾值觸發(fā)Minor GC時昂勒,會將Eden區(qū)和之前一個Survivor區(qū)中存活的對象復制到另一個Survivor區(qū)中蜀细,采用的是我之前提到的復制算法,同時它們的生命存活計數(shù)也會加一戈盈。這個過程會持續(xù)很多遍奠衔,直到對象的存活計數(shù)達到一定的閾值后會觸發(fā)一個叫做晉升的現(xiàn)象:新生代的這個對象會被放置到老年代中。
老年代中的對象都是經(jīng)過多次GC依然存活的生命周期很長的Java對象塘娶。當老年代的內(nèi)存達到閾值后會觸發(fā)Major GC归斤,采用的是標記整理算法。
JVM內(nèi)存區(qū)域的劃分刁岸,哪些區(qū)域會發(fā)生OOM
JVM的內(nèi)存區(qū)域可以分為兩類:線程私有和區(qū)域和線程共有的區(qū)域脏里。
線程私有的區(qū)域:程序計數(shù)器、JVM虛擬機棧虹曙、本地方法棧
線程共有的區(qū)域:堆迫横、方法區(qū)、運行時常量池
- 程序計數(shù)器酝碳。每個線程有有一個私有的程序計數(shù)器矾踱,任何時間一個線程都只會有一個方法正在執(zhí)行,也就是所謂的當前方法疏哗。程序計數(shù)器存放的就是這個當前方法的JVM指令地址呛讲。
- JVM虛擬機棧。創(chuàng)建線程的時候會創(chuàng)建線程內(nèi)的虛擬機棧,棧中存放著一個個的棧幀贝搁,對應著一個個方法的調(diào)用吗氏。JVM虛擬機棧有兩種操作,分別是壓棧和出站雷逆。棧幀中存放著局部變量表弦讽、方法返回值和方法的正常或異常退出的定義等等关面。
- 本地方法棧坦袍。跟JVM虛擬機棧比較類似,只不過它支持的是Native方法等太。
- 堆捂齐。堆是內(nèi)存管理的核心區(qū)域,用來存放對象實例缩抡。幾乎所有創(chuàng)建的對象實例都會直接分配到堆上奠宜。所以堆也是垃圾回收的主要區(qū)域,垃圾收集器會對堆有著更細的劃分瞻想,最常見的就是把堆劃分為新生代和老年代压真。
- 方法區(qū)。方法區(qū)主要存放類的結(jié)構(gòu)信息蘑险,比如靜態(tài)屬性和方法等等滴肿。
- 運行時常量池。運行時常量池位于方法區(qū)中佃迄,主要存放各種常量信息泼差。
其實除了程序計數(shù)器,其他的部分都會發(fā)生OOM呵俏。
- 堆堆缘。通常發(fā)生的OOM都會發(fā)生在堆中,最常見的可能導致OOM的原因就是內(nèi)存泄漏普碎。
- JVM虛擬機棧和本地方法棧吼肥。當我們寫一個遞歸方法,這個遞歸方法沒有循環(huán)終止條件麻车,最終會導致StackOverflow的錯誤缀皱。當然,如果椂空間擴展失敗唆鸡,也是會發(fā)生OOM的。
- 方法區(qū)枣察。方法區(qū)現(xiàn)在基本上不太會發(fā)生OOM,但在早期內(nèi)存中加載的類信息過多的情況下也是會發(fā)生OOM的。
類加載過程序目,雙親委派模型
Java中類加載分為3個步驟:加載臂痕、鏈接、初始化猿涨。
加載握童。加載是將字節(jié)碼數(shù)據(jù)從不同的數(shù)據(jù)源讀取到JVM內(nèi)存,并映射為JVM認可的數(shù)據(jù)結(jié)構(gòu)叛赚,也就是Class對象的過程澡绩。數(shù)據(jù)源可以是Jar文件、Class文件等等俺附。如果數(shù)據(jù)的格式并不是ClassFile的結(jié)構(gòu)肥卡,則會報ClassFormatError。
鏈接事镣。鏈接是類加載的核心部分步鉴,這一步分為3個步驟:驗證、準備璃哟、解析氛琢。
- 驗證。驗證是保證JVM安全的重要步驟随闪。JVM需要校驗字節(jié)信息是否符合規(guī)范阳似,避免惡意信息和不規(guī)范數(shù)據(jù)危害JVM運行安全。如果驗證出錯铐伴,則會報VerifyError撮奏。
- 準備。這一步會創(chuàng)建靜態(tài)變量盛杰,并為靜態(tài)變量開辟內(nèi)存空間挽荡。
- 解析。這一步會將符號引用替換為直接引用即供。
初始化定拟。初始化會為靜態(tài)變量賦值,并執(zhí)行靜態(tài)代碼塊中的邏輯逗嫡。
雙親委派模型青自。
類加載器大致分為3類:啟動類加載器、擴展類加載器驱证、應用程序類加載器延窜。
啟動類加載器主要加載 jre/lib下的jar文件。
擴展類加載器主要加載 jre/lib/ext 下的jar文件抹锄。
應用程序類加載器主要加載 classpath下的文件逆瑞。
所謂的雙親委派模型就是當加載一個類時荠藤,會優(yōu)先使用父類加載器加載,當父類加載器無法加載時才會使用子類加載器去加載获高。這么做的目的是為了避免類的重復加載哈肖。
Java中的集合類
HashMap的原理
HashMap的內(nèi)部可以看做數(shù)組+鏈表的復合結(jié)構(gòu)。數(shù)組被分為一個個的桶(bucket)念秧。哈希值決定了鍵值對在數(shù)組中的尋址淤井。具有相同哈希值的鍵值對會組成鏈表。需要注意的是當鏈表長度超過閾值(默認是8)的時候會觸發(fā)樹化摊趾,鏈表會變成樹形結(jié)構(gòu)币狠。
把握HashMap的原理需要關注4個方法:hash、put砾层、get漩绵、resize。
hash方法梢为。將key的hashCode值的高位數(shù)據(jù)移位到低位進行異或運算渐行。這么做的原因是有些key的hashCode值的差異集中在高位,而哈希尋址是忽略容量以上高位的铸董,這種做法可以有效避免哈希沖突祟印。
put方法。put方法主要有以下幾個步驟:
- 通過hash方法獲取hash值粟害,根據(jù)hash值尋址蕴忆。
- 如果未發(fā)生碰撞,直接放到桶中悲幅。
- 如果發(fā)生碰撞套鹅,則以鏈表形式放在桶后。
- 當鏈表長度大于閾值后會觸發(fā)樹化汰具,將鏈表轉(zhuǎn)換為紅黑樹卓鹿。
- 如果數(shù)組長度達到閾值,會調(diào)用resize方法擴展容量留荔。
get方法吟孙。get方法主要有以下幾個步驟:
- 通過hash方法獲取hash值,根據(jù)hash值尋址聚蝶。
- 如果與尋址到桶的key相等杰妓,直接返回對應的value。
- 如果發(fā)生沖突碘勉,分兩種情況巷挥。如果是樹,則調(diào)用getTreeNode獲取value验靡;如果是鏈表則通過循環(huán)遍歷查找對應的value倍宾。
resize方法雏节。resize做了兩件事:
- 將原數(shù)組擴展為原來的2倍
- 重新計算index索引值,將原節(jié)點重新放到新的數(shù)組中凿宾。這一步可以將原先沖突的節(jié)點分散到新的桶中矾屯。
什么情況下Java會產(chǎn)生死鎖,如何定位初厚、修復,手寫死鎖
sleep和wait的區(qū)別
- sleep方法是Thread類中的靜態(tài)方法孙技,wait是Object類中的方法
- sleep并不會釋放同步鎖产禾,而wait會釋放同步鎖
- sleep可以在任何地方使用,而wait只能在同步方法或者同步代碼塊中使用
- sleep中必須傳入時間牵啦,而wait可以傳亚情,也可以不傳,不傳時間的話只有notify或者notifyAll才能喚醒哈雏,傳時間的話在時間之后會自動喚醒
join的用法
join方法通常是保證線程間順序調(diào)度的一個方法楞件,它是Thread類中的方法。比方說在線程A中執(zhí)行線程B.join()裳瘪,這時線程A會進入等待狀態(tài)土浸,直到線程B執(zhí)行完畢之后才會喚醒,繼續(xù)執(zhí)行A線程中的后續(xù)方法彭羹。
join方法可以傳時間參數(shù)黄伊,也可以不傳參數(shù),不傳參數(shù)實際上調(diào)用的是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的區(qū)別
Java中的線程池
線程通信
Java中的并發(fā)集合
Java中生產(chǎn)者與消費者模式
生產(chǎn)者消費者模式要保證的是當緩沖區(qū)滿的時候生產(chǎn)者不再生產(chǎn)對象,當緩沖區(qū)空時毡惜,消費者不再消費對象拓轻。實現(xiàn)機制就是當緩沖區(qū)滿時讓生產(chǎn)者處于等待狀態(tài),當緩沖區(qū)為空時讓消費者處于等待狀態(tài)经伙。當生產(chǎn)者生產(chǎn)了一個對象后會喚醒消費者扶叉,當消費者消費一個對象后會喚醒生產(chǎn)者。
三種種實現(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)了一個新產(chǎn)品泳叠,現(xiàn)庫存為:" + 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("消費了一個產(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)了一個新產(chǎn)品危纫,現(xiàn)容量為:" + mList.size());
mEmpty.signalAll();
mLock.unlock();
}
public void consume() {
mLock.lock();
while (mList.size() == 0) {
System.out.println("緩沖區(qū)為空宗挥,暫停消費");
try {
mEmpty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mList.remove();
System.out.println("消費了一個產(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)了一個產(chǎn)品契耿,現(xiàn)容量為:" + list.size());
}
public void consume() {
if (list.size() == 0) {
System.out.println("緩沖區(qū)為空瞒大,暫停消費");
}
try {
list.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消費了一個產(chǎn)品,現(xiàn)容量為:" + list.size());
}
}
final搪桂、finally透敌、finalize區(qū)別
final可以修飾類、變量和方法踢械。修飾類代表這個類不可被繼承酗电。修飾變量代表此變量不可被改變。修飾方法表示此方法不可被重寫(override)内列。
finally是保證重點代碼一定會執(zhí)行的一種機制撵术。通常是使用try-finally或者try-catch-finally來進行文件流的關閉等操作。
finalize是Object類中的一個方法话瞧,它的設計目的是保證對象在垃圾收集前完成特定資源的回收嫩与。finalize機制現(xiàn)在已經(jīng)不推薦使用,并且在JDK 9已經(jīng)被標記為deprecated交排。
Java中單例模式
Java中常見的單例模式實現(xiàn)有這么幾種:餓漢式划滋、雙重判斷的懶漢式、靜態(tài)內(nèi)部類實現(xiàn)的單例个粱、枚舉實現(xiàn)的單例古毛。
這里著重講一下雙重判斷的懶漢式和靜態(tài)內(nèi)部類實現(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包裹代碼來實現(xiàn)線程安全,通過雙重判斷來提高程序執(zhí)行的效率胶征。這里需要注意的是單例對象實例需要有volatile修飾塞椎,如果沒有volatile修飾,在多線程情況下可能會出現(xiàn)問題睛低。原因是這樣的案狠,mInstance=new SingleTon() 這一句代碼并不是一個原子操作,它包含三個操作:
- 給mInstance分配內(nèi)存
- 調(diào)用SingleTon的構(gòu)造方法初始化成員變量
- 將mInstance指向分配的內(nèi)存空間(在這一步mInstance已經(jīng)不為null了)
我們知道JVM會發(fā)生指令重排钱雷,正常的執(zhí)行順序是1-2-3骂铁,但發(fā)生指令重排后可能會導致1-3-2。我們考慮這樣一種情況罩抗,當線程A執(zhí)行到1-3-2的3步驟暫停了拉庵,這時候線程B調(diào)用了getInstance,走到了最外層的if判斷上套蒂,由于最外層的if判斷并沒有synchronized包裹钞支,所以可以執(zhí)行到這一句茫蛹,這時候由于線程A已經(jīng)執(zhí)行了步驟3,此時mInstance已經(jīng)不為null了烁挟,所以線程B直接返回了mInstance婴洼。但其實我們知道,完整的初始化必須走完這三個步驟撼嗓,由于線程A只走了兩個步驟柬采,所以一定會報錯的。
解決的辦法就是使用volatile修飾mInstance且警,我們知道volatile有兩個作用:保證可見性和禁止指令重排警没,在這里關鍵在于禁止指令重排,禁止指令重排后保證了不會發(fā)生上述問題振湾。
靜態(tài)內(nèi)部類實現(xiàn)的單例:
class SingletonWithInnerClass {
private SingletonWithInnerClass() {
}
private static class SingletonHolder{
private static SingletonWithInnerClass INSTANCE=new SingletonWithInnerClass();
}
public SingletonWithInnerClass getInstance() {
return SingletonHolder.INSTANCE;
}
}
由于外部類的加載并不會導致內(nèi)部類立即加載,只有當調(diào)用getInstance的時候才會加載內(nèi)部類亡脸,所以實現(xiàn)了延遲初始化押搪。由于類只會被加載一次,并且類加載也是線程安全的浅碾,所以滿足我們所有的需求大州。靜態(tài)內(nèi)部類實現(xiàn)的單例也是最為推薦的一種方式。
Java中引用類型的區(qū)別垂谢,具體的使用場景
Java中引用類型分為四類:強引用厦画、軟引用、弱引用滥朱、虛引用根暑。
強引用:強引用指的是通過new對象創(chuàng)建的引用,垃圾回收器即使是內(nèi)存不足也不會回收強引用指向的對象徙邻。
軟引用:軟引用是通過SoftRefrence實現(xiàn)的排嫌,它的生命周期比強引用短,在內(nèi)存不足缰犁,拋出OOM之前淳地,垃圾回收器會回收軟引用引用的對象。軟引用常見的使用場景是存儲一些內(nèi)存敏感的緩存帅容,當內(nèi)存不足時會被回收颇象。
弱引用:弱引用是通過WeakRefrence實現(xiàn)的,它的生命周期比軟引用還短并徘,GC只要掃描到弱引用的對象就會回收遣钳。弱引用常見的使用場景也是存儲一些內(nèi)存敏感的緩存。
虛引用:虛引用是通過FanttomRefrence實現(xiàn)的饮亏,它的生命周期最短耍贾,隨時可能被回收阅爽。如果一個對象只被虛引用引用,我們無法通過虛引用來訪問這個對象的任何屬性和方法荐开。它的作用僅僅是保證對象在finalize后付翁,做某些事情。虛引用常見的使用場景是跟蹤對象被垃圾回收的活動晃听,當一個虛引用關聯(lián)的對象被垃圾回收器回收之前會收到一條系統(tǒng)通知百侧。
Exception和Error的區(qū)別
Exception和Error都繼承于Throwable,在Java中能扒,只有Throwable類型的對象才能被throw或者catch佣渴,它是異常處理機制的基本組成類型。
Exception和Error體現(xiàn)了Java對不同異常情況的分類初斑。Exception是程序正常運行中辛润,可以預料的意外情況,可能并且應該被捕獲见秤,進行相應的處理砂竖。
Error是指在正常情況下,不大可能出現(xiàn)的情況鹃答,絕大部分Error都會使程序處于非正常乎澄、不可恢復的狀態(tài)。既然是非正常测摔,所以不便于也不需要捕獲置济,常見的OutOfMemoryError就是Error的子類。
Exception又分為checked Exception和unchecked Exception锋八。checked Exception在代碼里必須顯式的進行捕獲浙于,這是編譯器檢查的一部分。unchecked Exception也就是運行時異常查库,類似空指針異常路媚、數(shù)組越界等,通常是可以避免的邏輯錯誤樊销,具體根據(jù)需求來判斷是否需要捕獲整慎,并不會在編譯器強制要求。
volatile
一般提到volatile围苫,就不得不提到內(nèi)存模型相關的概念裤园。我們都知道互站,在程序運行中励七,每條指令都是由CPU執(zhí)行的泥技,而指令的執(zhí)行過程中允悦,勢必涉及到數(shù)據(jù)的讀取和寫入黄痪。程序運行中的數(shù)據(jù)都存放在主存中圾旨,這樣會有一個問題妙黍,由于CPU的執(zhí)行速度是要遠高于主存的讀寫速度稚铣,所以直接從主存中讀寫數(shù)據(jù)會降低CPU的效率。為了解決這個問題铡羡,就有了高速緩存的概念积蔚,在每個CPU中都有高速緩存,它會事先從主存中讀取數(shù)據(jù)烦周,在CPU運算之后在合適的時候刷新到主存中尽爆。
這樣的運行模式在單線程中是沒有任何問題的,但在多線程中读慎,會導致緩存一致性的問題漱贱。舉個簡單的例子:i=i+1 ,在兩個線程中執(zhí)行這句代碼,假設i的初始值為0夭委。我們期望兩個線程運行后得到2幅狮,那么有這樣的一種情況,兩個線程都從主存中讀取i到各自的高速緩存中株灸,這時候兩個線程中的i都為0彪笼。在線程1執(zhí)行完畢得到i=1,將之刷新到主存后蚂且,線程2開始執(zhí)行,由于線程2中的i是高速緩存中的0幅恋,所以在執(zhí)行完線程2之后刷新到主存的i仍舊是1杏死。
所以這就導致了對共享變量的緩存一致性的問題,那么為了解決這個問題捆交,提出了緩存一致性協(xié)議:當CPU在寫數(shù)據(jù)時淑翼,如果發(fā)現(xiàn)操作的是共享變量,它會通知其他CPU將它們內(nèi)部的這個共享變量置為無效狀態(tài)品追,當其他CPU讀取緩存中的共享變量時玄括,發(fā)現(xiàn)這個變量是無效的,它會從新從主存中讀取最新的值肉瓦。
在Java的多線程開發(fā)中遭京,有三個重要概念:原子性、可見性泞莉、有序性哪雕。
原子性:一個或多個操作要么都不執(zhí)行,要么都執(zhí)行鲫趁。
可見性:一個線程中對共享變量(類中的成員變量或靜態(tài)變量)的修改斯嚎,在其他線程立即可見。
有序性:程序執(zhí)行的順序按照代碼的順序執(zhí)行。
把一個變量聲明為volatile堡僻,其實就是保證了可見性和有序性糠惫。
可見性我上面已經(jīng)說過了,在多線程開發(fā)中是很有必要的钉疫。這個有序性還是得說一下硼讽,為了執(zhí)行的效率,有時候會發(fā)生指令重排陌选,這在單線程中指令重排之后的輸出與我們的代碼邏輯輸出還是一致的理郑。但在多線程中就可能發(fā)生問題,volatile在一定程度上可以避免指令重排咨油。
volatile的原理是在生成的匯編代碼中多了一個lock前綴指令您炉,這個前綴指令相當于一個內(nèi)存屏障,這個內(nèi)存屏障有3個作用:
- 確保指令重排的時候不會把屏障后的指令排在屏障前役电,確保不會把屏障前的指令排在屏障后赚爵。
- 修改緩存中的共享變量后立即刷新到主存中。
- 當執(zhí)行寫操作時會導致其他CPU中的緩存無效法瑟。
網(wǎng)絡相關面試題
http 狀態(tài)碼
http 與 https 的區(qū)別冀膝?https 是如何工作的?
http是超文本傳輸協(xié)議霎挟,而https可以簡單理解為安全的http協(xié)議窝剖。https通過在http協(xié)議下添加了一層ssl協(xié)議對數(shù)據(jù)進行加密從而保證了安全。https的作用主要有兩點:建立安全的信息傳輸通道酥夭,保證數(shù)據(jù)傳輸安全赐纱;確認網(wǎng)站的真實性。
http與https的區(qū)別主要如下:
- https需要到CA申請證書熬北,很少免費疙描,因而需要一定的費用
- http是明文傳輸,安全性低讶隐;而https在http的基礎上通過ssl加密起胰,安全性高
- 二者的默認端口不一樣,http使用的默認端口是80巫延;https使用的默認端口是443
https的工作流程
提到https的話首先要說到加密算法效五,加密算法分為兩類:對稱加密和非對稱加密。
對稱加密:加密和解密用的都是相同的秘鑰炉峰,優(yōu)點是速度快火俄,缺點是安全性低。常見的對稱加密算法有DES讲冠、AES等等瓜客。
非對稱加密:非對稱加密有一個秘鑰對,分為公鑰和私鑰。一般來說谱仪,私鑰自己持有玻熙,公鑰可以公開給對方,優(yōu)點是安全性比對稱加密高疯攒,缺點是數(shù)據(jù)傳輸效率比對稱加密低嗦随。采用公鑰加密的信息只有對應的私鑰可以解密。常見的非對稱加密包括RSA等敬尺。
在正式的使用場景中一般都是對稱加密和非對稱加密結(jié)合使用枚尼,使用非對稱加密完成秘鑰的傳遞,然后使用對稱秘鑰進行數(shù)據(jù)加密和解密砂吞。二者結(jié)合既保證了安全性署恍,又提高了數(shù)據(jù)傳輸效率。
https的具體流程如下:
- 客戶端(通常是瀏覽器)先向服務器發(fā)出加密通信的請求
- 支持的協(xié)議版本蜻直,比如TLS 1.0版
- 一個客戶端生成的隨機數(shù) random1盯质,稍后用于生成"對話密鑰"
- 支持的加密方法,比如RSA公鑰加密
- 支持的壓縮方法
- 服務器收到請求,然后響應
- 確認使用的加密通信協(xié)議版本概而,比如TLS 1.0版本呼巷。如果瀏覽器與服務器支持的版本不一致,服務器關閉加密通信
- 一個服務器生成的隨機數(shù)random2赎瑰,稍后用于生成"對話密鑰"
- 確認使用的加密方法王悍,比如RSA公鑰加密
- 服務器證書
- 客戶端收到證書之后會首先會進行驗證
- 首先驗證證書的安全性
- 驗證通過之后,客戶端會生成一個隨機數(shù)pre-master secret餐曼,然后使用證書中的公鑰進行加密配名,然后傳遞給服務器端
- 服務器收到使用公鑰加密的內(nèi)容,在服務器端使用私鑰解密之后獲得隨機數(shù)pre-master secret晋辆,然后根據(jù)radom1、radom2宇整、pre-master secret通過一定的算法得出一個對稱加密的秘鑰瓶佳,作為后面交互過程中使用對稱秘鑰。同時客戶端也會使用radom1鳞青、radom2霸饲、pre-master secret,和同樣的算法生成對稱秘鑰臂拓。
- 然后再后續(xù)的交互中就使用上一步生成的對稱秘鑰對傳輸?shù)膬?nèi)容進行加密和解密厚脉。
TCP三次握手流程
Android面試題
進程間通信的方式有哪幾種
AIDL 、廣播胶惰、文件傻工、socket、管道
廣播靜態(tài)注冊和動態(tài)注冊的區(qū)別
- 動態(tài)注冊廣播不是常駐型廣播,也就是說廣播跟隨Activity的生命周期中捆。注意在Activity結(jié)束前鸯匹,移除廣播接收器。 靜態(tài)注冊是常駐型泄伪,也就是說當應用程序關閉后殴蓬,如果有信息廣播來,程序也會被系統(tǒng)調(diào)用自動運行蟋滴。
- 當廣播為有序廣播時:優(yōu)先級高的先接收(不分靜態(tài)和動態(tài))染厅。同優(yōu)先級的廣播接收器,動態(tài)優(yōu)先于靜態(tài)
- 同優(yōu)先級的同類廣播接收器津函,靜態(tài):先掃描的優(yōu)先于后掃描的肖粮,動態(tài):先注冊的優(yōu)先于后注冊的。
- 當廣播為默認廣播時:無視優(yōu)先級球散,動態(tài)廣播接收器優(yōu)先于靜態(tài)廣播接收器尿赚。同優(yōu)先級的同類廣播接收器,靜態(tài):先掃描的優(yōu)先于后掃描的蕉堰,動態(tài):先注冊的優(yōu)先于后冊的凌净。
Android性能優(yōu)化工具使用(這個問題建議配合Android中的性能優(yōu)化)
Android中常用的性能優(yōu)化工具包括這些:Android Studio自帶的Android Profiler、LeakCanary屋讶、BlockCanary
Android自帶的Android Profiler其實就很好用冰寻,Android Profiler可以檢測三個方面的性能問題:CPU、MEMORY皿渗、NETWORK斩芭。
LeakCanary是一個第三方的檢測內(nèi)存泄漏的庫,我們的項目集成之后LeakCanary會自動檢測應用運行期間的內(nèi)存泄漏乐疆,并將之輸出給我們划乖。
BlockCanary也是一個第三方檢測UI卡頓的庫,項目集成后Block也會自動檢測應用運行期間的UI卡頓挤土,并將之輸出給我們琴庵。
Android中的類加載器
PathClassLoader,只能加載系統(tǒng)中已經(jīng)安裝過的apk
DexClassLoader仰美,可以加載jar/apk/dex迷殿,可以從SD卡中加載未安裝的apk
Android中的動畫有哪幾類,它們的特點和區(qū)別是什么
Android中動畫大致分為3類:幀動畫咖杂、補間動畫(View Animation)庆寺、屬性動畫(Object Animation)。
- 幀動畫:通過xml配置一組圖片诉字,動態(tài)播放懦尝。很少會使用知纷。
- 補間動畫(View Animation):大致分為旋轉(zhuǎn)、透明导披、縮放屈扎、位移四類操作。很少會使用撩匕。
- 屬性動畫(Object Animation):屬性動畫是現(xiàn)在使用的最多的一種動畫鹰晨,它比補間動畫更加強大。屬性動畫大致分為兩種使用類型止毕,分別是ViewPropertyAnimator和ObjectAnimator模蜡。前者適合一些通用的動畫,比如旋轉(zhuǎn)扁凛、位移忍疾、縮放和透明,使用方式也很簡單通過View.animate()即可得到ViewPropertyAnimator谨朝,之后進行相應的動畫操作即可卤妒。后者適合用于為我們的自定義控件添加動畫,當然首先我們應該在自定義View中添加相應的getXXX()和setXXX()相應屬性的getter和setter方法字币,這里需要注意的是在setter方法內(nèi)改變了自定義View中的屬性后要調(diào)用invalidate()來刷新View的繪制则披。之后調(diào)用ObjectAnimator.of屬性類型()返回一個ObjectAnimator,調(diào)用start()方法啟動動畫即可洗出。
補間動畫與屬性動畫的區(qū)別:
- 補間動畫是父容器不斷的繪制view士复,看起來像移動了效果,其實view沒有變化,還在原地翩活。
- 是通過不斷改變view內(nèi)部的屬性值阱洪,真正的改變view。
Handler機制
說到Handler菠镇,就不得不提與之密切相關的這幾個類:Message冗荸、MessageQueue,Looper利耍。
- Message蚌本。Message中有兩個成員變量值得關注:target和callback。target其實就是發(fā)送消息的Handler對象堂竟,callback是當調(diào)用handler.post(runnable)時傳入的Runnable類型的任務。post事件的本質(zhì)也是創(chuàng)建了一個Message玻佩,將我們傳入的這個runnable賦值給創(chuàng)建的Message的callback這個成員變量出嘹。
- MessageQueue。消息隊列很明顯是存放消息的隊列咬崔,值得關注的是MessageQueue中的next()方法税稼,它會返回下一個待處理的消息烦秩。
- Looper。Looper消息輪詢器其實是連接Handler和消息隊列的核心郎仆。首先我們都知道只祠,如果想要在一個線程中創(chuàng)建一個Handler,首先要通過Looper.prepare()創(chuàng)建Looper扰肌,之后還得調(diào)用Looper.loop()開啟輪詢抛寝。我們著重看一下這兩個方法。
prepare()曙旭。這個方法做了兩件事:首先通過ThreadLocal.get()獲取當前線程中的Looper,如果不為空盗舰,則會拋出一個RunTimeException,意思是一個線程不能創(chuàng)建2個Looper桂躏。如果為null則執(zhí)行下一步钻趋。第二步是創(chuàng)建了一個Looper,并通過ThreadLocal.set(looper)剂习。將我們創(chuàng)建的Looper與當前線程綁定蛮位。這里需要提一下的是消息隊列的創(chuàng)建其實就發(fā)生在Looper的構(gòu)造方法中。
loop()鳞绕。這個方法開啟了整個事件機制的輪詢失仁。它的本質(zhì)是開啟了一個死循環(huán),不斷的通過MessageQueue的next()方法獲取消息猾昆。拿到消息后會調(diào)用msg.target.dispatchMessage()來做處理陶因。其實我們在說到Message的時候提到過,msg.target其實就是發(fā)送這個消息的handler垂蜗。這句代碼的本質(zhì)就是調(diào)用handler的dispatchMessage()楷扬。
- Handler。上面做了這么多鋪墊贴见,終于到了最重要的部分烘苹。Handler的分析著重在兩個部分:發(fā)送消息和處理消息。
發(fā)送消息片部。其實發(fā)送消息除了sendMessage之外還有sendMessageDelayed和post以及postDelayed等等不同的方式镣衡。但它們的本質(zhì)都是調(diào)用了sendMessageAtTime。在sendMessageAtTime這個方法中調(diào)用了enqueueMessage档悠。在enqueueMessage這個方法中做了兩件事:通過msg.target = this實現(xiàn)了消息與當前handler的綁定廊鸥。然后通過queue.enqueueMessage實現(xiàn)了消息入隊。
處理消息辖所。消息處理的核心其實就是dispatchMessage()這個方法惰说。這個方法里面的邏輯很簡單,先判斷msg.callback是否為null缘回,如果不為空則執(zhí)行這個runnable吆视。如果為空則會執(zhí)行我們的handleMessage方法典挑。
Android性能優(yōu)化
Android中的性能優(yōu)化在我看來分為以下幾個方面:內(nèi)存優(yōu)化、布局優(yōu)化啦吧、網(wǎng)絡優(yōu)化您觉、安裝包優(yōu)化。
內(nèi)存優(yōu)化:下一個問題就是授滓。
布局優(yōu)化:布局優(yōu)化的本質(zhì)就是減少View的層級琳水。常見的布局優(yōu)化方案如下
- 在LinearLayout和RelativeLayout都可以完成布局的情況下優(yōu)先選擇RelativeLayout,可以減少View的層級
- 將常用的布局組件抽取出來使用 < include > 標簽
- 通過 < ViewStub > 標簽來加載不常用的布局
- 使用 < Merge > 標簽來減少布局的嵌套層次
網(wǎng)絡優(yōu)化:常見的網(wǎng)絡優(yōu)化方案如下
- 盡量減少網(wǎng)絡請求褒墨,能夠合并的就盡量合并
- 避免DNS解析炫刷,根據(jù)域名查詢可能會耗費上百毫秒的時間,也可能存在DNS劫持的風險郁妈』肼辏可以根據(jù)業(yè)務需求采用增加動態(tài)更新IP的方式,或者在IP方式訪問失敗時切換到域名訪問方式噩咪。
- 大量數(shù)據(jù)的加載采用分頁的方式
- 網(wǎng)絡數(shù)據(jù)傳輸采用GZIP壓縮
- 加入網(wǎng)絡數(shù)據(jù)的緩存顾彰,避免頻繁請求網(wǎng)絡
- 上傳圖片時,在必要的時候壓縮圖片
安裝包優(yōu)化:安裝包優(yōu)化的核心就是減少apk的體積胃碾,常見的方案如下
- 使用混淆涨享,可以在一定程度上減少apk體積,但實際效果微乎其微
- 減少應用中不必要的資源文件仆百,比如圖片厕隧,在不影響APP效果的情況下盡量壓縮圖片,有一定的效果
- 在使用了SO庫的時候優(yōu)先保留v7版本的SO庫俄周,刪掉其他版本的SO庫吁讨。原因是在2018年,v7版本的SO庫可以滿足市面上絕大多數(shù)的要求峦朗,可能八九年前的手機滿足不了建丧,但我們也沒必要去適配老掉牙的手機。實際開發(fā)中減少apk體積的效果是十分顯著的波势,如果你使用了很多SO庫翎朱,比方說一個版本的SO庫一共10M,那么只保留v7版本尺铣,刪掉armeabi和v8版本的SO庫拴曲,一共可以減少20M的體積。
Android內(nèi)存優(yōu)化
Android的內(nèi)存優(yōu)化在我看來分為兩點:避免內(nèi)存泄漏凛忿、擴大內(nèi)存澈灼,其實就是開源節(jié)流。
其實內(nèi)存泄漏的本質(zhì)就是較長生命周期的對象引用了較短生命周期的對象侄非。
常見的內(nèi)存泄漏:
- 單例模式導致的內(nèi)存泄漏蕉汪。最常見的例子就是創(chuàng)建這個單例對象需要傳入一個Context,這時候傳入了一個Activity類型的Context逞怨,由于單例對象的靜態(tài)屬性者疤,導致它的生命周期是從單例類加載到應用程序結(jié)束為止,所以即使已經(jīng)finish掉了傳入的Activity叠赦,由于我們的單例對象依然持有Activity的引用驹马,所以導致了內(nèi)存泄漏。解決辦法也很簡單除秀,不要使用Activity類型的Context糯累,使用Application類型的Context可以避免內(nèi)存泄漏。
- 靜態(tài)變量導致的內(nèi)存泄漏册踩。靜態(tài)變量是放在方法區(qū)中的泳姐,它的生命周期是從類加載到程序結(jié)束,可以看到靜態(tài)變量生命周期是非常久的暂吉。最常見的因靜態(tài)變量導致內(nèi)存泄漏的例子是我們在Activity中創(chuàng)建了一個靜態(tài)變量胖秒,而這個靜態(tài)變量的創(chuàng)建需要傳入Activity的引用this。在這種情況下即使Activity調(diào)用了finish也會導致內(nèi)存泄漏慕的。原因就是因為這個靜態(tài)變量的生命周期幾乎和整個應用程序的生命周期一致阎肝,它一直持有Activity的引用,從而導致了內(nèi)存泄漏肮街。
- 非靜態(tài)內(nèi)部類導致的內(nèi)存泄漏风题。非靜態(tài)內(nèi)部類導致內(nèi)存泄漏的原因是非靜態(tài)內(nèi)部類持有外部類的引用,最常見的例子就是在Activity中使用Handler和Thread了嫉父。使用非靜態(tài)內(nèi)部類創(chuàng)建的Handler和Thread在執(zhí)行延時操作的時候會一直持有當前Activity的引用沛硅,如果在執(zhí)行延時操作的時候就結(jié)束Activity,這樣就會導致內(nèi)存泄漏熔号。解決辦法有兩種:第一種是使用靜態(tài)內(nèi)部類稽鞭,在靜態(tài)內(nèi)部類中使用弱引用調(diào)用Activity。第二種方法是在Activity的onDestroy中調(diào)用handler.removeCallbacksAndMessages來取消延時事件引镊。
- 使用資源未及時關閉導致的內(nèi)存泄漏朦蕴。常見的例子有:操作各種數(shù)據(jù)流未及時關閉,操作Bitmap未及時recycle等等弟头。
- 使用第三方庫未能及時解綁吩抓。有的三方庫提供了注冊和解綁的功能,最常見的就是EventBus了赴恨,我們都知道使用EventBus要在onCreate中注冊疹娶,在onDestroy中解綁。如果沒有解綁的話伦连,EventBus其實是一個單例模式雨饺,他會一直持有Activity的引用钳垮,導致內(nèi)存泄漏。同樣常見的還有RxJava额港,在使用Timer操作符做了一些延時操作后也要注意在onDestroy方法中調(diào)用disposable.dispose()來取消操作饺窿。
- 屬性動畫導致的內(nèi)存泄漏。常見的例子就是在屬性動畫執(zhí)行的過程中退出了Activity移斩,這時View對象依然持有Activity的引用從而導致了內(nèi)存泄漏肚医。解決辦法就是在onDestroy中調(diào)用動畫的cancel方法取消屬性動畫。
- WebView導致的內(nèi)存泄漏向瓷。WebView比較特殊肠套,即使是調(diào)用了它的destroy方法,依然會導致內(nèi)存泄漏猖任。其實避免WebView導致內(nèi)存泄漏的最好方法就是讓WebView所在的Activity處于另一個進程中钻心,當這個Activity結(jié)束時殺死當前WebView所處的進程即可仿荆,我記得阿里釘釘?shù)腤ebView就是另外開啟的一個進程区匠,應該也是采用這種方法避免內(nèi)存泄漏九巡。
擴大內(nèi)存,為什么要擴大我們的內(nèi)存呢室琢?有時候我們實際開發(fā)中不可避免的要使用很多第三方商業(yè)的SDK乾闰,這些SDK其實有好有壞,大廠的SDK可能內(nèi)存泄漏會少一些盈滴,但一些小廠的SDK質(zhì)量也就不太靠譜一些涯肩。那應對這種我們無法改變的情況,最好的辦法就是擴大內(nèi)存巢钓。
擴大內(nèi)存通常有兩種方法:一個是在清單文件中的Application下添加largeHeap="true"這個屬性病苗,另一個就是同一個應用開啟多個進程來擴大一個應用的總內(nèi)存空間。第二種方法其實就很常見了症汹,比方說我使用過個推的SDK硫朦,個推的Service其實就是處在另外一個單獨的進程中。
Android中的內(nèi)存優(yōu)化總的來說就是開源和節(jié)流背镇,開源就是擴大內(nèi)存咬展,節(jié)流就是避免內(nèi)存泄漏。
Binder機制
在Linux中瞒斩,為了避免一個進程對其他進程的干擾破婆,進程之間是相互獨立的。在一個進程中其實還分為用戶空間和內(nèi)核空間胸囱。這里的隔離分為兩個部分祷舀,進程間的隔離和進程內(nèi)的隔離。
既然進程間存在隔離,那其實也是存在著交互裳扯。進程間通信就是IPC抛丽,用戶空間和內(nèi)核空間的通信就是系統(tǒng)調(diào)用。
Linux為了保證獨立性和安全性饰豺,進程之間不能直接相互訪問铺纽,Android是基于Linux的,所以也是需要解決進程間通信的問題哟忍。
其實Linux進程間通信有很多方式,比如管道陷寝、socket等等锅很。為什么Android進程間通信采用了Binder而不是Linux已有的方式,主要是有這么兩點考慮:性能和安全
性能凤跑。在移動設備上對性能要求是比較嚴苛的爆安。Linux傳統(tǒng)的進程間通信比如管道、socket等等進程間通信是需要復制兩次數(shù)據(jù)仔引,而Binder則只需要一次扔仓。所以Binder在性能上是優(yōu)于傳統(tǒng)進程通信的。
安全咖耘。傳統(tǒng)的Linux進程通信是不包含通信雙方的身份驗證的翘簇,這樣會導致一些安全性問題。而Binder機制自帶身份驗證儿倒,從而有效的提高了安全性版保。
Binder是基于CS架構(gòu)的,有四個主要組成部分夫否。
- Client彻犁。客戶端進程凰慈。
- Server汞幢。服務端進程。
- ServiceManager微谓。提供注冊森篷、查詢和返回代理服務對象的功能。
- Binder驅(qū)動豺型。主要負責建立進程間的Binder連接疾宏,進程間的數(shù)據(jù)交互等等底層操作。
Binder機制主要的流程是這樣的:
- 服務端通過Binder驅(qū)動在ServiceManager中注冊我們的服務触创。
- 客戶端通過Binder驅(qū)動查詢在ServiceManager中注冊的服務坎藐。
- ServiceManager通過Binder驅(qū)動返回服務端的代理對象。
- 客戶端拿到服務端的代理對象后即可進行進程間通信。
LruCache的原理
LruCache的核心原理就是對LinkedHashMap的有效利用岩馍,它的內(nèi)部存在一個LinkedHashMap成員變量碉咆。值得我們關注的有四個方法:構(gòu)造方法、get蛀恩、put疫铜、trimToSize。
構(gòu)造方法:在LruCache的構(gòu)造方法中做了兩件事双谆,設置了maxSize壳咕、創(chuàng)建了一個LinkedHashMap。這里值得注意的是LruCache將LinkedHashMap的accessOrder設置為了true顽馋,accessOrder就是遍歷這個LinkedHashMap的輸出順序谓厘。true代表按照訪問順序輸出,false代表按添加順序輸出寸谜,因為通常都是按照添加順序輸出竟稳,所以accessOrder這個屬性默認是false,但我們的LruCache需要按訪問順序輸出熊痴,所以顯式的將accessOrder設置為true他爸。
get方法:本質(zhì)上是調(diào)用LinkedHashMap的get方法,由于我們將accessOrder設置為了true果善,所以每調(diào)用一次get方法诊笤,就會將我們訪問的當前元素放置到這個LinkedHashMap的尾部。
put方法:本質(zhì)上也是調(diào)用了LinkedHashMap的put方法巾陕,由于LinkedHashMap的特性盏混,每調(diào)用一次put方法,也會將新加入的元素放置到LinkedHashMap的尾部惜论。添加之后會調(diào)用trimToSize方法來保證添加后的內(nèi)存不超過maxSize许赃。
trimToSize方法:trimToSize方法的內(nèi)部其實是開啟了一個while(true)的死循環(huán),不斷的從LinkedHashMap的首部刪除元素馆类,直到刪除之后的內(nèi)存小于maxSize之后使用break跳出循環(huán)混聊。
其實到這里我們可以總結(jié)一下,為什么這個算法叫 最近最少使用 算法呢乾巧?原理很簡單句喜,我們的每次put或者get都可以看做一次訪問,由于LinkedHashMap的特性沟于,會將每次訪問到的元素放置到尾部咳胃。當我們的內(nèi)存達到閾值后,會觸發(fā)trimToSize方法來刪除LinkedHashMap首部的元素旷太,直到當前內(nèi)存小于maxSize展懈。為什么刪除首部的元素销睁,原因很明顯:我們最近經(jīng)常訪問的元素都會放置到尾部,那首部的元素肯定就是 最近最少使用 的元素了存崖,因此當內(nèi)存不足時應當優(yōu)先刪除這些元素冻记。
DiskLruCache原理
設計一個圖片的異步加載框架
設計一個圖片加載框架,肯定要用到圖片加載的三級緩存的思想来惧。三級緩存分為內(nèi)存緩存冗栗、本地緩存和網(wǎng)絡緩存。
內(nèi)存緩存:將Bitmap緩存到內(nèi)存中供搀,運行速度快隅居,但是內(nèi)存容量小。
本地緩存:將圖片緩存到文件中葛虐,速度較慢胎源,但容量較大。
網(wǎng)絡緩存:從網(wǎng)絡獲取圖片挡闰,速度受網(wǎng)絡影響。
如果我們設計一個圖片加載框架掰盘,流程一定是這樣的:
- 拿到圖片url后首先從內(nèi)存中查找BItmap摄悯,如果找到直接加載。
- 內(nèi)存中沒有找到愧捕,會從本地緩存中查找奢驯,如果本地緩存可以找到,則直接加載次绘。
- 內(nèi)存和本地都沒有找到瘪阁,這時會從網(wǎng)絡下載圖片,下載到后會加載圖片邮偎,并且將下載到的圖片放到內(nèi)存緩存和本地緩存中管跺。
上面是一些基本的概念,如果是具體的代碼實現(xiàn)的話禾进,大概需要這么幾個方面的文件:
- 首先需要確定我們的內(nèi)存緩存豁跑,這里一般用的都是LruCache。
- 確定本地緩存泻云,通常用的是DiskLruCache艇拍,這里需要注意的是圖片緩存的文件名一般是url被MD5加密后的字符串,為了避免文件名直接暴露圖片的url宠纯。
- 內(nèi)存緩存和本地緩存確定之后卸夕,需要我們創(chuàng)建一個新的類MemeryAndDiskCache,當然婆瓜,名字隨便起快集,這個類包含了之前提到的LruCache和DiskLruCache。在MemeryAndDiskCache這個類中我們定義兩個方法,一個是getBitmap碍讨,另一個是putBitmap治力,對應著圖片的獲取和緩存,內(nèi)部的邏輯也很簡單勃黍。getBitmap中按內(nèi)存宵统、本地的優(yōu)先級去取BItmap,putBitmap中先緩存內(nèi)存覆获,之后緩存到本地马澈。
- 在緩存策略類確定好之后,我們創(chuàng)建一個ImageLoader類弄息,這個類必須包含兩個方法痊班,一個是展示圖片displayImage(url,imageView),另一個是從網(wǎng)絡獲取圖片downloadImage(url,imageView)摹量。在展示圖片方法中首先要通過ImageView.setTag(url)涤伐,將url和imageView進行綁定,這是為了避免在列表中加載網(wǎng)絡圖片時會由于ImageView的復用導致的圖片錯位的bug缨称。之后會從MemeryAndDiskCache中獲取緩存凝果,如果存在,直接加載睦尽;如果不存在器净,則調(diào)用從網(wǎng)絡獲取圖片這個方法。從網(wǎng)絡獲取圖片方法很多当凡,這里我一般都會使用OkHttp+Retrofit山害。當從網(wǎng)絡中獲取到圖片之后,首先判斷一下imageView.getTag()與圖片的url是否一致沿量,如果一致則加載圖片浪慌,如果不一致則不加載圖片,通過這樣的方式避免了列表中異步加載圖片的錯位朴则。同時在獲取到圖片之后會通過MemeryAndDiskCache來緩存圖片眷射。
Android中的事件分發(fā)機制
在我們的手指觸摸到屏幕的時候,事件其實是通過 Activity -> ViewGroup -> View 這樣的流程到達最后響應我們觸摸事件的View佛掖。
說到事件分發(fā)妖碉,必不可少的是這幾個方法:dispatchTouchEvent()、onInterceptTouchEvent()芥被、onTouchEvent欧宜。接下來就按照 Activity -> ViewGroup -> View 的流程來大致說一下事件分發(fā)機制。
我們的手指觸摸到屏幕的時候拴魄,會觸發(fā)一個Action_Down類型的事件冗茸,當前頁面的Activity會首先做出響應席镀,也就是說會走到Activity的dispatchTouchEvent()方法內(nèi)。在這個方法內(nèi)部簡單來說是這么一個邏輯:
- 調(diào)用getWindow.superDispatchTouchEvent()夏漱。
- 如果上一步返回true豪诲,直接返回true;否則就return自己的onTouchEvent()挂绰。
這個邏輯很好理解屎篱,getWindow().superDispatchTouchEvent()如果返回true代表當前事件已經(jīng)被處理,無需調(diào)用自己的onTouchEvent葵蒂;否則代表事件并沒有被處理交播,需要Activity自己處理,也就是調(diào)用自己的onTouchEvent践付。
getWindow()方法返回了一個Window類型的對象秦士,這個我們都知道,在Android中永高,PhoneWindow是Window的唯一實現(xiàn)類隧土。所以這句本質(zhì)上是調(diào)用了PhoneWindow中的superDispatchTouchEvent()。
而在PhoneWindow的這個方法中實際調(diào)用了mDecor.superDispatchTouchEvent(event)命爬。這個mDecor就是DecorView曹傀,它是FrameLayout的一個子類,在DecorView中的superDispatchTouchEvent()中調(diào)用的是super.dispatchTouchEvent()遇骑。到這里就很明顯了卖毁,DecorView是一個FrameLayout的子類揖曾,F(xiàn)rameLayout是一個ViewGroup的子類落萎,本質(zhì)上調(diào)用的還是ViewGroup的dispatchTouchEvent()。
分析到這里炭剪,我們的事件已經(jīng)從Activity傳遞到了ViewGroup练链,接下來我們來分析下ViewGroup中的這幾個事件處理方法。
在ViewGroup中的dispatchTouchEvent()中的邏輯大致如下:
- 通過onInterceptTouchEvent()判斷當前ViewGroup是否攔截事件奴拦,默認的ViewGroup都是不攔截的媒鼓;
- 如果攔截,則return自己的onTouchEvent()错妖;
- 如果不攔截绿鸣,則根據(jù) child.dispatchTouchEvent()的返回值判斷。如果返回true暂氯,則return true潮模;否則return自己的onTouchEvent(),在這里實現(xiàn)了未處理事件的向上傳遞痴施。
通常情況下ViewGroup的onInterceptTouchEvent()都返回false擎厢,也就是不攔截究流。這里需要注意的是事件序列,比如Down事件动遭、Move事件......Up事件芬探,從Down到Up是一個完整的事件序列,對應著手指從按下到抬起這一系列的事件厘惦,如果ViewGroup攔截了Down事件偷仿,那么后續(xù)事件都會交給這個ViewGroup的onTouchEvent。如果ViewGroup攔截的不是Down事件绵估,那么會給之前處理這個Down事件的View發(fā)送一個Action_Cancel類型的事件炎疆,通知子View這個后續(xù)的事件序列已經(jīng)被ViewGroup接管了,子View恢復之前的狀態(tài)即可国裳。
這里舉一個常見的例子:在一個Recyclerview鐘有很多的Button形入,我們首先按下了一個button,然后滑動一段距離再松開缝左,這時候Recyclerview會跟著滑動亿遂,并不會觸發(fā)這個button的點擊事件。這個例子中渺杉,當我們按下button時蛇数,這個button接收到了Action_Down事件,正常情況下后續(xù)的事件序列應該由這個button處理是越。但我們滑動了一段距離耳舅,這時Recyclerview察覺到這是一個滑動操作,攔截了這個事件序列倚评,走了自身的onTouchEvent()方法浦徊,反映在屏幕上就是列表的滑動。而這時button仍然處于按下的狀態(tài)天梧,所以在攔截的時候需要發(fā)送一個Action_Cancel來通知button恢復之前狀態(tài)盔性。
事件分發(fā)最終會走到View的dispatchTouchEvent()中。在View的dispatchTouchEvent()中沒有onInterceptTouchEvent()呢岗,這也很容易理解冕香,View不是ViewGroup,不會包含其他子View后豫,所以也不存在攔截不攔截這一說悉尾。忽略一些細節(jié),View的dispatchTouchEvent()中直接return了自己的onTouchEvent()挫酿。如果onTouchEvent()返回true代表事件被處理构眯,否則未處理的事件會向上傳遞,直到有View處理了事件或者一直沒有處理饭豹,最終到達了Activity的onTouchEvent()終止鸵赖。
這里經(jīng)常有人問onTouch和onTouchEvent的區(qū)別务漩。首先,這兩個方法都在View的dispatchTouchEvent()中它褪,是這么一個邏輯:
- 如果touchListener不為null饵骨,并且這個View是enable的,而且onTouch返回的是true茫打,滿足這三個條件時會直接return true居触,不會走onTouchEvent()方法。
- 上面只要有一個條件不滿足老赤,就會走到onTouchEvent()方法中轮洋。所以onTouch的順序是在onTouchEvent之前的。
View的繪制流程
視圖繪制的起點在ViewRootImpl類的performTraversals()方法抬旺,在這個方法內(nèi)其實是按照順序依次調(diào)用了mView.measure()弊予、mView.layout()、mView.draw()
View的繪制流程分為3步:測量开财、布局汉柒、繪制,分別對應3個方法measure责鳍、layout碾褂、draw。
測量階段历葛。measure方法會被父View調(diào)用正塌,在measure方法中做一些優(yōu)化和準備工作后會調(diào)用onMeasure方法進行實際的自我測量。onMeasure方法在View和ViewGroup做的事情是不一樣的:
- View恤溶。View中的onMeasure方法會計算自己的尺寸并通過setMeasureDimension保存乓诽。
- ViewGroup。ViewGroup中的onMeasure方法會調(diào)用所有子View的measure方法進行自我測量并保存宏娄。然后通過子View的尺寸和位置計算出自己的尺寸并保存问裕。
布局階段逮壁。layout方法會被父View調(diào)用孵坚,layout方法會保存父View傳進來的尺寸和位置,并調(diào)用onLayout進行實際的內(nèi)部布局窥淆。onLayout在View和ViewGroup中做的事情也是不一樣的:
- View卖宠。因為View是沒有子View的,所以View的onLayout里面什么都不做忧饭。
- ViewGroup扛伍。ViewGroup中的onLayout方法會調(diào)用所有子View的layout方法,把尺寸和位置傳給他們词裤,讓他們完成自我的內(nèi)部布局刺洒。
繪制階段鳖宾。draw方法會做一些調(diào)度工作,然后會調(diào)用onDraw方法進行View的自我繪制逆航。draw方法的調(diào)度流程大致是這樣的:
- 繪制背景鼎文。對應drawBackground(Canvas)方法。
- 繪制主體因俐。對應onDraw(Canvas)方法拇惋。
- 繪制子View。對應dispatchDraw(Canvas)方法抹剩。
- 繪制滑動相關和前景撑帖。對應onDrawForeground(Canvas)。
Android源碼中常見的設計模式以及自己在開發(fā)中常用的設計模式
Android與js是如何交互的
在Android中澳眷,Android與js的交互分為兩個方面:Android調(diào)用js里的方法胡嘿、js調(diào)用Android中的方法。
Android調(diào)js钳踊。Android調(diào)js有兩種方法:
- WebView.loadUrl("javascript:js中的方法名")灶平。這種方法的優(yōu)點是很簡潔,缺點是沒有返回值箍土,如果需要拿到js方法的返回值則需要js調(diào)用Android中的方法來拿到這個返回值逢享。
- WebView.evaluateJavaScript("javascript:js中的方法名",ValueCallback)。這種方法比loadUrl好的是可以通過ValueCallback這個回調(diào)拿到js方法的返回值吴藻。缺點是這個方法Android4.4才有瞒爬,兼容性較差。不過放在2018年來說沟堡,市面上絕大多數(shù)App都要求最低版本是4.4了侧但,所以我認為這個兼容性問題不大。
js調(diào)Android航罗。js調(diào)Android有三種方法:
- WebView.addJavascriptInterface()禀横。這是官方解決js調(diào)用Android方法的方案,需要注意的是要在供js調(diào)用的Android方法上加上 @JavascriptInterface 注解粥血,以避免安全漏洞柏锄。這種方案的缺點是Android4.2以前會有安全漏洞,不過在4.2以后已經(jīng)修復了复亏。同樣趾娃,在2018年來說,兼容性問題不大缔御。
- 重寫WebViewClient的shouldOverrideUrlLoading()方法來攔截url抬闷,拿到url后進行解析,如果符合雙方的規(guī)定耕突,即可調(diào)用Android方法笤成。優(yōu)點是避免了Android4.2以前的安全漏洞评架,缺點也很明顯,無法直接拿到調(diào)用Android方法的返回值炕泳,只能通過Android調(diào)用js方法來獲取返回值古程。
- 重寫WebChromClient的onJsPrompt()方法,同前一個方式一樣喊崖,拿到url之后先進行解析挣磨,如果符合雙方規(guī)定,即可調(diào)用Android方法荤懂。最后如果需要返回值茁裙,通過result.confirm("Android方法返回值")即可將Android的返回值返回給js。方法的優(yōu)點是沒有漏洞节仿,也沒有兼容性限制晤锥,同時還可以方便的獲取Android方法的返回值。其實這里需要注意的是在WebChromeClient中除了onJsPrompt之外還有onJsAlert和onJsConfirm方法廊宪。那么為什么不選擇另兩個方法呢矾瘾?原因在于onJsAlert是沒有返回值的,而onJsConfirm只有true和false兩個返回值箭启,同時在前端開發(fā)中prompt方法基本不會被調(diào)用壕翩,所以才會采用onJsPrompt。
熱修復原理
Activity啟動過程
SparseArray原理
SparseArray傅寡,通常來講是Android中用來替代HashMap的一個數(shù)據(jù)結(jié)構(gòu)放妈。
準確來講,是用來替換key為Integer類型荐操,value為Object類型的HashMap芜抒。需要注意的是SparseArray僅僅實現(xiàn)了Cloneable接口陨享,所以不能用Map來聲明牡属。
從內(nèi)部結(jié)構(gòu)來講,SparseArray內(nèi)部由兩個數(shù)組組成丈莺,一個是int[]類型的mKeys屯耸,用來存放所有的鍵拐迁;另一個是Object[]類型的mValues,用來存放所有的值肩民。
最常見的是拿SparseArray跟HashMap來做對比唠亚,由于SparseArray內(nèi)部組成是兩個數(shù)組链方,所以占用內(nèi)存比HashMap要小持痰。我們都知道,增刪改查等操作都首先需要找到相應的鍵值對祟蚀,而SparseArray內(nèi)部是通過二分查找來尋址的工窍,效率很明顯要低于HashMap的常數(shù)級別的時間復雜度割卖。提到二分查找,這里還需要提一下的是二分查找的前提是數(shù)組已經(jīng)是排好序的患雏,沒錯鹏溯,SparseArray中就是按照key進行升序排列的。
綜合起來來說淹仑,SparseArray所占空間優(yōu)于HashMap丙挽,而效率低于HashMap,是典型的時間換空間匀借,適合較小容量的存儲颜阐。
從源碼角度來說,我認為需要注意的是SparseArray的remove()吓肋、put()和gc()方法凳怨。
- remove()。SparseArray的remove()方法并不是直接刪除之后再壓縮數(shù)組是鬼,而是將要刪除的value設置為DELETE這個SparseArray的靜態(tài)屬性肤舞,這個DELETE其實就是一個Object對象,同時會將SparseArray中的mGarbage這個屬性設置為true均蜜,這個屬性是便于在合適的時候調(diào)用自身的gc()方法壓縮數(shù)組來避免浪費空間李剖。這樣可以提高效率,如果將來要添加的key等于刪除的key囤耳,那么會將要添加的value覆蓋DELETE杖爽。
- gc()。SparseArray中的gc()方法跟JVM的GC其實完全沒有任何關系紫皇。gc()方法的內(nèi)部實際上就是一個for循環(huán)慰安,將value不為DELETE的鍵值對往前移動覆蓋value為DELETE的鍵值對來實現(xiàn)數(shù)組的壓縮,同時將mGarbage置為false聪铺,避免內(nèi)存的浪費化焕。
- put()。put方法是這么一個邏輯铃剔,如果通過二分查找在mKeys數(shù)組中找到了key撒桨,那么直接覆蓋value即可。如果沒有找到键兜,會拿到與數(shù)組中與要添加的key最接近的key索引凤类,如果這個索引對應的value為DELETE,則直接把新的value覆蓋DELETE即可普气,在這里可以避免數(shù)組元素的移動谜疤,從而提高了效率。如果value不為DELETE,會判斷mGarbage夷磕,如果為true履肃,則會調(diào)用gc()方法壓縮數(shù)組,之后會找到合適的索引坐桩,將索引之后的鍵值對后移尺棋,插入新的鍵值對,這個過程中可能會觸發(fā)數(shù)組的擴容绵跷。
圖片加載如何避免OOM
我們知道內(nèi)存中的Bitmap大小的計算公式是:長所占像素 * 寬所占像素 * 每個像素所占內(nèi)存膘螟。想避免OOM有兩種方法:等比例縮小長寬、減少每個像素所占的內(nèi)存碾局。
- 等比縮小長寬萍鲸。我們知道Bitmap的創(chuàng)建是通過BitmapFactory的工廠方法,decodeFile()擦俐、decodeStream()脊阴、decodeByteArray()、decodeResource()蚯瞧。這些方法中都有一個Options類型的參數(shù)嘿期,這個Options是BitmapFactory的內(nèi)部類,存儲著BItmap的一些信息埋合。Options中有一個屬性:inSampleSize备徐。我們通過修改inSampleSize可以縮小圖片的長寬,從而減少BItmap所占內(nèi)存甚颂。需要注意的是這個inSampleSize大小需要是2的冪次方蜜猾,如果小于1,代碼會強制讓inSampleSize為1振诬。
- 減少像素所占內(nèi)存蹭睡。Options中有一個屬性inPreferredConfig,默認是ARGB_8888赶么,代表每個像素所占尺寸肩豁。我們可以通過將之修改為RGB_565或者ARGB_4444來減少一半內(nèi)存。
大圖加載
加載高清大圖辫呻,比如清明上河圖清钥,首先屏幕是顯示不下的,而且考慮到內(nèi)存情況放闺,也不可能一次性全部加載到內(nèi)存祟昭。這時候就需要局部加載了,Android中有一個負責局部加載的類:BitmapRegionDecoder怖侦。使用方法很簡單篡悟,通過BitmapRegionDecoder.newInstance()創(chuàng)建對象谜叹,之后調(diào)用decodeRegion(Rect rect, BitmapFactory.Options options)即可。第一個參數(shù)rect是要顯示的區(qū)域恰力,第二個參數(shù)是BitmapFactory中的內(nèi)部類Options叉谜。
Android三方庫的源碼分析
由于源碼分析篇幅太大旗吁,所以這里之貼出我的源碼分析的鏈接(掘金)踩萎。
OkHttp
Retrofit
Retrofit源碼分析1
Retrofit源碼分析2
Retrofit源碼分析3
RxJava
Glide
EventBus
大致是這么一個流程:
register:
- 獲取訂閱者的Class對象
- 使用反射查找訂閱者中的事件處理方法集合
- 遍歷事件處理方法集合,調(diào)用subscribe(subscriber很钓,subscriberMethod)方法香府,在subscribe方法內(nèi):
- 通過subscriberMethod獲取處理的事件類型eventType
- 將訂閱者subscriber和方法subscriberMethod綁在一起形成一個Subscription對象
- 通過subscriptionsByEventType.get(eventType)獲取Subscription集合
- 如果Subscription集合為空則創(chuàng)建一個新的集合,這一步目的是延遲集合的初始化
- 拿到Subscription集合后遍歷這個集合码倦,通過比較事件處理的優(yōu)先級企孩,將新的Subscription對象加入合適的位置
- 通過typesBySubscriber.get(subscriber)獲取事件類型集合
- 如果事件類型集合為空則創(chuàng)建一個新的集合,這一步目的是延遲集合的初始化
- 拿到事件類型集合后將新的事件類型加入到集合中
- 判斷當前事件類型是否是sticky
- 如果當前事件類型不是sticky(粘性事件)袁稽,subscribe(subscriber勿璃,subscriberMethod)到此終結(jié)
- 如果是sticky,判斷EventBus中的一個事件繼承性的屬性推汽,默認是true
- 如果事件繼承性為true补疑,遍歷這個Map類型的stickEvents,通過isAssignableFrom方法判斷當前事件是否是遍歷事件的父類歹撒,如果是則發(fā)送事件
- 如果事件繼承性為false莲组,通過stickyEvents.get(eventType)獲取事件并發(fā)送
post:
- postSticky
- 將事件加入到stickyEvents這個Map類型的集合中
- 調(diào)用post方法
- post
- 將事件加入當前線程的事件隊列中
- 通過while循環(huán)不斷從事件隊列中取出事件并調(diào)用postSingleEvent方法發(fā)送事件
- 在postSingleEvent中,判斷事件繼承性暖夭,默認為true
- 事件繼承性為true锹杈,找到當前事件所有的父類型并調(diào)用postSingleEventForEventType方法發(fā)送事件
- 事件繼承性為false,只發(fā)送當前事件類型的事件
- 在postSingleEventForEventType中迈着,通過subscriptionsByEventType.get(eventClass)獲取Subscription類型集合
- 遍歷這個集合竭望,調(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處理事件
- 在postToSubscription中分為四種情況
unregister:
- 刪除subscriptionsByEventType中與訂閱者相關的所有subscription
- 刪除typesBySubscriber中與訂閱者相關的所有類型
數(shù)據(jù)結(jié)構(gòu)與算法
手寫快排
手寫歸并排序
手寫堆以及堆排序
說一下排序算法的區(qū)別(時間復雜度和空間復雜度)
工作中解決了什么難題,做了什么有成就感的項目(這個問題一定會問到雀扶,所以肯定要做準備)
這個問題其實還是靠平時的積累杖小,對我來說的話肆汹,最有成就感的就是開發(fā)了KCommon這個項目,它大大提升了我的開發(fā)效率予权。