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ì)有很大的不同芒粹,有的生命周期很長(zhǎng)兄纺,有的甚至使用一次之后就不再使用。所以針對(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 依然存活的生命周期很長(zhǎng)的 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ī)棧有兩種操作崖咨,分別是壓棧和出站锻拘。棧幀中存放著局部變量表、方法返回值和方法的正郴鞫祝或異常退出的定義等等署拟。
- 本地方法棧婉宰。 跟 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)然,如果椊霾疲空間擴(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類:?jiǎn)?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)鏈表長(zhǎ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)鏈表長(zhǎng)度大于閾值后會(huì)觸發(fā)樹化绎谦,將鏈表轉(zhuǎn)換為紅黑樹。
- 如果數(shù)組長(zhǎng)度達(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("倉(cāng)庫(kù)已滿:生產(chǎn)暫停");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(new Object());
System.out.println("生產(chǎn)了一個(gè)新產(chǎn)品软免,現(xiàn)庫(kù)存為:" + list.size());
list.notifyAll();
}
}
public void consume() {
synchronized (list) {
while (list.size() == 0) {
System.out.println("庫(kù)存為0:消費(fèi)暫停");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove();
System.out.println("消費(fèi)了一個(gè)產(chǎn)品宫纬,現(xiàn)庫(kù)存為:" + 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è)操作:
- 給 mInstance 分配內(nèi)存
- 調(diào)用 SingleTon 的構(gòu)造方法初始化成員變量
- 將 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è)簡(jiǎn)單的例子: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 可以簡(jiǎn)單理解為安全的 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 的具體流程如下:
- 客戶端(通常是瀏覽器)先向服務(wù)器發(fā)出加密通信的請(qǐng)求
- 支持的協(xié)議版本,比如 TLS 1.0版
- 一個(gè)客戶端生成的隨機(jī)數(shù) random1蚂维,稍后用于生成"對(duì)話密鑰"
- 支持的加密方法戳粒,比如 RSA 公鑰加密
- 支持的壓縮方法
- 服務(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ù)器證書
- 客戶端收到證書之后會(huì)首先會(huì)進(jìn)行驗(yàn)證
- 首先驗(yàn)證證書的安全性
- 驗(yàn)證通過之后苹祟,客戶端會(huì)生成一個(gè)隨機(jī)數(shù) pre-master secret,然后使用證書中的公鑰進(jìn)行加密评雌,然后傳遞給服務(wù)器端
- 服務(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ì)稱秘鑰体谒。
- 然后再后續(xù)的交互中就使用上一步生成的對(duì)稱秘鑰對(duì)傳輸?shù)膬?nèi)容進(jìn)行加密和解密。
TCP三次握手流程
Android面試題
進(jìn)程間通信的方式有哪幾種
AIDL 臼婆、廣播抒痒、文件、socket颁褂、管道
廣播靜態(tài)注冊(cè)和動(dòng)態(tài)注冊(cè)的區(qū)別
- 動(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)行贮聂。
- 當(dāng)廣播為有序廣播時(shí):優(yōu)先級(jí)高的先接收(不分靜態(tài)和動(dòng)態(tài))。同優(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è)的靡狞。
- 當(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)存泄漏的庫(kù)吝羞,我們的項(xiàng)目集成之后 LeakCanary 會(huì)自動(dòng)檢測(cè)應(yīng)用運(yùn)行期間的內(nèi)存泄漏,并將之輸出給我們内颗。
BlockCanary 也是一個(gè)第三方檢測(cè)UI卡頓的庫(kù)钧排,項(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
Android中的動(dòng)畫有哪幾類糟袁,它們的特點(diǎn)和區(qū)別是什么
Android中動(dòng)畫大致分為3類:幀動(dòng)畫、補(bǔ)間動(dòng)畫(Tween Animation)躺盛、屬性動(dòng)畫(Property Animation)项戴。
- 幀動(dòng)畫:通過xml配置一組圖片,動(dòng)態(tài)播放槽惫。很少會(huì)使用周叮。
- 補(bǔ)間動(dòng)畫(Tween Animation):大致分為旋轉(zhuǎn)、透明界斜、縮放仿耽、位移四類操作。很少會(huì)使用各薇。
- 屬性動(dòng)畫(Property Animation):屬性動(dòng)畫是現(xiàn)在使用的最多的一種動(dòng)畫项贺,它比補(bǔ)間動(dòng)畫更加強(qiáng)大。屬性動(dòng)畫大致分為兩種使用類型,分別是 ViewPropertyAnimator 和 ObjectAnimator敬扛。前者適合一些通用的動(dòng)畫晰洒,比如旋轉(zhuǎn)、位移啥箭、縮放和透明谍珊,使用方式也很簡(jiǎ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è)方法里面的邏輯很簡(jiǎn)單岖寞,先判斷msg.callback
是否為 null抡四,如果不為空則執(zhí)行這個(gè) runnable。如果為空則會(huì)執(zhí)行我們的handleMessage
方法仗谆。
-
處理消息梨树。 消息處理的核心其實(shí)就是
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ù)的加載采用分頁(yè)的方式
- 網(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 庫(kù)的時(shí)候優(yōu)先保留 v7 版本的 SO 庫(kù)驶臊,刪掉其他版本的SO庫(kù)。原因是在 2018 年叼丑,v7 版本的 SO 庫(kù)可以滿足市面上絕大多數(shù)的要求关翎,可能八九年前的手機(jī)滿足不了,但我們也沒必要去適配老掉牙的手機(jī)鸠信。實(shí)際開發(fā)中減少 apk 體積的效果是十分顯著的纵寝,如果你使用了很多 SO 庫(kù),比方說一個(gè)版本的SO庫(kù)一共 10M星立,那么只保留 v7 版本爽茴,刪掉 armeabi 和 v8 版本的 SO 庫(kù),一共可以減少 20M 的體積绰垂。
Android 內(nèi)存優(yōu)化
Android的內(nèi)存優(yōu)化在我看來分為兩點(diǎn):避免內(nèi)存泄漏室奏、擴(kuò)大內(nèi)存,其實(shí)就是開源節(jié)流劲装。
其實(shí)內(nèi)存泄漏的本質(zhì)就是較長(zhǎng)生命周期的對(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)存泄漏餐蔬。解決辦法也很簡(jiǎn)單碎紊,不要使用 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è)?Activity 中創(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 等等刃榨。
- 使用第三方庫(kù)未能及時(shí)解綁扯俱。有的三方庫(kù)提供了注冊(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ù)?WebView 就是另外開啟的一個(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è)推的 S DK绳锅,個(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剥扣。 客戶端進(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è)?ServiceManager 中注冊(cè)的服務(wù)。
- ServiceManager 通過 inder 驅(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际前凑仗砑禹樞蜉敵瞿苏?accessOrder 這個(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è)算法叫 最近最少使用 算法呢?原理很簡(jiǎn)單府框,我們的每次 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)部的邏輯也很簡(jiǎn)單。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)前頁(yè)面的 Activity 會(huì)首先做出響應(yīng)仇祭,也就是說會(huì)走到 Activity 的 dispatchTouchEvent()
方法內(nèi)。在這個(gè)方法內(nèi)部簡(jiǎn)單來說是這么一個(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)用所有子 iew的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)?View 是沒有子 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)
弦聂。
-
繪制背景。對(duì)應(yīng)
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)是很簡(jiǎn)潔,缺點(diǎn)是沒有返回值再层,如果需要拿到j(luò)s方法的返回值則需要js調(diào)用Android中的方法來拿到這個(gè)返回值贸铜。
- WebView.evaluateJavaScript("javascript:js中的方法名",ValueCallback)。 這種方法比 loadUrl 好的是可以通過 ValueCallback 這個(gè)回調(diào)拿到 js方法的返回值聂受。缺點(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 覆蓋 DELET 即可撮竿,在這里可以避免數(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ì)算公式是:長(zhǎng)所占像素 * 寬所占像素 * 每個(gè)像素所占內(nèi)存逞盆。想避免 OOM 有兩種方法:等比例縮小長(zhǎng)寬檀蹋、減少每個(gè)像素所占的內(nèi)存。
- 等比縮小長(zhǎng)寬云芦。我們知道 Bitmap 的創(chuàng)建是通過 BitmapFactory 的工廠方法俯逾,
decodeFile()贸桶、decodeStream()、decodeByteArray()桌肴、decodeResource()
皇筛。這些方法中都有一個(gè) Options 類型的參數(shù),這個(gè) Options 是 BitmapFactory 的內(nèi)部類坠七,存儲(chǔ)著 BItmap 的一些信息水醋。Options 中有一個(gè)屬性:inSampleSize。我們通過修改 inSampleSize 可以縮小圖片的長(zhǎng)寬灼捂,從而減少 BItma p 所占內(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署浩。使用方法很簡(jiǎn)單,通過BitmapRegionDecoder.newInstance()創(chuàng)建對(duì)象扫尺,之后調(diào)用decodeRegion(Rect rect, BitmapFactory.Options options)即可筋栋。第一個(gè)參數(shù)rect是要顯示的區(qū)域,第二個(gè)參數(shù)是BitmapFactory中的內(nèi)部類Options正驻。
Android三方庫(kù)的源碼分析
由于源碼分析篇幅太大弊攘,所以這里之貼出我的源碼分析的鏈接(掘金)。
OkHttp
Retrofit
Retrofit源碼分析1 Retrofit源碼分析2 Retrofit源碼分析3
RxJava
Glide
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 處理事件
- POSTING,調(diào)用
- 在 postToSubscription 中分為四種情況
- 在 postSingleEventForEventType 中级零,通過
unregister:
- 刪除 subscriptionsByEventType 中與訂閱者相關(guān)的所有 subscription
- 刪除 typesBySubscriber 中與訂閱者相關(guān)的所有類型
數(shù)據(jù)結(jié)構(gòu)與算法
手寫快排
手寫歸并排序
手寫堆以及堆排序
說一下排序算法的區(qū)別(時(shí)間復(fù)雜度和空間復(fù)雜度)
工作中解決了什么難題,做了什么有成就感的項(xiàng)目(這個(gè)問題一定會(huì)問到边酒,所以肯定要做準(zhǔn)備)
學(xué)習(xí)分享
我的【Github】會(huì)分享一些關(guān)于Android進(jìn)階方面的知識(shí)经柴,也會(huì)分享一下最新的面試題~
如果你熟練掌握【Github】中列出的知識(shí)點(diǎn),相信將會(huì)大大增加你通過前兩輪技術(shù)面試的幾率甚纲!這些內(nèi)容都供大家參考口锭,互相學(xué)習(xí)朦前。
①「Android面試真題解析大全」PDF完整高清版+②「Android面試知識(shí)體系」學(xué)習(xí)思維導(dǎo)圖壓縮包——————可以我的【Github】閱讀下載介杆,最后覺得有幫助、有需要的朋友可以點(diǎn)個(gè)贊
imageimageimage
</article>
0人點(diǎn)贊