正式參加工做以來第一次換工做,很巧的遇上了疫情,倒也省去了面試的奔波。(總結的內容較長,建議使用電腦查看,遇到的算法另寫了一篇 Android-Flutter面經二--算法)javascript
由於這都是面試期間總結的 遇到問題第一反應就是去網上查找你們優秀的回答 這是我第一次發表總結沒有把用到大神們總結的知識點連接加上非常抱歉 之後不會了
前端
3月26號開始了第一家公司的第一面,期間沒考慮過去投其餘公司的簡歷,主要目的是鍛鍊一下本身的面試,畢竟4年沒面試過了,仍是很虛。磕磕絆絆歷時兩個星期五輪面試最終拿到了第一家offer,同時公司人力那邊也知道我有離職的打算就間接催促我提交OA離職,本身很被動,一衝動直接提交OA了,預留了兩個星期的交接時間,也就是有兩個星期的時間找工做,開始變的焦慮,就直接在boss上打開簡歷,並投了五家,而後一天過去沒有任何反應,非常着急,直到第三天才接到4家的面試邀請,懸着的心終於鬆口氣。java
4月15號正式開始面試,4月16號請假在家安心面試,一天三家公司共經歷了五輪面試,每輪面試都是一個小時左右,由於我表達了時間緊迫因此面試公司的HR跟進的很給力第三天有兩個公司就都進行到了第五輪面試。4月20號基本算是拿到了3個offer也有個比較滿意的公司,後續幾天另外2家的複試也開始了,就沒約新的面試公司,也過了。目前還有3家到了最終面,由於週期太長就直接拒絕了。android
一共是面了7家公司,共經歷了27輪面試,拿到了4個offer(有一家是flutter開發),3家hr面試直接拒絕沒參加。7家公司有BBA大廠也有D輪中型公司。程序員
參加的公司面試流程都很標準,會提早發郵件預定1個小時的面試時間。面試
也有一面很滿意以後一次性解決的,3個小時的面試算法
說了這麼多廢話,進入正題吧,面試知識點,我只大概總結下,其實每一個知識點均可以深刻拓展,分了五個模塊java、Android、網絡、dart、flutter數據庫
垃圾回收須要完成兩件事:找到垃圾,回收垃圾。 找到垃圾通常的話有兩種方法:編程
新生代對象分爲三個區域:Eden 區和兩個 Survivor 區。新建立的對象都放在 Eden區,當 Eden 區的內存達到閾值以後會觸發 Minor GC,這時會將存活的對象複製到一個 Survivor 區中,這些存活對象的生命存活計數會加一。這時 Eden 區會閒置,當再一次達到閾值觸發 Minor GC 時,會將Eden區和以前一個 Survivor 區中存活的對象複製到另外一個 Survivor 區中,採用的是我以前提到的複製算法,同時它們的生命存活計數也會加一。設計模式
這個過程會持續不少遍,直到對象的存活計數達到必定的閾值後會觸發一個叫作晉升的現象:新生代的這個對象會被放置到老年代中。 老年代中的對象都是通過屢次 GC 依然存活的生命週期很長的 Java 對象。當老年代的內存達到閾值後會觸發 Major GC,採用的是標記整理算法。
JVM 的內存區域能夠分爲兩類:線程私有和區域和線程共有的區域。 線程私有的區域:程序計數器、JVM 虛擬機棧、本地方法棧 線程共有的區域:堆、方法區、運行時常量池
其實除了程序計數器,其餘的部分都會發生 OOM。
Java內存模型規定了全部的變量都存儲在主內存中,每條線程還有本身的工做內存,線程的工做內存中保存了該線程中是用到的變量的主內存副本拷貝,線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量的傳遞均須要本身的工做內存和主存之間進行數據同步進行。
原子性
在Java中,爲了保證原子性,提供了兩個高級的字節碼指令monitorenter
和monitorexit
。在synchronized的實現原理文章中,介紹過,這兩個字節碼,在Java中對應的關鍵字就是synchronized
。
所以,在Java中能夠使用synchronized
來保證方法和代碼塊內的操做是原子性的。
可見性
Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值的這種依賴主內存做爲傳遞媒介的方式來實現的。
Java中的volatile
關鍵字提供了一個功能,那就是被其修飾的變量在被修改後能夠當即同步到主內存,被其修飾的變量在每次是用以前都從主內存刷新。所以,能夠使用volatile
來保證多線程操做時變量的可見性。
除了volatile
,Java中的synchronized
和final
兩個關鍵字也能夠實現可見性。只不過實現方式不一樣,這裏再也不展開了。
有序性
在Java中,能夠使用synchronized
和volatile
來保證多線程之間操做的有序性。實現方式有所區別:
volatile
關鍵字會禁止指令重排。synchronized
關鍵字保證同一時刻只容許一條線程操做。
好了,這裏簡單的介紹完了Java併發編程中解決原子性、可見性以及有序性能夠使用的關鍵字。讀者可能發現了,好像synchronized
關鍵字是萬能的,他能夠同時知足以上三種特性,這其實也是不少人濫用synchronized
的緣由。
可是synchronized
是比較影響性能的,雖然編譯器提供了不少鎖優化技術,可是也不建議過分使用
Java 中類加載分爲 3 個步驟:加載、連接、初始化。
類加載器大體分爲3類:啓動類加載器、擴展類加載器、應用程序類加載器。
jre/lib
下的jar
文件。jre/lib/ext
下的jar
文件。classpath
下的文件。所謂的雙親委派模型就是當加載一個類時,會優先使用父類加載器加載,當父類加載器沒法加載時纔會使用子類加載器去加載。這麼作的目的是爲了不類的重複加載。
HashMap 的內部能夠看作數組+鏈表的複合結構。數組被分爲一個個的桶(bucket)。哈希值決定了鍵值對在數組中的尋址。具備相同哈希值的鍵值對會組成鏈表。須要注意的是當鏈表長度超過閾值(默認是8)的時候會觸發樹化,鏈表會變成樹形結構。
把握HashMap的原理須要關注4個方法:hash、put、get、resize。
join 方法一般是保證線程間順序調度的一個方法,它是 Thread 類中的方法。比方說在線程 A 中執行線程 B.join()
,這時線程 A 會進入等待狀態,直到線程 B 執行完畢以後纔會喚醒,繼續執行A線程中的後續方法。
join 方法能夠傳時間參數,也能夠不傳參數,不傳參數實際上調用的是 join(0)
。它的原理實際上是使用了 wait 方法,join 的原理以下:
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
複製代碼複製代碼
通常提到 volatile,就不得不提到內存模型相關的概念。咱們都知道,在程序運行中,每條指令都是由 CPU 執行的,而指令的執行過程當中,勢必涉及到數據的讀取和寫入。程序運行中的數據都存放在主存中,這樣會有一個問題,因爲 CPU 的執行速度是要遠高於主存的讀寫速度,因此直接從主存中讀寫數據會下降 CPU 的效率。爲了解決這個問題,就有了高速緩存的概念,在每一個 CPU 中都有高速緩存,它會事先從主存中讀取數據,在 CPU 運算以後在合適的時候刷新到主存中。
這樣的運行模式在單線程中是沒有任何問題的,但在多線程中,會致使緩存一致性的問題。舉個簡單的例子:i=i+1
,在兩個線程中執行這句代碼,假設i的初始值爲0。咱們指望兩個線程運行後獲得2,那麼有這樣的一種狀況,兩個線程都從主存中讀取i到各自的高速緩存中,這時候兩個線程中的i都爲0。在線程1執行完畢獲得i=1
,將之刷新到主存後,線程2開始執行,因爲線程2中的i是高速緩存中的0,因此在執行完線程2以後刷新到主存的i仍舊是1。
因此這就致使了對共享變量的緩存一致性的問題,那麼爲了解決這個問題,提出了緩存一致性協議:當 CPU 在寫數據時,若是發現操做的是共享變量,它會通知其餘 CPU 將它們內部的這個共享變量置爲無效狀態,當其餘 CPU 讀取緩存中的共享變量時,發現這個變量是無效的,它會重新從主存中讀取最新的值。
在Java的多線程開發中,有三個重要概念:原子性、可見性、有序性。
volatile的原理是在生成的彙編代碼中多了一個lock前綴指令,這個前綴指令至關於一個內存屏障,這個內存屏障有3個做用:
volatile
它所修飾的變量不保留拷貝,直接訪問主內存中的。
在Java內存模型中,有main memory,每一個線程也有本身的memory (例如寄存器)。爲了性能,一個線程會在本身的memory中保持要訪問的變量的副本。這樣就會出現同一個變量在某個瞬間,在一個線程的memory中的值可能與另一個線程memory中的值,或者main memory中的值不一致的狀況。 一個變量聲明爲volatile,就意味着這個變量是隨時會被其餘線程修改的,所以不能將它cache在線程memory中。
使用場景
您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件:
1)對變量的寫操做不依賴於當前值。
2)該變量沒有包含在具備其餘變量的不變式中。
volatile最適用一個線程寫,多個線程讀的場合。
若是有多個線程併發寫操做,仍然須要使用鎖或者線程安全的容器或者原子變量來代替。
synchronized
當它用來修飾一個方法或者一個代碼塊的時候,可以保證在同一時刻最多隻有一個線程執行該段代碼。
區別
線程安全包含原子性和可見性兩個方面,Java的同步機制都是圍繞這兩個方面來確保線程安全的。
關鍵字volatile主要使用的場合是在多個線程中能夠感知實例變量被修改,而且能夠得到最新的值使用,也就是多線程讀取共享變量時能夠得到最新值使用。
關鍵字volatile提示線程每次從共享內存中讀取變量,而不是私有內存中讀取,這樣就保證了同步數據的可見性。可是要注意的是:若是修改實例變量中的數據
ThreadLocal的做用是提供線程內的局部變量,說白了,就是在各線程內部建立一個變量的副本,相比於使用各類鎖機制訪問變量,ThreadLocal的思想就是用空間換時間,使各線程都能訪問屬於本身這一份的變量副本,變量值不互相干擾,減小同一個線程內的多個函數或者組件之間一些公共變量傳遞的複雜度。
get函數用來獲取與當前線程關聯的ThreadLocal的值,若是當前線程沒有該ThreadLocal的值,則調用initialValue函數獲取初始值返回,initialValue是protected類型的,因此通常咱們使用時須要繼承該函數,給出初始值。而set函數是用來設置當前線程的該ThreadLocal的值,remove函數用來刪除ThreadLocal綁定的值,在某些狀況下須要手動調用,防止內存泄露。
生產者消費者模式要保證的是當緩衝區滿的時候生產者再也不生產對象,當緩衝區空時,消費者再也不消費對象。實現機制就是當緩衝區滿時讓生產者處於等待狀態,當緩衝區爲空時讓消費者處於等待狀態。當生產者生產了一個對象後會喚醒消費者,當消費者消費一個對象後會喚醒生產者。
三種種實現方式:wait 和 notify、await 和 signal、BlockingQueue。
//wait和notify
import java.util.LinkedList;
public class StorageWithWaitAndNotify {
private final int MAX_SIZE = 10;
private LinkedList<Object> list = new LinkedList<Object>();
public void produce() {
synchronized (list) {
while (list.size() == MAX_SIZE) {
System.out.println("倉庫已滿:生產暫停");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(new Object());
System.out.println("生產了一個新產品,現庫存爲:" + list.size());
list.notifyAll();
}
}
public void consume() {
synchronized (list) {
while (list.size() == 0) {
System.out.println("庫存爲0:消費暫停");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove();
System.out.println("消費了一個產品,現庫存爲:" + list.size());
list.notifyAll();
}
}
}
複製代碼複製代碼
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class StorageWithAwaitAndSignal {
private final int MAX_SIZE = 10;
private ReentrantLock mLock = new ReentrantLock();
private Condition mEmpty = mLock.newCondition();
private Condition mFull = mLock.newCondition();
private LinkedList<Object> mList = new LinkedList<Object>();
public void produce() {
mLock.lock();
while (mList.size() == MAX_SIZE) {
System.out.println("緩衝區滿,暫停生產");
try {
mFull.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mList.add(new Object());
System.out.println("生產了一個新產品,現容量爲:" + mList.size());
mEmpty.signalAll();
mLock.unlock();
}
public void consume() {
mLock.lock();
while (mList.size() == 0) {
System.out.println("緩衝區爲空,暫停消費");
try {
mEmpty.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mList.remove();
System.out.println("消費了一個產品,現容量爲:" + mList.size());
mFull.signalAll();
mLock.unlock();
}
}
複製代碼複製代碼
import java.util.concurrent.LinkedBlockingQueue;
public class StorageWithBlockingQueue {
private final int MAX_SIZE = 10;
private LinkedBlockingQueue<Object> list = new LinkedBlockingQueue<Object>(MAX_SIZE);
public void produce() {
if (list.size() == MAX_SIZE) {
System.out.println("緩衝區已滿,暫停生產");
}
try {
list.put(new Object());
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生產了一個產品,現容量爲:" + list.size());
}
public void consume() {
if (list.size() == 0) {
System.out.println("緩衝區爲空,暫停消費");
}
try {
list.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消費了一個產品,現容量爲:" + list.size());
}
}
複製代碼複製代碼
final 能夠修飾類、變量和方法。修飾類表明這個類不可被繼承。修飾變量表明此變量不可被改變。修飾方法表示此方法不可被重寫 (override)。
finally 是保證重點代碼必定會執行的一種機制。一般是使用 try-finally 或者 try-catch-finally 來進行文件流的關閉等操做。
finalize 是 Object 類中的一個方法,它的設計目的是保證對象在垃圾收集前完成特定資源的回收。finalize 機制如今已經不推薦使用,而且在 JDK 9已經被標記爲 deprecated。
Java 中常見的單例模式實現有這麼幾種:餓漢式、雙重判斷的懶漢式、靜態內部類實現的單例、枚舉實現的單例。 這裏着重講一下雙重判斷的懶漢式和靜態內部類實現的單例。
雙重判斷的懶漢式:
public class SingleTon {
//須要注意的是volatile
private static volatile SingleTon mInstance;
private SingleTon() {
}
public static SingleTon getInstance() {
if (mInstance == null) {
synchronized (SingleTon.class) {
if (mInstance == null) {
mInstance=new SingleTon();
}
}
}
return mInstance;
}
}
複製代碼複製代碼
雙重判斷的懶漢式單例既知足了延遲初始化,又知足了線程安全。經過 synchronized 包裹代碼來實現線程安全,經過雙重判斷來提升程序執行的效率。這裏須要注意的是單例對象實例須要有 volatile 修飾,若是沒有 volatile 修飾,在多線程狀況下可能會出現問題。緣由是這樣的,mInstance=new SingleTon()
這一句代碼並非一個原子操做,它包含三個操做:
咱們知道 JVM 會發生指令重排,正常的執行順序是1-2-3
,但發生指令重排後可能會致使1-3-2
。咱們考慮這樣一種狀況,當線程 A 執行到1-3-2
的3步驟暫停了,這時候線程 B 調用了 getInstance,走到了最外層的if判斷上,因爲最外層的 if 判斷並無 synchronized 包裹,因此能夠執行到這一句,這時候因爲線程 A 已經執行了步驟3,此時 mInstance 已經不爲 null 了,因此線程B直接返回了 mInstance。但其實咱們知道,完整的初始化必須走完這三個步驟,因爲線程 A 只走了兩個步驟,因此必定會報錯的。
解決的辦法就是使用 volatile 修飾 mInstance,咱們知道 volatile 有兩個做用:保證可見性和禁止指令重排,在這裏關鍵在於禁止指令重排,禁止指令重排後保證了不會發生上述問題。
靜態內部類實現的單例:
class SingletonWithInnerClass {
private SingletonWithInnerClass() {
}
private static class SingletonHolder{
private static SingletonWithInnerClass INSTANCE=new SingletonWithInnerClass();
}
public SingletonWithInnerClass getInstance() {
return SingletonHolder.INSTANCE;
}
}
複製代碼複製代碼
因爲外部類的加載並不會致使內部類當即加載,只有當調用 getInstance 的時候纔會加載內部類,因此實現了延遲初始化。因爲類只會被加載一次,而且類加載也是線程安全的,因此知足咱們全部的需求。靜態內部類實現的單例也是最爲推薦的一種方式。
Java中引用類型分爲四類:強引用、軟引用、弱引用、虛引用。
Exception 和 Error 都繼承於 Throwable,在 Java 中,只有 Throwable 類型的對象才能被 throw 或者 catch,它是異常處理機制的基本組成類型。
Exception 和 Error 體現了 Java 對不一樣異常狀況的分類。Exception 是程序正常運行中,能夠預料的意外狀況,可能而且應該被捕獲,進行相應的處理。
Error 是指在正常狀況下,不大可能出現的狀況,絕大部分 Error 都會使程序處於非正常、不可恢復的狀態。既然是非正常,因此不便於也不須要捕獲,常見的 OutOfMemoryError 就是 Error 的子類。
Exception 又分爲 checked Exception 和 unchecked Exception。
任意一個Java對象,都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()、
wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,能夠
實現等待/通知模式
強引用:
1. 正常建立的對象,只要引用存在,永遠不會被GC回收,即便OOM
Object obj = new Object();
2. 若是要中斷強引用和某個對象的關聯,爲其賦值null,這樣GC就會在合適的時候回收對象
3. Vector類的clear()方法就是經過賦值null進行清除
軟引用
1. 內存溢出以前進行回收,GC時內存不足時回收,若是內存足夠就不回收
2. 使用場景:在內存足夠的狀況下進行緩存,提高速度,內存不足時JVM自動回收
3. 能夠和引用隊列ReferenceQueue聯合使用,若是軟引用所引用的對象被JVM回收,這個軟引用就會被加入到與之關聯的引用隊列中
弱引用
1. 每次GC時回收,不管內存是否足夠
2. 使用場景:a. ThreadLocalMap防止內存泄漏 b. 監控對象是否將要被回收
3. 弱引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是弱引用所引用的對象被JVM回收,這個軟引用就會被加入到與之關聯的引用隊列中
虛引用
1. 每次垃圾回收時都會被回收,主要用於監測對象是否已經從內存中刪除
2. 虛引用必須和引用隊列關聯使用, 當垃圾回收器準備回收一個對象時,若是發現它還有虛引用,就會把這個虛引用加入到與之 關聯的引用隊列中
3. 程序能夠經過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。若是程序發現某個虛引用已經被加入到引用隊列,那麼就能夠在所引用的對象的內存被回收以前採起必要的行動
app冷啓動: 當應用啓動時,後臺沒有該應用的進程,這時系統會從新建立一個新的進程分配給該應用, 這個啓動方式就叫作冷啓動(後臺不存在該應用進程)。冷啓動由於系統會從新建立一個新的進程分配給它,因此會先建立和初始化Application類,再建立和初始化MainActivity
類(包括一系列的測量、佈局、繪製),最後顯示在界面上。
app熱啓動: 當應用已經被打開, 可是被按下返回鍵、Home鍵等按鍵時回到桌面或者是其餘程序的時候,再從新打開該app時, 這個方式叫作熱啓動(後臺已經存在該應用進程)。熱啓動由於會從已有的進程中來啓動,因此熱啓動就不會走Application這步了,而是直接走MainActivity
(包括一系列的測量、佈局、繪製),因此熱啓動的過程只須要建立和初始化一個MainActivity
就好了,而沒必要建立和初始化Application
冷啓動的流程
當點擊app的啓動圖標時,安卓系統會從Zygote進程中fork建立出一個新的進程分配給該應用,以後會依次建立和初始化Application類、建立MainActivity
類、加載主題樣式Theme中的windowBackground
等屬性設置給MainActivity
以及配置Activity層級上的一些屬性、再inflate佈局、當onCreate
/onStart
/onResume
方法都走完了後最後才進行contentView
的measure
/layout
/draw
顯示在界面上冷啓動的生命週期簡要流程:
Application構造方法 –> attachBaseContext()
–>onCreate
–>Activity構造方法 –> onCreate()
–> 配置主體中的背景等操做 –>onStart()
–> onResume()
–> 測量、佈局、繪製顯示
冷啓動的優化主要是視覺上的優化,解決白屏問題,提升用戶體驗,因此經過上面冷啓動的過程。能作的優化以下:
一、減小onCreate()方法的工做量二、不要讓Application參與業務的操做三、不要在Application進行耗時操做四、不要以靜態變量的方式在Application保存數據五、減小布局的複雜度和層級六、減小主線程耗時
①點擊桌面App圖標,Launcher進程採用Binder IPC向system_server進程發起startActivity請求;
②system_server進程接收到請求後,向zygote進程發送建立進程的請求;
③Zygote進程fork出新的子進程,即App進程;
④App進程,經過Binder IPC向sytem_server進程發起attachApplication請求;
⑤system_server進程在收到請求後,進行一系列準備工做後,再經過binder IPC向App進程發送scheduleLaunchActivity請求;
⑥App進程的binder線程(ApplicationThread)在收到請求後,經過handler向主線程發送LAUNCH_ACTIVITY消息;
⑦主線程在收到Message後,經過發射機制建立目標Activity,並回調Activity.onCreate()等方法。
⑧到此,App便正式啓動,開始進入Activity生命週期,執行完onCreate/onStart/onResume方法,UI渲染結束後即可以看到App的主界面。
1. Activity1調用startActivity,實際會調用Instrumentation類的execStartActivity方法,Instrumentation是系統用來監控Activity運行的一個類,Activity的整個生命週期都有它的影子。(1- 4)
2. 經過跨進程的binder調用,進入到ActivityManagerService中,其內部會處理Activity棧,通知Activity1 Pause,Activity1 執行Pause 後告知AMS。(5 - 29)
3. 在ActivityManagerService中的startProcessLocked中調用了Process.start()方法。並經過鏈接調用Zygote的native方法forkAndSpecialize,執行fork任務。以後再經過跨進程調用進入到Activity2所在的進程中。(30 - 36)
4. ApplicationThread是一個binder對象,其運行在binder線程池中,內部包含一個H類,該類繼承於類Handler。主線程發起bind Application,AMS 會作一些配置工做,而後讓主線程 bind ApplicationThread,ApplicationThread將啓動Activity2的信息經過H對象發送給主線程。發送的消息是EXECUTE_TRANSACTION,消息體是一個 ClientTransaction,即 LaunchActivityItem。主線程拿到Activity2的信息後,調用Instrumentation類的newActivity方法,其內經過ClassLoader建立Activity2實例。(37 - 40)
5. 通知Activity2去performCreate。(41 - 最後)
注:如今發送的都是EXECUTE_TRANSACTION ,經過 TransactionExecutor 來執行 ClientTransaction, ClientTransaction 中包含各類 ClientTransactionItem,如 PauseActivityItem、LaunchActivityItem、StopActivityItem、ResumeActivityItem、DestroyActivityItem 等,這些Item的execute方法來處理相應的handle,如handlePauseActivity、handleLaunchActivity等,通知相應的Activity來perform。
1.啓動Activity:系統會先調用onCreate方法,而後調用onStart方法,最後調用onResume,Activity進入運行狀態。
2.當前Activity被其餘Activity覆蓋其上或被鎖屏:系統會調用onPause方法,暫停當前Activity的執行。
3.當前Activity由被覆蓋狀態回到前臺或解鎖屏:系統會調用onResume方法,再次進入運行狀態。
4.當前Activity轉到新的Activity界面或按Home鍵回到主屏,自身退居後臺:系統會先調用onPause方法,而後調用onStop方法,進入停滯狀態。
5.用戶後退回到此Activity:系統會先調用onRestart方法,而後調用onStart方法,最後調用onResume方法,再次進入運行狀態。
6.當前Activity處於被覆蓋狀態或者後臺不可見狀態,即第2步和第4步,系統內存不足,殺死當前Activity,然後用戶退回當前Activity:再次調用onCreate方法、onStart方法、onResume方法,進入運行狀態。
7.用戶退出當前Activity:系統先調用onPause方法,而後調用onStop方法,最後調用onDestory方法,結束當前Activity。
standard:標準模式:若是在mainfest中不設置就默認standard;standard就是新建一個Activity就在棧中新建一個activity實例;
singleTop:棧頂複用模式:與standard相比棧頂複用能夠有效減小activity重複建立對資源的消耗,可是這要根據具體狀況而定,不能一律而論;
singleTask:棧內單例模式,棧內只有一個activity實例,棧內已存activity實例,在其餘activity中start這個activity,Android直接把這個實例上面其餘activity實例踢出棧GC掉;
singleInstance :堆內單例:整個手機操做系統裏面只有一個實例存在就是內存單例;
在singleTop、singleTask、singleInstance 中若是在應用內存在Activity實例,而且再次發生startActivity(Intent intent)回到Activity後,因爲並非從新建立Activity而是複用棧中的實例,所以Activity再獲取焦點後並沒調用onCreate、onStart,而是直接調用了onNewIntent(Intent intent)函數;
LauchMode Instance
standard 郵件、mainfest中沒有配置就默認標準模式
singleTop 登陸頁面、WXPayEntryActivity、WXEntryActivity 、推送通知欄
singleTask 程序模塊邏輯入口:主頁面(Fragment的containerActivity)、WebView頁面、掃一掃頁面、電商中:購物界面,確認訂單界面,付款界面
singleInstance 系統Launcher、鎖屏鍵、來電顯示等系統應用
第一高:前臺進程
前臺進程是Android系統中最重要的進程,是與用戶正在交互的進程。
第二高:可見進程
可見進程指部分程序界面可以被用戶看見,卻不在前臺與用戶交互。
第三高:服務進程
一個包含已啓動服務的進程就是服務進程,服務沒有用戶界面,不與用戶直接交互,但可以在後臺長期運行,提供用戶所關心的重要功能。
第四高:後臺進程
若是一個進程不包含任何已經啓動的服務,並且沒有用戶可見的Activity,則這個進程就是後臺進程。
第五高:空進程
空進程是不包含任何活躍組件的進程。在系統資源緊張時會被首先清楚。
startService 和bindService 區別
startService: onCreate -> onStartCommand -> onDestory ,在屢次調用startService的時候,onCreate不重複執行,可是onStartCommand會執行。startService調用了這後,會一直存在,直到其調用了stopService。
bindService : onCreate -> onBind -> onUnbind -> onDestory,屢次調用bindService,onCreate及onBind都只執行一次。它生命週期跟隨其調用者,調用者釋放的時候,必須對該Service解綁,當全部綁定所有取消後,系統即會銷燬該服務。 bindService 的方式經過onServiceConnected方法,獲取到Service對象,經過該對象能夠直接操做到Service內部的方法,從而實現的Service 與調用者之間的交互。
使用場景
若是想要啓動一個後臺服務長期進行某項任務,那麼使用startService
若是隻是短暫的使用,那麼使用bindService。
若是想啓動一個後臺服務長期進行任務,且這個過程當中須要與調用者進行交互,那麼能夠二者同時使用,或者使用startService + BoardCast/ EventBus 等方法。
對於既使用startService,又使用bindService的狀況,結束服務時須要注意的事項:
順便提一下IntentService,與Service的區別在於它內部封裝了一個工做線程,也就是說,在其內部onHandleIntent的代碼都是在子線程裏面工做的。
IntentService是一個經過Context.startService(Intent)啓動能夠處理異步請求的Service,使用時你只須要繼承IntentService和重寫其中的onHandleIntent(Intent)方法接收一個Intent對象,在適當的時候會中止本身(通常在工做完成的時候). 全部的請求的處理都在一個工做線程中完成,它們會交替執行(但不會阻塞主線程的執行),一次只能執行一個請求。
這是一個基於消息的服務,每次啓動該服務並非立刻處理你的工做,而是首先會建立對應的Looper,Handler而且在MessageQueue中添加的附帶客戶Intent的Message對象,當Looper發現有Message的時候接着獲得Intent對象經過在onHandleIntent((Intent)msg.obj)中調用你的處理程序.處理完後即會中止本身的服務.意思是Intent的生命週期跟你的處理的任務是一致的.因此這個類用下載任務中很是好,下載任務結束後服務自身就會結束退出.
AIDL 、廣播、文件、socket、管道
Android 中經常使用的性能優化工具包括這些:Android Studio 自帶的 Android Profiler、LeakCanary、BlockCanary
Android 自帶的 Android Profiler 其實就很好用,Android Profiler 能夠檢測三個方面的性能問題:CPU、MEMORY、NETWORK。
LeakCanary 是一個第三方的檢測內存泄漏的庫,咱們的項目集成以後 LeakCanary 會自動檢測應用運行期間的內存泄漏,並將之輸出給咱們。
BlockCanary 也是一個第三方檢測UI卡頓的庫,項目集成後Block也會自動檢測應用運行期間的UI卡頓,並將之輸出給咱們。
jar/apk/dex
,能夠從 SD卡中加載未安裝的 apkAndroid中動畫大體分爲3類:幀動畫、補間動畫(Tween Animation)、屬性動畫(Property Animation)。
View.animate()
便可獲得 ViewPropertyAnimator,以後進行相應的動畫操做便可。後者適合用於爲咱們的自定義控件添加動畫,固然首先咱們應該在自定義 View 中添加相應的 getXXX()
和 setXXX()
相應屬性的 getter 和 setter 方法,這裏須要注意的是在 setter 方法內改變了自定義 View 中的屬性後要調用 invalidate()
來刷新View的繪製。以後調用 ObjectAnimator.of
屬性類型()返回一個 ObjectAnimator,調用 start()
方法啓動動畫便可。補間動畫與屬性動畫的區別:
TimeInterpolator(時間插值器)
做用:根據時間流逝的百分比計算出當前屬性值改變的百分比
系統已有的插值器:
TypeEvaluator(類型估值算法,即估值器):
做用:根據當前屬性改變的百分比來計算改變後的屬性值。
系統已有的估值器:
說到 Handler,就不得不提與之密切相關的這幾個類:Message、MessageQueue,Looper。
handler.post(runnable)
時傳入的 Runnable 類型的任務。post 事件的本質也是建立了一個 Message,將咱們傳入的這個 runnable 賦值給建立的Message的 callback 這個成員變量。next()
方法,它會返回下一個待處理的消息。Looper.prepare()
建立 Looper,以後還得調用Looper.loop()
開啓輪詢。咱們着重看一下這兩個方法。prepare()
。 這個方法作了兩件事:首先經過ThreadLocal.get()
獲取當前線程中的Looper,若是不爲空,則會拋出一個RunTimeException,意思是一個線程不能建立2個Looper。若是爲null則執行下一步。第二步是建立了一個Looper,並經過 ThreadLocal.set(looper)。
將咱們建立的Looper與當前線程綁定。這裏須要提一下的是消息隊列的建立其實就發生在Looper的構造方法中。loop()
。 這個方法開啓了整個事件機制的輪詢。它的本質是開啓了一個死循環,不斷的經過 MessageQueue的next()
方法獲取消息。拿到消息後會調用 msg.target.dispatchMessage()
來作處理。其實咱們在說到 Message 的時候提到過,msg.target
其實就是發送這個消息的 handler。這句代碼的本質就是調用 handler的dispatchMessage()。
msg.target = this
實現了消息與當前 handler 的綁定。而後經過queue.enqueueMessage
實現了消息入隊。dispatchMessage()
這個方法。這個方法裏面的邏輯很簡單,先判斷 msg.callback
是否爲 null,若是不爲空則執行這個 runnable。若是爲空則會執行咱們的handleMessage
方法。ActivityThread.handleResumeActivity() 中初始化了ViewRootImpl 而後執行 requestLayout()進行線程校驗
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient) {
if (!a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
} else {複製代碼
Android 中的性能優化在我看來分爲如下幾個方面:內存優化、佈局優化、網絡優化、安裝包優化。
\< include \>
標籤\< ViewStub \>
標籤來加載不經常使用的佈局\< Merge \>
標籤來減小布局的嵌套層次Android的內存優化在我看來分爲兩點:避免內存泄漏、擴大內存,其實就是開源節流。
其實內存泄漏的本質就是較長生命週期的對象引用了較短生命週期的對象。
handler.removeCallbacksAndMessages
來取消延時事件。disposable.dispose()
來取消操做。擴大內存
爲何要擴大咱們的內存呢?有時候咱們實際開發中不可避免的要使用不少第三方商業的 SDK,這些 SDK 其實有好有壞,大廠的 SDK 可能內存泄漏會少一些,但一些小廠的 SDK 質量也就不太靠譜一些。那應對這種咱們沒法改變的狀況,最好的辦法就是擴大內存。
擴大內存一般有兩種方法:一個是在清單文件中的 Application 下添加largeHeap="true"
這個屬性,另外一個就是同一個應用開啓多個進程來擴大一個應用的總內存空間。第二種方法其實就很常見了,比方說我使用過個推的 S DK,個推的 Service 其實就是處在另一個單獨的進程中。
Android 中的內存優化總的來講就是開源和節流,開源就是擴大內存,節流就是避免內存泄漏。
在Linux中,爲了不一個進程對其餘進程的干擾,進程之間是相互獨立的。在一個進程中其實還分爲用戶空間和內核空間。這裏的隔離分爲兩個部分,進程間的隔離和進程內的隔離。
既然進程間存在隔離,那其實也是存在着交互。進程間通訊就是 IPC,用戶空間和內核空間的通訊就是系統調用。
Linux 爲了保證獨立性和安全性,進程之間不能直接相互訪問,Android 是基於 Linux 的,因此也是須要解決進程間通訊的問題。
其實 Linux 進程間通訊有不少方式,好比管道、socket 等等。爲何 Android 進程間通訊採用了Binder而不是 Linux
已有的方式,主要是有這麼兩點考慮:性能和安全
Binder 是基於 CS 架構的,有四個主要組成部分。
Binder 機制主要的流程是這樣的:
LruCache 的核心原理就是對 LinkedHashMap 的有效利用,它的內部存在一個 LinkedHashMap 成員變量。值得咱們關注的有四個方法:構造方法、get、put、trimToSize。
其實到這裏咱們能夠總結一下,爲何這個算法叫 最近最少使用 算法呢?原理很簡單,咱們的每次 put 或者get均可以看作一次訪問,因爲 LinkedHashMap 的特性,會將每次訪問到的元素放置到尾部。當咱們的內存達到閾值後,會觸發 trimToSize 方法來刪除 LinkedHashMap 首部的元素,直到當前內存小於 maxSize。爲何刪除首部的元素,緣由很明顯:咱們最近常常訪問的元素都會放置到尾部,那首部的元素確定就是 最近最少使用 的元素了,所以當內存不足時應當優先刪除這些元素。
設計一個圖片加載框架,確定要用到圖片加載的三級緩存的思想。三級緩存分爲內存緩存、本地緩存和網絡緩存。
內存緩存:將Bitmap緩存到內存中,運行速度快,可是內存容量小。 本地緩存:將圖片緩存到文件中,速度較慢,但容量較大。 網絡緩存:從網絡獲取圖片,速度受網絡影響。
若是咱們設計一個圖片加載框架,流程必定是這樣的:
上面是一些基本的概念,若是是具體的代碼實現的話,大概須要這麼幾個方面的文件:
displayImage(url,imageView)
,另外一個是從網絡獲取圖片downloadImage(url,imageView)
。在展現圖片方法中首先要經過 ImageView.setTag(url)
,將 url 和 imageView 進行綁定,這是爲了不在列表中加載網絡圖片時會因爲ImageView的複用致使的圖片錯位的 bug。以後會從 MemeryAndDiskCache 中獲取緩存,若是存在,直接加載;若是不存在,則調用從網絡獲取圖片這個方法。從網絡獲取圖片方法不少,這裏我通常都會使用 OkHttp+Retrofit
。當從網絡中獲取到圖片以後,首先判斷一下imageView.getTag()
與圖片的 url 是否一致,若是一致則加載圖片,若是不一致則不加載圖片,經過這樣的方式避免了列表中異步加載圖片的錯位。同時在獲取到圖片以後會經過 MemeryAndDiskCache 來緩存圖片。在咱們的手指觸摸到屏幕的時候,事件實際上是經過 Activity -> ViewGroup -> View
這樣的流程到達最後響應咱們觸摸事件的 View。
說到事件分發,必不可少的是這幾個方法:dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent。
接下來就按照Activity -> ViewGroup -> View
的流程來大體說一下事件分發機制。
咱們的手指觸摸到屏幕的時候,會觸發一個 Action_Down 類型的事件,當前頁面的 Activity 會首先作出響應,也就是說會走到 Activity 的 dispatchTouchEvent()
方法內。在這個方法內部簡單來講是這麼一個邏輯:
getWindow.superDispatchTouchEvent()。
onTouchEvent()。
這個邏輯很好理解,getWindow().superDispatchTouchEvent()
若是返回 true 表明當前事件已經被處理,無需調用本身的 onTouchEvent;不然表明事件並無被處理,須要 Activity 本身處理,也就是調用本身的 onTouchEvent。getWindow()
方法返回了一個 Window 類型的對象,這個咱們都知道,在 Android 中,PhoneWindow 是Window 的惟一實現類。因此這句本質上是調用了``PhoneWindow中的superDispatchTouchEvent()。`
而在 PhoneWindow 的這個方法中實際調用了mDecor.superDispatchTouchEvent(event)
。這個 mDecor 就是 DecorView,它是 FrameLayout 的一個子類,在 DecorView 中的 superDispatchTouchEvent()
中調用的是 super.dispatchTouchEvent()
。到這裏就很明顯了,DecorView 是一個 FrameLayout 的子類,FrameLayout 是一個 ViewGroup 的子類,本質上調用的仍是 ViewGroup的dispatchTouchEvent()
。
分析到這裏,咱們的事件已經從 Activity 傳遞到了 ViewGroup,接下來咱們來分析下 ViewGroup 中的這幾個事件處理方法。
在 ViewGroup 中的 dispatchTouchEvent()
中的邏輯大體以下:
onInterceptTouchEvent()
判斷當前 ViewGroup 是否攔截事件,默認的 ViewGroup 都是不攔截的;onTouchEvent()
;child.dispatchTouchEvent()
的返回值判斷。若是返回 true,則 return true;不然 return 本身的 onTouchEvent()
,在這裏實現了未處理事件的向上傳遞。一般狀況下 ViewGroup 的 onInterceptTouchEvent()
都返回 false,也就是不攔截。這裏須要注意的是事件序列,好比 Down 事件、Move 事件......Up事件,從 Down 到 Up 是一個完整的事件序列,對應着手指從按下到擡起這一系列的事件,若是 ViewGroup 攔截了 Down 事件,那麼後續事件都會交給這個 ViewGroup的onTouchEvent。若是 ViewGroup 攔截的不是 Down 事件,那麼會給以前處理這個 Down 事件的 View 發送一個 Action_Cancel 類型的事件,通知子 View 這個後續的事件序列已經被 ViewGroup 接管了,子 View 恢復以前的狀態便可。
這裏舉一個常見的例子:在一個 Recyclerview 鐘有不少的 Button,咱們首先按下了一個 button,而後滑動一段距離再鬆開,這時候 Recyclerview 會跟着滑動,並不會觸發這個 button 的點擊事件。這個例子中,當咱們按下 button 時,這個 button 接收到了 Action_Down 事件,正常狀況下後續的事件序列應該由這個 button處理。但咱們滑動了一段距離,這時 Recyclerview 察覺到這是一個滑動操做,攔截了這個事件序列,走了自身的 onTouchEvent()
方法,反映在屏幕上就是列表的滑動。而這時 button 仍然處於按下的狀態,因此在攔截的時候須要發送一個 Action_Cancel 來通知 button 恢復以前狀態。
事件分發最終會走到 View 的 dispatchTouchEvent()
中。在 View 的 dispatchTouchEvent()
中沒有 onInterceptTouchEvent()
,這也很容易理解,View 不是 ViewGroup,不會包含其餘子 View,因此也不存在攔截不攔截這一說。忽略一些細節,View 的 dispatchTouchEvent()
中直接 return 了本身的 onTouchEvent()
。若是 onTouchEvent()
返回 true 表明事件被處理,不然未處理的事件會向上傳遞,直到有 View 處理了事件或者一直沒有處理,最終到達了 Activity 的 onTouchEvent()
終止。
這裏常常有人問 onTouch 和 onTouchEvent 的區別。首先,這兩個方法都在 View 的 dispatchTouchEvent()
中,是這麼一個邏輯:
onTouchEvent()
方法。onTouchEvent()
方法中。因此 onTouch 的順序是在 onTouchEvent 以前的。視圖繪製的起點在 ViewRootImpl 類的 performTraversals()
方法,在這個方法內實際上是按照順序依次調用了 mView.measure()、mView.layout()、mView.draw()
View的繪製流程分爲3步:測量、佈局、繪製,分別對應3個方法 measure、layout、draw。
drawBackground(Canvas)
方法。onDraw(Canvas)
方法。dispatchDraw(Canvas)
方法。onDrawForeground(Canvas)
。MeasureSpec 是 View 的測量規則。一般父控件要測量子控件的時候,會傳給子控件 widthMeasureSpec 和 heightMeasureSpec 這兩個 int 類型的值。這個值裏面包含兩個信息,SpecMode 和 SpecSize。一個 int 值怎麼會包含兩個信息呢?咱們知道 int 是一個4字節32位的數據,在這兩個 int 類型的數據中,前面高2位是 SpecMode ,後面低30位表明了 SpecSize。
mode 有三種類型:
UNSPECIFIED
,EXACTLY
,AT_MOST
測量模式 | 應用 |
EXACTLY | 精準模式,當 width 或 height 爲固定 xxdp 或者爲 MACH_PARENT 的時候,是這種測量模式 |
AT_MOST | 當 width 或 height 設置爲 warp_content 的時候,是這種測量模式 |
UNSPECIFIED | 父容器對當前 View 沒有任何顯示,子 View 能夠取任意大小。通常用在系統內部,好比:Scrollview、ListView。 |
咱們怎麼從一個 int 值裏面取出兩個信息呢?別擔憂,在 View 內部有一個 MeasureSpec 類。這個類已經給咱們封裝好了各類方法:
//將 Size 和 mode 組合成一個 int 值
int measureSpec = MeasureSpec.makeMeasureSpec(size,mode);
//獲取 size 大小
int size = MeasureSpec.getSize(measureSpec);
//獲取 mode 類型
int mode = MeasureSpec.getMode(measureSpec);
複製代碼複製代碼
具體實現細節,能夠查看源碼
可能咱們會有疑問,若是全部子控件的 measureSpec 都是父控件結合自身的 measureSpec 和子 View 的 LayoutParams 來生成的。那麼做爲視圖的頂級父類 DecorView 怎麼獲取本身的 measureSpec 呢?下面咱們來分析源碼:(如下源碼有所刪減)
//ViewRootImpl 類
private void performTraversals() {
//獲取 DecorView 寬度的 measureSpec
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
//獲取 DecorView 高度的 measureSpec
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
//開始執行測量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
複製代碼複製代碼
//ViewRootImpl 類
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY); break; case ViewGroup.LayoutParams.WRAP_CONTENT: // Window can resize. Set max size for root view. measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST); break; default: // Window wants to be an exact size. Force root view to be that size. measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY); break; } return measureSpec; } 複製代碼複製代碼
windowSize 是 widow 的寬高大小,因此咱們能夠看出 DecorView 的 measureSpec 是根據 window 的寬高大小和自身的 LayoutParams 來生成的。
兩種方案:外部和內部
可能在非UI線程中刷新界面的時候,UI線程(或者其餘非UI線程)也在刷新界面,這樣就致使多個界面刷新的操做不能同步,致使線程不安全。
ANR的全稱是application not responding,是指應用程序未響應,Android系統對於一些事件須要在必定的時間範圍內完成,若是超過預約時間能未能獲得有效響應或者響應時間過長,都會形成ANR。通常地,這時每每會彈出一個提示框,告知用戶當前xxx未響應,用戶可選擇繼續等待或者Force Close。
首先ANR的發生是有條件限制的,分爲如下三點:
只有主線程纔會產生ANR,主線程就是UI線程;
必須發生某些輸入事件或特定操做,好比按鍵或觸屏等輸入事件,在BroadcastReceiver或Service的各個生命週期調用函數;
上述事件響應超時,不一樣的context規定的上限時間不一樣
a.主線程對輸入事件5秒內沒有處理完畢
b.主線程在執行BroadcastReceiver的onReceive()函數時10秒內沒有處理完畢
c.主線程在前臺Service的各個生命週期函數時20秒內沒有處理完畢(後臺Service200s)
那麼致使ANR的根本緣由是什麼呢?簡單的總結有如下兩點:
1.主線程執行了耗時操做,好比數據庫操做或網絡編程,I/O操做
2.其餘進程(就是其餘程序)佔用CPU致使本進程得不到CPU時間片,好比其餘進程的頻繁讀寫操做可能會致使這個問題。
細分的話,致使ANR的緣由有以下幾點:
耗時的網絡訪問
大量的數據讀寫
數據庫操做
硬件操做(好比camera)
調用thread的join()方法、sleep()方法、wait()方法或者等待線程鎖的時候
service binder的數量達到上限
system server中發生WatchDog ANR
service忙致使超時無響應
其餘線程持有鎖,致使主線程等待超時
其它線程終止或崩潰致使主線程一直等待
那麼如何避免ANR的發生呢或者說ANR的解決辦法是什麼呢?
1.避免在主線程執行耗時操做,全部耗時操做應新開一個子線程完成,而後再在主線程更新UI。
2.BroadcastReceiver要執行耗時操做時應啓動一個service,將耗時操做交給service來完成。
3.避免在Intent Receiver裏啓動一個Activity,由於它會建立一個新的畫面,並從當前用戶正在運行的程序上搶奪焦點。若是你的應用程序在響應Intent廣 播時須要向用戶展現什麼,你應該使用Notification Manager來實現。
在 Android 中,Android 與js 的交互分爲兩個方面:Android 調用 js 裏的方法、js 調用 Android 中的方法。
WebView.addJavascriptInterface()。
這是官方解決 js 調用 Android 方法的方案,須要注意的是要在供 js 調用的 Android 方法上加上 @JavascriptInterface 註解,以免安全漏洞。這種方案的缺點是 Android4.2 之前會有安全漏洞,不過在 4.2 之後已經修復了。一樣,在 2018 年來講,兼容性問題不大。WebViewClient的shouldOverrideUrlLoading()
方法來攔截url, 拿到 url 後進行解析,若是符合雙方的規定,便可調用 Android 方法。優勢是避免了 Android4.2 之前的安全漏洞,缺點也很明顯,沒法直接拿到調用 Android 方法的返回值,只能經過 Android 調用 js 方法來獲取返回值。onJsPrompt()
方法,同前一個方式同樣,拿到 url 以後先進行解析,若是符合雙方規定,便可調用Android方法。最後若是須要返回值,經過 result.confirm("Android方法返回值")
便可將 Android 的返回值返回給 js。方法的優勢是沒有漏洞,也沒有兼容性限制,同時還能夠方便的獲取 Android 方法的返回值。其實這裏須要注意的是在 WebChromeClient 中除 了 onJsPrompt 以外還有 onJsAlert 和 onJsConfirm 方法。那麼爲何不選擇另兩個方法呢?緣由在於 onJsAlert 是沒有返回值的,而 onJsConfirm 只有 true 和 false 兩個返回值,同時在前端開發中 prompt 方法基本不會被調用,因此纔會採用 onJsPrompt。SparseArray,一般來說是 Android 中用來替代 HashMap 的一個數據結構。 準確來說,是用來替換key爲 Integer 類型,value爲Object 類型的HashMap。須要注意的是 SparseArray 僅僅實現了 Cloneable 接口,因此不能用Map來聲明。 從內部結構來說,SparseArray 內部由兩個數組組成,一個是 int[]
類型的 mKeys,用來存放全部的鍵;另外一個是 Object[]
類型的 mValues,用來存放全部的值。 最多見的是拿 SparseArray 跟HashMap 來作對比,因爲 SparseArray 內部組成是兩個數組,因此佔用內存比 HashMap 要小。咱們都知道,增刪改查等操做都首先須要找到相應的鍵值對,而 SparseArray 內部是經過二分查找來尋址的,效率很明顯要低於 HashMap 的常數級別的時間複雜度。提到二分查找,這裏還須要提一下的是二分查找的前提是數組已是排好序的,沒錯,SparseArray 中就是按照key進行升序排列的。 綜合起來來講,SparseArray 所佔空間優於 HashMap,而效率低於 HashMap,是典型的時間換空間,適合較小容量的存儲。 從源碼角度來講,我認爲須要注意的是 SparseArray的remove()、put()
和 gc()
方法。
remove()
。 SparseArray 的 remove()
方法並非直接刪除以後再壓縮數組,而是將要刪除的 value 設置爲 DELETE 這個 SparseArray 的靜態屬性,這個 DELETE 其實就是一個 Object 對象,同時會將 SparseArray 中的 mGarbage 這個屬性設置爲 true,這個屬性是便於在合適的時候調用自身的 gc()
方法壓縮數組來避免浪費空間。這樣能夠提升效率,若是未來要添加的key等於刪除的key,那麼會將要添加的 value 覆蓋 DELETE。gc()。
SparseArray 中的 gc()
方法跟 JVM 的 GC 其實徹底沒有任何關係。``gc()` 方法的內部實際上就是一個for循環,將 value 不爲 DELETE 的鍵值對往前移動覆蓋value 爲DELETE的鍵值對來實現數組的壓縮,同時將 mGarbage 置爲 false,避免內存的浪費。put()。
put 方法是這麼一個邏輯,若是經過二分查找 在 mKeys 數組中找到了 key,那麼直接覆蓋 value 便可。若是沒有找到,會拿到與數組中與要添加的 key 最接近的 key 索引,若是這個索引對應的 value 爲 DELETE,則直接把新的 value 覆蓋 DELET 便可,在這裏能夠避免數組元素的移動,從而提升了效率。若是 value 不爲 DELETE,會判斷 mGarbage,若是爲 true,則會調用 gc()
方法壓縮數組,以後會找到合適的索引,將索引以後的鍵值對後移,插入新的鍵值對,這個過程當中可能會觸發數組的擴容。咱們知道內存中的 Bitmap 大小的計算公式是:長所佔像素 * 寬所佔像素 * 每一個像素所佔內存。想避免 OOM 有兩種方法:等比例縮小長寬、減小每一個像素所佔的內存。
decodeFile()、decodeStream()、decodeByteArray()、decodeResource()
。這些方法中都有一個 Options 類型的參數,這個 Options 是 BitmapFactory 的內部類,存儲着 BItmap 的一些信息。Options 中有一個屬性:inSampleSize。咱們經過修改 inSampleSize 能夠縮小圖片的長寬,從而減小 BItma p 所佔內存。須要注意的是這個 inSampleSize 大小須要是 2 的冪次方,若是小於 1,代碼會強制讓inSampleSize爲1。ARGB_8888
,表明每一個像素所佔尺寸。咱們能夠經過將之修改成 RGB_565
或者 ARGB_4444
來減小一半內存。加載高清大圖,好比清明上河圖,首先屏幕是顯示不下的,並且考慮到內存狀況,也不可能一次性所有加載到內存。這時候就須要局部加載了,Android中有一個負責局部加載的類:BitmapRegionDecoder。使用方法很簡單,經過BitmapRegionDecoder.newInstance()建立對象,以後調用decodeRegion(Rect rect, BitmapFactory.Options options)便可。第一個參數rect是要顯示的區域,第二個參數是BitmapFactory中的內部類Options。
Bitmap內存佔用 ≈ 像素數據總大小 = 圖片寬 × 圖片高× (設備分辨率/資源目錄分辨率)^2 × 每一個像素的字節大小(能夠詳細闡述)
圖片佔用的內存通常會分爲運行時佔用的運存和存儲時本地開銷(反映在包大小上),這裏咱們只關注運行時佔用內存的優化。
在上一節中,咱們看到對於一張800 * 600 大小的圖片,不加任何處理直接解析到內存中,將近佔用了17.28M的內存大小。想象一下這樣的開銷發生在一個圖片列表中,內存佔用將達到很是誇張的地步。從以前Bitmap佔用內存的計算公式來看,減小內存主要能夠經過如下幾種方式:
第一種方式,大約能減小一半的內存開銷。Android默認是使用ARGB8888配置來處理色彩,佔用4字節,改用RGB565,將只佔用2字節,代價是顯示的色彩將相對少,適用於對色彩豐富程度要求不高的場景。
第二種方式,和圖片的具體分辨率有關,建議開發中,高分辨率的圖像應該放置到合理的資源目錄下,注意到Android默認放置的資源目錄是對應於160dpi,目前手機屏幕分辨率愈來愈高,此處能節省下來的開銷也是很可觀的。理論上,圖片放置的資源目錄分辨率越高,其佔用內存會越小,可是低分辨率圖片會所以被拉伸,顯示上出現失真。另外一方面,高分辨率圖片也意味着其佔用的本地儲存也變大。
第三種方式,理論上根據適用的環境,是能夠減小十幾倍的內存使用的,它基於這樣一個事實:源圖片尺寸通常都大於目標須要顯示的尺寸,所以能夠經過縮放的方式,來減小顯示時的圖片寬高,從而大大減小佔用的內存。
Android UI是線程不安全的,若是想要在子線程裏進行UI操做,就須要藉助Android的異步消息處理機制。
不過爲了更加方便咱們在子線程中更新UI元素,Android從1.5版本就引入了一個AsyncTask類
在Android當中,一般將線程分爲兩種,一種叫作Main Thread,除了Main Thread以外的線程均可稱爲Worker Thread
AsyncTask:異步任務,從字面上來講,就是在咱們的UI主線程運行的時候,異步的完成一些操做。AsyncTask容許咱們的執行一個異步的任務在後臺。咱們能夠將耗時的操做放在異步任務當中來執行,並隨時將任務執行的結果返回給咱們的UI線程來更新咱們的UI控件
咱們定義一個類來繼承AsyncTask這個類的時候,咱們須要爲其指定3個泛型參數:
AsyncTask <Params, Progress, Result>
Params: 這個泛型指定的是咱們傳遞給異步任務執行時的參數的類型
Progress: 這個泛型指定的是咱們的異步任務在執行的時候將執行的進度返回給UI線程的參數的類型
Result: 這個泛型指定的異步任務執行完後返回給UI線程的結果的類型
若是都不指定的話,則都將其寫成Void
執行一個異步任務的時候,其須要按照下面的4個步驟分別執行:
onPreExecute(): 這個方法是在執行異步任務以前的時候執行,而且是在UI Thread當中執行的,一般咱們在這個方法裏作一些UI控件的初始化的操做,例如彈出要給ProgressDialog
doInBackground(Params... params): 在onPreExecute()方法執行完以後,會立刻執行這個方法,這個方法就是來處理異步任務的方法,Android操做系統會在後臺的線程池當中開啓一個worker thread來執行咱們的這個方法,因此這個方法是在worker thread當中執行的,這個方法執行完以後就能夠將咱們的執行結果發送給咱們的最後一個 onPostExecute 方法,在這個方法裏,咱們能夠從網絡當中獲取數據等一些耗時的操做
onProgressUpdate(Progess... values): 這個方法也是在UI Thread當中執行的,咱們在異步任務執行的時候,有時候須要將執行的進度返回給咱們的UI界面,例以下載一張網絡圖片,咱們須要時刻顯示其下載的進度,就能夠使用這個方法來更新咱們的進度。這個方法在調用以前,咱們須要在 doInBackground 方法中調用一個 publishProgress(Progress) 的方法來將咱們的進度時時刻刻傳遞給 onProgressUpdate 方法來更新
onPostExecute(Result... result): 當咱們的異步任務執行完以後,就會將結果返回給這個方法,這個方法也是在UI Thread當中調用的,咱們能夠將返回的結果顯示在UI控件上
能夠在任什麼時候刻來取消咱們的異步任務的執行,經過調用 cancel(boolean)方法.
在使用AsyncTask作異步任務的時候必需要遵循的原則:
AsyncTask類必須在UI Thread當中加載,在Android Jelly_Bean版本後這些都是自動完成的
AsyncTask的對象必須在UI Thread當中實例化
execute方法必須在UI Thread當中調用
不要手動的去調用AsyncTask的onPreExecute, doInBackground, publishProgress, onProgressUpdate, onPostExecute方法,這些都是由Android系統自動調用的
AsyncTask任務只能被執行一次
使用的優勢:
l 簡單,快捷
l 過程可控
使用的缺點:
l 在使用多個異步操做和並須要進行Ui變動時,就變得複雜起來.
RelativeLayout分別對全部子View進行兩次measure,橫向縱向分別進行一次,這是爲何呢?首先RelativeLayout中子View的排列方式是基於彼此的依賴關係,而這個依賴關係可能和佈局中View的順序並不相同,在肯定每一個子View的位置的時候,須要先給全部的子View排序一下。又由於RelativeLayout容許A,B 2個子View,橫向上B依賴A,縱向上A依賴B。因此須要橫向縱向分別進行一次排序測量。 mSortedHorizontalChildren和mSortedVerticalChildren是分別對水平方向的子控件和垂直方向的子控件進行排序後的View數組。
與RelativeLayout相比LinearLayout的measure就簡單的多,只需判斷線性佈局是水平佈局仍是垂直佈局便可,而後才進行測量:若是不使用weight屬性,LinearLayout會在當前方向上進行一次measure的過程,若是使用weight屬性,LinearLayout會避開設置過weight屬性的view作第一次measure,完了再對設置過weight屬性的view作第二次measure。因而可知,weight屬性對性能是有影響的,並且自己有大坑,請注意避讓。
結論
(1)RelativeLayout會讓子View調用2次onMeasure,LinearLayout 在有weight時,也會調用子View 2次onMeasure
(2)RelativeLayout的子View若是高度和RelativeLayout不一樣,則會引起效率問題,當子View很複雜時,這個問題會更加嚴重。若是能夠,儘可能使用padding代替margin。
(3)在不影響層級深度的狀況下,使用LinearLayout和FrameLayout而不是RelativeLayout。
(4)提升繪製性能的使用方式
根據上面源碼的分析,RelativeLayout將對全部的子View進行兩次measure,而LinearLayout在使用weight屬性進行佈局時也會對子View進行兩次measure,若是他們位於整個View樹的頂端時並可能進行多層的嵌套時,位於底層的View將會進行大量的measure操做,大大下降程序性能。所以,應儘可能將RelativeLayout和LinearLayout置於View樹的底層,並減小嵌套
1. 層級不一樣:
RecyclerView比ListView多兩級緩存,支持多個離屏ItemView緩存,支持開發者自定義緩存處理邏輯,支持全部RecyclerView共用同一個RecyclerViewPool(緩存池)。
具體來講:
ListView(兩級緩存):
RecyclerView(四級緩存):
ListView和RecyclerView緩存機制基本一致:
1). mActiveViews和mAttachedScrap功能類似,意義在於快速重用屏幕上可見的列表項ItemView,而不須要從新createView和bindView;
2). mScrapView和mCachedViews + mReyclerViewPool功能類似,意義在於緩存離開屏幕的ItemView,目的是讓即將進入屏幕的ItemView重用.
3). RecyclerView的優點在於a.mCacheViews的使用,能夠作到屏幕外的列表項ItemView進入屏幕內時也無須bindView快速重用;b.mRecyclerPool能夠供多個RecyclerView共同使用,在特定場景下,如viewpaper+多個列表頁下有優點.客觀來講,RecyclerView在特定場景下對ListView的緩存機制作了補強和完善。
2. 緩存不一樣:
1). RecyclerView緩存RecyclerView.ViewHolder,抽象可理解爲:
View + ViewHolder(避免每次createView時調用findViewById) + flag(標識狀態);
2). ListView緩存View。
http 是超文本傳輸協議,而 https 能夠簡單理解爲安全的 http 協議。https 經過在 http 協議下添加了一層 ssl 協議對數據進行加密從而保證了安全。https 的做用主要有兩點:創建安全的信息傳輸通道,保證數據傳輸安全;確認網站的真實性。
提到 https 的話首先要說到加密算法,加密算法分爲兩類:對稱加密和非對稱加密。
在正式的使用場景中通常都是對稱加密和非對稱加密結合使用,使用非對稱加密完成祕鑰的傳遞,而後使用對稱祕鑰進行數據加密和解密。兩者結合既保證了安全性,又提升了數據傳輸效率。
Accept: text/plain
Accept-Charset: utf-8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN
Cookie:user=admin
Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==
Content-Type: application/x-www-form-urlencoded
Date: Tue, 15 Nov 1994 08:12:31 GMT
From: user@example.com
Host: www.some.com:182
Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/12.0
Dart 當中的 「..」意思是 「級聯操做符」,爲了方便配置而使用。「..」和「.」不一樣的是 調用「..」後返回的至關因而 this,而「.」返回的則是該方法返回的值 。
共同點
區別(須要注意的地方)
if else
for , forEach , for-in
Dart 沒有 「public」「private」等關鍵字,默認就是公開的,私有變量使用 下劃線 _開頭。「_」的限制範圍並非類訪問級別的,而是庫訪問級別。
Dart 是單線程模型,運行的的流程以下圖。
簡單來講,Dart 在單線程中是以消息循環機制來運行的,包含兩個任務隊列,一個是「微任務隊列」 microtask queue,另外一個叫作「事件隊列」 event queue。
當Flutter應用啓動後,消息循環機制便啓動了。首先會按照先進先出的順序逐個執行微任務隊列中的任務,當全部微任務隊列執行完後便開始執行事件隊列中的任務,事件任務執行完畢後再去執行微任務,如此循環往復,生生不息。
Dart 是單線程的,不存在多線程,那如何進行多任務並行的呢?其實,Dart的多線程和前端的多線程有不少的類似之處。Flutter的多線程主要依賴Dart的併發編程、異步和事件驅動機制。
簡單的說,在Dart中,一個Isolate對象其實就是一個isolate執行環境的引用,通常來講咱們都是經過當前的isolate去控制其餘的isolate完成彼此之間的交互,而當咱們想要建立一個新的Isolate能夠使用Isolate.spawn方法獲取返回的一個新的isolate對象,兩個isolate之間使用SendPort相互發送消息,而isolate中也存在了一個與之對應的ReceivePort接受消息用來處理,可是咱們須要注意的是,ReceivePort和SendPort在每一個isolate都有一對,只有同一個isolate中的ReceivePort才能接受到當前類的SendPort發送的消息而且處理。
Future 是異步編程的解決方案,Future 是基於觀察者模式的,它有 3 種狀態:pending(進行中)、fulfilled(已成功)和 rejected(已失敗)能夠用 then 方法指定成功狀態的回調函數 then 方法還能夠接受一個可選命名參數,參數的名稱是 onError,即失敗狀態的回調函數 Future 實例的函數中拋出了異常,被 onError 回調函數捕獲到,而且能夠看出 then 方法返回的仍是一個 Future 對象,因此其實咱們還能夠利用 Future 對象的 cathError 進行鏈式調用從而捕獲異常
在Dart1.9中加入了async
和await
關鍵字,有了這兩個關鍵字,咱們能夠更簡潔的編寫異步代碼,而不須要調用Future
相關的API
將 async
關鍵字做爲方法聲明的後綴時,具備以下意義
Future
對象做爲返回值async 不是並行執行,它是遵循Dart 事件循環規則來執行的,它僅僅是一個語法糖,簡化Future API
的使用。
在Dart中,Stream 和 Future 同樣,都是用來處理異步編程的工具。它們的區別在於,Stream 能夠接收多個異步結果,而Future 只有一個。
Stream 的建立能夠使用 Stream.fromFuture,也能夠使用 StreamController 來建立和控制。還有一個注意點是:普通的 Stream 只能夠有一個訂閱者,若是想要多訂閱的話,要使用 asBroadcastStream()。
Stream有兩種訂閱模式:單訂閱(single) 和 多訂閱(broadcast)。單訂閱就是隻能有一個訂閱者,而廣播是能夠有多個訂閱者。這就有點相似於消息服務(Message Service)的處理模式。單訂閱相似於點對點,在訂閱者出現以前會持有數據,在訂閱者出現以後就才轉交給它。而廣播相似於發佈訂閱模式,能夠同時有多個訂閱者,當有數據時就會傳遞給全部的訂閱者,而無論當前是否已有訂閱者存在。
Stream 默認處於單訂閱模式,因此同一個 stream 上的 listen 和其它大多數方法只能調用一次,調用第二次就會報錯。但 Stream 能夠經過 transform() 方法(返回另外一個 Stream)進行連續調用。經過 Stream.asBroadcastStream() 能夠將一個單訂閱模式的 Stream 轉換成一個多訂閱模式的 Stream,isBroadcast 屬性能夠判斷當前 Stream 所處的模式。
mixin 是Dart 2.1 加入的特性,之前版本一般使用abstract class代替。簡單來講,mixin是爲了解決繼承方面的問題而引入的機制,Dart爲了支持多重繼承,引入了mixin關鍵字,它最大的特殊處在於:mixin定義的類不能有構造方法,這樣能夠避免繼承多個類而產生的父類構造方法衝突。
mixins的對象是類,mixins毫不是繼承,也不是接口,而是一種全新的特性,能夠mixins多個類,mixins的使用須要知足必定條件。
藉助於先進的工具鏈和編譯器,Dart 是少數同時支持 JIT(Just In Time,即時編譯)和 AOT(Ahead of Time,運行前編譯)的語言之一。那,到底什麼是 JIT 和 AOT 呢?語言在運行以前一般都須要編譯,JIT 和 AOT 則是最多見的兩種編譯模式。JIT 在運行時即時編譯,在開發週期中使用,能夠動態下發和執行代碼,開發測試效率高,但運行速度和執行性能則會由於運行時即時編譯受到影響。AOT 即提早編譯,能夠生成被直接執行的二進制代碼,運行速度快、執行性能表現好,但每次執行前都須要提早編譯,開發測試效率低。
Dart VM 的內存分配策略比較簡單,建立對象時只須要在堆上移動指針,內存增加始終是線性的,省去了查找可用內存的過程。在 Dart 中,併發是經過 Isolate 實現的。Isolate 是相似於線程但不共享內存,獨立運行的 worker。這樣的機制,就可讓 Dart 實現無鎖的快速分配。Dart 的垃圾回收,則是採用了多生代算法。新生代在回收內存時採用「半空間」機制,觸發垃圾回收時,Dart 會將當前半空間中的「活躍」對象拷貝到備用空間,而後總體釋放當前空間的全部內存。回收過程當中,Dart 只須要操做少許的「活躍」對象,沒有引用的大量「死亡」對象則被忽略,這樣的回收機制很適合 Flutter 框架中大量 Widget 銷燬重建的場景。
Flutter是Google推出的一套開源跨平臺UI框架,能夠快速地在Android、iOS和Web平臺上構建高質量的原生用戶界面。同時,Flutter仍是Google新研發的Fuchsia操做系統的默認開發套件。在全世界,Flutter正在被愈來愈多的開發者和組織使用,而且Flutter是徹底免費、開源的。Flutter採用現代響應式框架構建,其中心思想是使用組件來構建應用的UI。當組件的狀態發生改變時,組件會重構它的描述,Flutter會對比以前的描述,以肯定底層渲染樹從當前狀態轉換到下一個狀態所須要的最小更改。
優勢
缺點
其實也就是下面這張圖。
由上圖可知,Flutter框架自下而上分爲Embedder、Engine和Framework三層。其中,Embedder是操做系統適配層,實現了渲染 Surface設置,線程設置,以及平臺插件等平臺相關特性的適配;Engine層負責圖形繪製、文字排版和提供Dart運行時,Engine層具備獨立虛擬機,正是因爲它的存在,Flutter程序才能運行在不一樣的平臺上,實現跨平臺運行;Framework層則是使用Dart編寫的一套基礎視圖庫,包含了動畫、圖形繪製和手勢識別等功能,是使用頻率最高的一層。
Flutter的FrameWork層是用Drat編寫的框架(SDK),它實現了一套基礎庫,包含Material(Android風格UI)和Cupertino(iOS風格)的UI界面,下面是通用的Widgets(組件),以後是一些動畫、繪製、渲染、手勢庫等。這個純 Dart實現的 SDK被封裝爲了一個叫做 dart:ui的 Dart庫。咱們在使用 Flutter寫 App的時候,直接導入這個庫便可使用組件等功能。
Flutter的Engine層是Skia 2D的繪圖引擎庫,其前身是一個向量繪圖軟件,Chrome和 Android均採用 Skia做爲繪圖引擎。Skia提供了很是友好的 API,而且在圖形轉換、文字渲染、位圖渲染方面都提供了友好、高效的表現。Skia是跨平臺的,因此能夠被嵌入到 Flutter的 iOS SDK中,而不用去研究 iOS閉源的 Core Graphics / Core Animation。Android自帶了 Skia,因此 Flutter Android SDK要比 iOS SDK小不少。
Flutter的Widget分爲StatelessWidget和StatefulWidget兩種。其中,StatelessWidget是無狀態的,StatefulWidget是有狀態的,所以實際使用時,更多的是StatefulWidget。StatefulWidget的生命週期以下圖
首先看一下這幾個對象的含義及做用。
Widget會被inflate(填充)到Element,並由Element管理底層渲染樹。Widget並不會直接管理狀態及渲染,而是經過State這個對象來管理狀態。Flutter建立Element的可見樹,相對於Widget來講,是可變的,一般界面開發中,咱們不用直接操做Element,而是由框架層實現內部邏輯。就如一個UI視圖樹中,可能包含有多個TextWidget(Widget被使用屢次),可是放在內部視圖樹的視角,這些TextWidget都是填充到一個個獨立的Element中。Element會持有renderObject和widget的實例。記住,Widget 只是一個配置,RenderObject 負責管理佈局、繪製等操做。
在第一次建立 Widget 的時候,會對應建立一個 Element, 而後將該元素插入樹中。若是以後 Widget 發生了變化,則將其與舊的 Widget 進行比較,而且相應地更新 Element。重要的是,Element 不會被重建,只是更新而已。
Flutter中的狀態和前端React中的狀態概念是一致的。React框架的核心思想是組件化,應用由組件搭建而成,組件最重要的概念就是狀態,狀態是一個組件的UI數據模型,是組件渲染時的數據依據。
Flutter的狀態能夠分爲全局狀態和局部狀態兩種。經常使用的狀態管理有ScopedModel、BLoC、Redux / FishRedux和Provider。詳細使用狀況和差別能夠自行了解。
Flutter只關心向 GPU提供視圖數據,GPU的 VSync信號同步到 UI線程,UI線程使用 Dart來構建抽象的視圖結構,這份數據結構在 GPU線程進行圖層合成,視圖數據提供給 Skia引擎渲染爲 GPU數據,這些數據經過 OpenGL或者 Vulkan提供給 GPU。
默認狀況下,Flutter Engine層會建立一個Isolate,而且Dart代碼默認就運行在這個主Isolate上。必要時能夠使用spawnUri和spawn兩種方式來建立新的Isolate,在Flutter中,新建立的Isolate由Flutter進行統一的管理。
事實上,Flutter Engine本身不建立和管理線程,Flutter Engine線程的建立和管理是Embeder負責的,Embeder指的是將引擎移植到平臺的中間層代碼,Flutter Engine層的架構示意圖以下圖所示。
在Flutter的架構中,Embeder提供四個Task Runner,分別是Platform Task Runner、UI Task Runner Thread、GPU Task Runner和IO Task Runner,每一個Task Runner負責不一樣的任務,Flutter Engine不在意Task Runner運行在哪一個線程,可是它須要線程在整個生命週期裏面保持穩定
Flutter 經過 PlatformChannel 與原生進行交互,其中 PlatformChannel 分爲三種:
同時 Platform Channel 並不是是線程安全的 ,更多詳細可查閱閒魚技術的 《深刻理解Flutter Platform Channel》
Flutter 的熱重載是基於 JIT 編譯模式的代碼增量同步。因爲 JIT 屬於動態編譯,可以將 Dart 代碼編譯成生成中間代碼,讓 Dart VM 在運行時解釋執行,所以能夠經過動態更新中間代碼實現增量同步。
熱重載的流程能夠分爲 5 步,包括:掃描工程改動、增量編譯、推送更新、代碼合併、Widget 重建。Flutter 在接收到代碼變動後,並不會讓 App 從新啓動執行,而只會觸發 Widget 樹的從新繪製,所以能夠保持改動前的狀態,大大縮短了從代碼修改到看到修改產生的變化之間所須要的時間。
另外一方面,因爲涉及到狀態的保存與恢復,涉及狀態兼容與狀態初始化的場景,熱重載是沒法支持的,如改動先後 Widget 狀態沒法兼容、全局變量與靜態屬性的更改、main 方法裏的更改、initState 方法裏的更改、枚舉和泛型的更改等。
能夠發現,熱重載提升了調試 UI 的效率,很是適合寫界面樣式這樣須要反覆查看修改效果的場景。但因爲其狀態保存的機制所限,熱重載自己也有一些沒法支持的邊界。
與用於構建移動應用程序的其餘大多數框架不一樣,Flutter 是重寫了一整套包括底層渲染邏輯和上層開發語言的完整解決方案。這樣不只能夠保證視圖渲染在 Android 和 iOS 上的高度一致性(即高保真),在代碼執行效率和渲染性能上也能夠媲美原生 App 的體驗(即高性能)。這,就是
React Native 之類的框架,只是經過 JavaScript 虛擬機擴展調用系統組件,由 Android 和 iOS 系統進行組件的渲染;
Flutter 則是本身完成了組件渲染的閉環。那麼,Flutter 是怎麼完成組件渲染的呢?這須要從圖像顯示的基本原理提及。在計算機系統中,圖像的顯示須要 CPU、GPU 和顯示器一塊兒配合完成:CPU 負責圖像數據計算,GPU 負責圖像數據渲染,而顯示器則負責最終圖像顯示。CPU 把計算好的、須要顯示的內容交給 GPU,由 GPU 完成渲染後放入幀緩衝區,隨後視頻控制器根據垂直同步信號(VSync)以每秒 60 次的速度,從幀緩衝區讀取幀數據交由顯示器完成圖像顯示。操做系統在呈現圖像時遵循了這種機制,而 Flutter 做爲跨平臺開發框架也採用了這種底層方案。下面有一張更爲詳盡的示意圖來解釋 Flutter 的繪製原理。
Flutter 繪製原理能夠看到,Flutter 關注如何儘量快地在兩個硬件時鐘的 VSync 信號之間計算併合成視圖數據,而後經過 Skia 交給 GPU 渲染:UI 線程使用 Dart 來構建視圖結構數據,這些數據會在 GPU 線程進行圖層合成,隨後交給 Skia 引擎加工成 GPU 數據,而這些數據會經過 OpenGL 最終提供給 GPU 渲染。
組件化又叫模塊化,即基於可重用的目的,將一個大型軟件系統(App)按照關注點分離的方式,拆分紅多個獨立的組件或模塊。每一個獨立的組件都是一個單獨的系統,能夠單獨維護、升級甚至直接替換,也能夠依賴於別的獨立組件,只要組件提供的功能不發生變化,就不會影響其餘組件和軟件系統的總體功能。
能夠看到,組件化的中心思想是將獨立的功能進行拆分,而在拆分粒度上,組件化的約束則較爲鬆散。一個獨立的組件能夠是一個軟件包(Package)、頁面、UI 控件,甚至多是封裝了一些函數的模塊。組件的粒度可大可小,那咱們如何才能作好組件的封裝重用呢?哪些代碼應該被放到一個組件中?這裏有一些基本原則,包括單一性原則、抽象化原則、穩定性原則和自完備性原則。接下來,咱們先看看這些原則具體是什麼意思。
單一性原則指的是,每一個組件僅提供一個功能。分而治之是組件化的中心思想,每一個組件都有本身固定的職責和清晰的邊界,專一地作一件事兒,這樣這個組件才能良性發展。一個反例是 Common 或 Util 組件,這類組件每每是由於在開發中出現了定義不明確、歸屬邊界不清晰的代碼:「哎呀,這段代碼放哪兒好像都不合適,那就放 Common(Util)吧」。長此以往,這類組件就變成了無人問津的垃圾堆。因此,再遇到不知道該放哪兒的代碼時,就須要從新思考組件的設計和職責了。
抽象化原則指的是,組件提供的功能抽象應該儘可能穩定,具備高複用度。而穩定的直觀表現就是對外暴露的接口不多發生變化,要作到這一點,須要咱們提高對功能的抽象總結能力,在組件封裝時作好功能抽象和接口設計,將全部可能發生變化的因子都在組件內部作好適配,不要暴露給它的調用方。
穩定性原則指的是,不要讓穩定的組件依賴不穩定的組件。好比組件 1 依賴了組件 5,若是組件 1 很穩定,可是組件 5 常常變化,那麼組件 1 也就會變得不穩定了,須要常常適配。若是組件 5 裏確實有組件 1 不可或缺的代碼,咱們能夠考慮把這段代碼拆出來單獨作成一個新的組件 X,或是直接在組件 1 中拷貝一份依賴的代碼。
自完備性,即組件須要儘量地作到自給自足,儘可能減小對其餘底層組件的依賴,達到代碼可複用的目的。好比,組件 1 只是依賴某個大組件 5 中的某個方法,這時更好的處理方法是,剝離掉組件 1 對組件 5 的依賴,直接把這個方法拷貝到組件 1 中。這樣一來組件 1 就可以更好地應對後續的外部變動了。
在理解了組件化的基本原則以後,咱們再來看看組件化的具體實施步驟,即剝離基礎功能、抽象業務模塊和最小化服務能力。首先,咱們須要剝離應用中與業務無關的基礎功能,好比網絡請求、組件中間件、第三方庫封裝、UI 組件等,將它們封裝爲獨立的基礎庫;而後,咱們在項目裏用 pub 進行管理。若是是第三方庫,考慮到後續的維護適配成本,咱們最好再封裝一層,使項目不直接依賴外部代碼,方便後續更新或替換。基礎功能已經封裝成了定義更清晰的組件,接下來咱們就能夠按照業務維度,好比首頁、詳情頁、搜索頁等,去拆分獨立的模塊了。拆分的粒度能夠先粗後細,只要能將大致劃分清晰的業務組件進行拆分,後續就能夠經過分佈迭代、局部微調,最終實現整個業務項目的組件化。在業務組件和基礎組件都完成拆分封裝後,應用的組件化架構就基本成型了,最後就能夠按照剛纔咱們說的 4 個原則,去修正各個組件向下的依賴,以及最小化對外暴露的能力了。
從組件的定義能夠看到,組件是個鬆散的廣義概念,其規模取決於咱們封裝的功能維度大小,而各個組件之間的關係也僅靠依賴去維持。若是組件之間的依賴關係比較複雜,就會在必定程度上形成功能耦合現象。
以下所示的組件示意圖中,組件 2 和組件 3 同時被多個業務組件和基礎功能組件直接引用,甚至組件 2 和組件 五、組件 3 和組件 4 之間還存在着循環依賴的狀況。一旦這些組件的內部實現和外部接口發生變化,整個 App 就會陷入不穩定的狀態,即所謂牽一髮而動全身
平臺化是組件化的升級,即在組件化的基礎上,對它們提供的功能進行分類,統一分層劃分,增長依賴治理的概念。爲了對這些功能單元在概念上進行更爲統一的分類,咱們按照四象限分析法,把應用程序的組件按照業務和 UI 分解爲 4 個維度,來分析組件能夠分爲哪幾類。
能夠看出,通過業務與 UI 的分解以後,這些組件能夠分爲 4 類:
具有 UI 屬性的獨立業務模塊;
不具有 UI 屬性的基礎業務功能;
不具有業務屬性的 UI 控件
不具有業務屬性的基礎功能
按照自身定義,這 4 類組件其實隱含着分層依賴的關係。好比,處於業務模塊中的首頁,依賴位於基礎業務模塊中的帳號功能;再好比,位於 UI 控件模塊中的輪播卡片,依賴位於基礎功能模塊中的存儲管理等功能。咱們將它們按照依賴的前後順序從上到下進行劃分,就是一個完整的 App 了
能夠看到,平臺化與組件化最大的差別在於增長了分層的概念,每一層的功能均基於同層和下層的功能之上,這使得各個組件之間既保持了獨立性,同時也具備必定的彈性,在不越界的狀況下按照功能劃分各司其職。
與組件化更關注組件的獨立性相比,平臺化更關注的是組件之間關係的合理性,而這也是在設計平臺化架構時須要重點考慮的單向依賴原則。
所謂單向依賴原則,指的是組件依賴的順序應該按照應用架構的層數從上到下依賴,不要出現下層模塊依賴上層模塊這樣循環依賴的現象。這樣能夠最大限度地避免複雜的耦合,減小組件化時的困難。若是咱們每一個組件都只是單向依賴其餘組件,各個組件之間的關係都是清晰的,代碼解耦也就會變得很是輕鬆了。平臺化強調依賴的順序性,除了不容許出現下層組件依賴上層組件的狀況,跨層組件和同層組件之間的依賴關係也應當嚴格控制,由於這樣的依賴關係每每會帶來架構設計上的混亂。
若是下層組件確實須要調用上層組件的代碼怎麼辦?
這時,咱們能夠採用增長中間層的方式,好比 Event Bus、Provider 或 Router,以中間層轉發的形式實現信息同步。好比,位於第 4 層的網絡引擎中,會針對特定的錯誤碼跳轉到位於第 1 層的統一錯誤頁,這時咱們就能夠利用 Router 提供的命名路由跳轉,在不感知錯誤頁的實現狀況下來完成。又好比,位於第 2 層的帳號組件中,會在用戶登入登出時主動刷新位於第 1 層的首頁和個人頁面,這時咱們就能夠利用 Event Bus 來觸發帳號切換事件,在不須要獲取頁面實例的狀況下通知它們更新界面。
簡歷不要寫本身不會的
換工做要給本身留足夠的時間,否則很被動,由於不少公司流程太長,兩輪面試之間有可能會間隔一個星期
此次面試有不少想和你們分享的好比簡歷(很重要)、項目問題、回答技巧等,看後續你們感興趣