Android-Flutter面經

正式參加工做以來第一次換工做,很巧的遇上了疫情,倒也省去了面試的奔波。(總結的內容較長,建議使用電腦查看,遇到的算法另寫了一篇 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個小時的面試時間。面試

image.png

也有一面很滿意以後一次性解決的,3個小時的面試算法

image.png

說了這麼多廢話,進入正題吧,面試知識點,我只大概總結下,其實每一個知識點均可以深刻拓展,分了五個模塊java、Android、網絡、dart、flutter數據庫

java

GC機制

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

  • 引用計數法: 當一個對象被引用時,它的引用計數器會加一,垃圾回收時會清理掉引用計數爲0的對象。但這種方法有一個問題,比方說有兩個對象 A 和 B,A 引用了 B,B 又引用了 A,除此以外沒有別的對象引用 A 和 B,那麼 A 和 B 在咱們看來已是垃圾對象,須要被回收,但它們的引用計數不爲 0,沒有達到回收的條件。正由於這個循環引用的問題,Java 並無採用引用計數法。
  • 可達性分析法: 咱們把 Java 中對象引用的關係看作一張圖,從根級對象不可達的對象會被垃圾收集器清除。根級對象通常包括 Java 虛擬機棧中的對象、本地方法棧中的對象、方法區中的靜態對象和常量池中的常量。 回收垃圾的話有這麼四種方法:
  • 標記清除算法: 顧名思義分爲兩步,標記和清除。首先標記到須要回收的垃圾對象,而後回收掉這些垃圾對象。標記清除算法的缺點是清除垃圾對象後會形成內存的碎片化。
  • 複製算法: 複製算法是將存活的對象複製到另外一塊內存區域中,並作相應的內存整理工做。複製算法的優勢是能夠避免內存碎片化,缺點也顯而易見,它須要兩倍的內存。
  • 標記整理算法: 標記整理算法也是分兩步,先標記後整理。它會標記須要回收的垃圾對象,清除掉垃圾對象後會將存活的對象壓縮,避免了內存的碎片化。
  • 分代算法: 分代算法將對象分爲新生代和老年代對象。那麼爲何作這樣的區分呢?主要是在Java運行中會產生大量對象,這些對象的生命週期會有很大的不一樣,有的生命週期很長,有的甚至使用一次以後就再也不使用。因此針對不一樣生命週期的對象採用不一樣的回收策略,這樣能夠提升GC的效率。

新生代對象分爲三個區域:Eden 區和兩個 Survivor 區。新建立的對象都放在 Eden區,當 Eden 區的內存達到閾值以後會觸發 Minor GC,這時會將存活的對象複製到一個 Survivor 區中,這些存活對象的生命存活計數會加一。這時 Eden 區會閒置,當再一次達到閾值觸發 Minor GC 時,會將Eden區和以前一個 Survivor 區中存活的對象複製到另外一個 Survivor 區中,採用的是我以前提到的複製算法,同時它們的生命存活計數也會加一。設計模式

這個過程會持續不少遍,直到對象的存活計數達到必定的閾值後會觸發一個叫作晉升的現象:新生代的這個對象會被放置到老年代中。 老年代中的對象都是通過屢次 GC 依然存活的生命週期很長的 Java 對象。當老年代的內存達到閾值後會觸發 Major GC,採用的是標記整理算法。

JVM內存區域的劃分,哪些區域會發生 OOM

JVM 的內存區域能夠分爲兩類:線程私有和區域和線程共有的區域。 線程私有的區域:程序計數器、JVM 虛擬機棧、本地方法棧 線程共有的區域:堆、方法區、運行時常量池

  • 程序計數器。 每一個線程有有一個私有的程序計數器,任什麼時候間一個線程都只會有一個方法正在執行,也就是所謂的當前方法。程序計數器存放的就是這個當前方法的JVM指令地址。
  • JVM虛擬機棧。 建立線程的時候會建立線程內的虛擬機棧,棧中存放着一個個的棧幀,對應着一個個方法的調用。JVM 虛擬機棧有兩種操做,分別是壓棧和出站。棧幀中存放着局部變量表、方法返回值和方法的正常或異常退出的定義等等。
  • 本地方法棧。 跟 JVM 虛擬機棧比較相似,只不過它支持的是 Native 方法。
  • 堆。 堆是內存管理的核心區域,用來存放對象實例。幾乎全部建立的對象實例都會直接分配到堆上。因此堆也是垃圾回收的主要區域,垃圾收集器會對堆有着更細的劃分,最多見的就是把堆劃分爲新生代和老年代。
  • 方法區。方法區主要存放類的結構信息,好比靜態屬性和方法等等。
  • 運行時常量池。運行時常量池位於方法區中,主要存放各類常量信息。

其實除了程序計數器,其餘的部分都會發生 OOM。

  • 堆。 一般發生的 OOM 都會發生在堆中,最多見的可能致使 OOM 的緣由就是內存泄漏。
  • JVM虛擬機棧和本地方法棧。 當咱們寫一個遞歸方法,這個遞歸方法沒有循環終止條件,最終會致使 StackOverflow 的錯誤。固然,若是棧空間擴展失敗,也是會發生 OOM 的。
  • 方法區。方法區如今基本上不太會發生 OOM,但在早期內存中加載的類信息過多的狀況下也是會發生 OOM 的。

java的內存模型

Java內存模型規定了全部的變量都存儲在主內存中,每條線程還有本身的工做內存,線程的工做內存中保存了該線程中是用到的變量的主內存副本拷貝,線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量的傳遞均須要本身的工做內存和主存之間進行數據同步進行。

原子性

在Java中,爲了保證原子性,提供了兩個高級的字節碼指令monitorentermonitorexit。在synchronized的實現原理文章中,介紹過,這兩個字節碼,在Java中對應的關鍵字就是synchronized

所以,在Java中能夠使用synchronized來保證方法和代碼塊內的操做是原子性的。

可見性

Java內存模型是經過在變量修改後將新值同步回主內存,在變量讀取前從主內存刷新變量值的這種依賴主內存做爲傳遞媒介的方式來實現的。

Java中的volatile關鍵字提供了一個功能,那就是被其修飾的變量在被修改後能夠當即同步到主內存,被其修飾的變量在每次是用以前都從主內存刷新。所以,能夠使用volatile來保證多線程操做時變量的可見性。

除了volatile,Java中的synchronizedfinal兩個關鍵字也能夠實現可見性。只不過實現方式不一樣,這裏再也不展開了。

有序性

在Java中,能夠使用synchronizedvolatile來保證多線程之間操做的有序性。實現方式有所區別:

volatile關鍵字會禁止指令重排。synchronized關鍵字保證同一時刻只容許一條線程操做。

好了,這裏簡單的介紹完了Java併發編程中解決原子性、可見性以及有序性能夠使用的關鍵字。讀者可能發現了,好像synchronized關鍵字是萬能的,他能夠同時知足以上三種特性,這其實也是不少人濫用synchronized的緣由。

可是synchronized是比較影響性能的,雖然編譯器提供了不少鎖優化技術,可是也不建議過分使用

類加載過程

Java 中類加載分爲 3 個步驟:加載、連接、初始化。

  • 加載。 加載是將字節碼數據從不一樣的數據源讀取到JVM內存,並映射爲 JVM 承認的數據結構,也就是 Class 對象的過程。數據源能夠是 Jar 文件、Class 文件等等。若是數據的格式並非 ClassFile 的結構,則會報 ClassFormatError。
  • 連接。連接是類加載的核心部分,這一步分爲 3 個步驟:驗證、準備、解析。
  • 驗證。 驗證是保證JVM安全的重要步驟。JVM須要校驗字節信息是否符合規範,避免惡意信息和不規範數據危害JVM運行安全。若是驗證出錯,則會報VerifyError。
  • 準備。 這一步會建立靜態變量,併爲靜態變量開闢內存空間。
  • 解析。 這一步會將符號引用替換爲直接引用。
  • 初始化。 初始化會爲靜態變量賦值,並執行靜態代碼塊中的邏輯。

雙親委派模型

類加載器大體分爲3類:啓動類加載器、擴展類加載器、應用程序類加載器。

  • 啓動類加載器主要加載 jre/lib下的jar文件。
  • 擴展類加載器主要加載 jre/lib/ext 下的jar文件。
  • 應用程序類加載器主要加載 classpath 下的文件。

所謂的雙親委派模型就是當加載一個類時,會優先使用父類加載器加載,當父類加載器沒法加載時纔會使用子類加載器去加載。這麼作的目的是爲了不類的重複加載。

HashMap 的原理

HashMap 的內部能夠看作數組+鏈表的複合結構。數組被分爲一個個的桶(bucket)。哈希值決定了鍵值對在數組中的尋址。具備相同哈希值的鍵值對會組成鏈表。須要注意的是當鏈表長度超過閾值(默認是8)的時候會觸發樹化,鏈表會變成樹形結構。

把握HashMap的原理須要關注4個方法:hash、put、get、resize。

  • hash方法。 將 key 的 hashCode 值的高位數據移位到低位進行異或運算。這麼作的緣由是有些 key 的 hashCode 值的差別集中在高位,而哈希尋址是忽略容量以上高位的,這種作法能夠有效避免哈希衝突。
  • put 方法。put 方法主要有如下幾個步驟:
  • 經過 hash 方法獲取 hash 值,根據 hash 值尋址。
  • 若是未發生碰撞,直接放到桶中。
  • 若是發生碰撞,則以鏈表形式放在桶後。
  • 當鏈表長度大於閾值後會觸發樹化,將鏈表轉換爲紅黑樹。
  • 若是數組長度達到閾值,會調用 resize 方法擴展容量。
  • get方法。get 方法主要有如下幾個步驟:
  • 經過 hash 方法獲取 hash 值,根據 hash 值尋址。
  • 若是與尋址到桶的 key 相等,直接返回對應的 value。
  • 若是發生衝突,分兩種狀況。若是是樹,則調用 getTreeNode 獲取 value;若是是鏈表則經過循環遍歷查找對應的 value。
  • resize 方法。resize 作了兩件事:
  • 將原數組擴展爲原來的 2 倍
  • 從新計算 index 索引值,將原節點從新放到新的數組中。這一步能夠將原先衝突的節點分散到新的桶中。

sleep 和 wait 的區別

  • sleep 方法是 Thread 類中的靜態方法,wait 是 Object 類中的方法
  • sleep 並不會釋放同步鎖,而 wait 會釋放同步鎖
  • sleep 能夠在任何地方使用,而 wait 只能在同步方法或者同步代碼塊中使用
  • sleep 中必須傳入時間,而 wait 能夠傳,也能夠不傳,不傳時間的話只有 notify 或者 notifyAll 才能喚醒,傳時間的話在時間以後會自動喚醒

join 的用法

join 方法一般是保證線程間順序調度的一個方法,它是 Thread 類中的方法。比方說在線程 A 中執行線程 B.join(),這時線程 A 會進入等待狀態,直到線程 B 執行完畢以後纔會喚醒,繼續執行A線程中的後續方法。

join 方法能夠傳時間參數,也能夠不傳參數,不傳參數實際上調用的是 join(0)。它的原理實際上是使用了 wait 方法,join 的原理以下:

public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }
複製代碼複製代碼

volatile

通常提到 volatile,就不得不提到內存模型相關的概念。咱們都知道,在程序運行中,每條指令都是由 CPU 執行的,而指令的執行過程當中,勢必涉及到數據的讀取和寫入。程序運行中的數據都存放在主存中,這樣會有一個問題,因爲 CPU 的執行速度是要遠高於主存的讀寫速度,因此直接從主存中讀寫數據會下降 CPU 的效率。爲了解決這個問題,就有了高速緩存的概念,在每一個 CPU 中都有高速緩存,它會事先從主存中讀取數據,在 CPU 運算以後在合適的時候刷新到主存中。

這樣的運行模式在單線程中是沒有任何問題的,但在多線程中,會致使緩存一致性的問題。舉個簡單的例子:i=i+1 ,在兩個線程中執行這句代碼,假設i的初始值爲0。咱們指望兩個線程運行後獲得2,那麼有這樣的一種狀況,兩個線程都從主存中讀取i到各自的高速緩存中,這時候兩個線程中的i都爲0。在線程1執行完畢獲得i=1,將之刷新到主存後,線程2開始執行,因爲線程2中的i是高速緩存中的0,因此在執行完線程2以後刷新到主存的i仍舊是1。

因此這就致使了對共享變量的緩存一致性的問題,那麼爲了解決這個問題,提出了緩存一致性協議:當 CPU 在寫數據時,若是發現操做的是共享變量,它會通知其餘 CPU 將它們內部的這個共享變量置爲無效狀態,當其餘 CPU 讀取緩存中的共享變量時,發現這個變量是無效的,它會重新從主存中讀取最新的值。

在Java的多線程開發中,有三個重要概念:原子性、可見性、有序性。

  • **原子性:**一個或多個操做要麼都不執行,要麼都執行。
  • 可見性: 一個線程中對共享變量(類中的成員變量或靜態變量)的修改,在其餘線程當即可見。
  • 有序性: 程序執行的順序按照代碼的順序執行。 把一個變量聲明爲volatile,其實就是保證了可見性和有序性。 可見性我上面已經說過了,在多線程開發中是頗有必要的。這個有序性仍是得說一下,爲了執行的效率,有時候會發生指令重排,這在單線程中指令重排以後的輸出與咱們的代碼邏輯輸出仍是一致的。但在多線程中就可能發生問題,volatile在必定程度上能夠避免指令重排。

volatile的原理是在生成的彙編代碼中多了一個lock前綴指令,這個前綴指令至關於一個內存屏障,這個內存屏障有3個做用:

  • 確保指令重排的時候不會把屏障後的指令排在屏障前,確保不會把屏障前的指令排在屏障後。
  • 修改緩存中的共享變量後當即刷新到主存中。
  • 當執行寫操做時會致使其餘CPU中的緩存無效。

volatile和synchronize的區別

volatile

它所修飾的變量不保留拷貝,直接訪問主內存中的。

在Java內存模型中,有main memory,每一個線程也有本身的memory (例如寄存器)。爲了性能,一個線程會在本身的memory中保持要訪問的變量的副本。這樣就會出現同一個變量在某個瞬間,在一個線程的memory中的值可能與另一個線程memory中的值,或者main memory中的值不一致的狀況。 一個變量聲明爲volatile,就意味着這個變量是隨時會被其餘線程修改的,所以不能將它cache在線程memory中。

使用場景

您只能在有限的一些情形下使用 volatile 變量替代鎖。要使 volatile 變量提供理想的線程安全,必須同時知足下面兩個條件:

1)對變量的寫操做不依賴於當前值。

2)該變量沒有包含在具備其餘變量的不變式中。

volatile最適用一個線程寫,多個線程讀的場合。

若是有多個線程併發寫操做,仍然須要使用鎖或者線程安全的容器或者原子變量來代替。

synchronized

當它用來修飾一個方法或者一個代碼塊的時候,可以保證在同一時刻最多隻有一個線程執行該段代碼。

  1. 當兩個併發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程獲得執行。另外一個線程必須等待當前線程執行完這個代碼塊之後才能執行該代碼塊。
  2. 然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另外一個線程仍然能夠訪問該object中的非synchronized(this)同步代碼塊。
  3. 尤爲關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其餘線程對object中全部其它synchronized(this)同步代碼塊的訪問將被阻塞。
  4. 當一個線程訪問object的一個synchronized(this)同步代碼塊時,它就得到了這個object的對象鎖。結果,其它線程對該object對象全部同步代碼部分的訪問都被暫時阻塞。

區別

  1. volatile是變量修飾符,而synchronized則做用於一段代碼或方法。
  2. volatile只是在線程內存和「主」內存間同步某個變量的值;而synchronized經過鎖定和解鎖某個監視器同步全部變量的值, 顯然synchronized要比volatile消耗更多資源。
  3. volatile不會形成線程的阻塞;synchronized可能會形成線程的阻塞。
  4. volatile保證數據的可見性,但不能保證原子性;而synchronized能夠保證原子性,也能夠間接保證可見性,由於它會將私有內存中和公共內存中的數據作同步。
  5. volatile標記的變量不會被編譯器優化;synchronized標記的變量能夠被編譯器優化。

線程安全包含原子性和可見性兩個方面,Java的同步機制都是圍繞這兩個方面來確保線程安全的。

關鍵字volatile主要使用的場合是在多個線程中能夠感知實例變量被修改,而且能夠得到最新的值使用,也就是多線程讀取共享變量時能夠得到最新值使用。

關鍵字volatile提示線程每次從共享內存中讀取變量,而不是私有內存中讀取,這樣就保證了同步數據的可見性。可是要注意的是:若是修改實例變量中的數據

ThreadLocal的做用

ThreadLocal的做用是提供線程內的局部變量,說白了,就是在各線程內部建立一個變量的副本,相比於使用各類鎖機制訪問變量,ThreadLocal的思想就是用空間換時間,使各線程都能訪問屬於本身這一份的變量副本,變量值不互相干擾,減小同一個線程內的多個函數或者組件之間一些公共變量傳遞的複雜度。

get函數用來獲取與當前線程關聯的ThreadLocal的值,若是當前線程沒有該ThreadLocal的值,則調用initialValue函數獲取初始值返回,initialValue是protected類型的,因此通常咱們使用時須要繼承該函數,給出初始值。而set函數是用來設置當前線程的該ThreadLocal的值,remove函數用來刪除ThreadLocal綁定的值,在某些狀況下須要手動調用,防止內存泄露。

Java中生產者與消費者模式

生產者消費者模式要保證的是當緩衝區滿的時候生產者再也不生產對象,當緩衝區空時,消費者再也不消費對象。實現機制就是當緩衝區滿時讓生產者處於等待狀態,當緩衝區爲空時讓消費者處於等待狀態。當生產者生產了一個對象後會喚醒消費者,當消費者消費一個對象後會喚醒生產者。

三種種實現方式:wait 和 notify、await 和 signal、BlockingQueue。

  • wait 和 notify
//wait和notify
import java.util.LinkedList;
public class StorageWithWaitAndNotify {
    private final int                MAX_SIZE = 10;
    private       LinkedList<Object> list     = new LinkedList<Object>();
    public void produce() {
        synchronized (list) {
            while (list.size() == MAX_SIZE) {
                System.out.println("倉庫已滿:生產暫停");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.add(new Object());
            System.out.println("生產了一個新產品,現庫存爲:" + list.size());
            list.notifyAll();
        }
    }
    public void consume() {
        synchronized (list) {
            while (list.size() == 0) {
                System.out.println("庫存爲0:消費暫停");
                try {
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            list.remove();
            System.out.println("消費了一個產品,現庫存爲:" + list.size());
            list.notifyAll();
        }
    }
}
複製代碼複製代碼
  • await 和 signal
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class StorageWithAwaitAndSignal {
    private final int                MAX_SIZE = 10;
    private       ReentrantLock      mLock    = new ReentrantLock();
    private       Condition          mEmpty   = mLock.newCondition();
    private       Condition          mFull    = mLock.newCondition();
    private       LinkedList<Object> mList    = new LinkedList<Object>();
    public void produce() {
        mLock.lock();
        while (mList.size() == MAX_SIZE) {
            System.out.println("緩衝區滿,暫停生產");
            try {
                mFull.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        mList.add(new Object());
        System.out.println("生產了一個新產品,現容量爲:" + mList.size());
        mEmpty.signalAll();
        mLock.unlock();
    }
    public void consume() {
        mLock.lock();
        while (mList.size() == 0) {
            System.out.println("緩衝區爲空,暫停消費");
            try {
                mEmpty.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        mList.remove();
        System.out.println("消費了一個產品,現容量爲:" + mList.size());
        mFull.signalAll();
        mLock.unlock();
    }
}
複製代碼複製代碼
  • BlockingQueue
import java.util.concurrent.LinkedBlockingQueue;
public class StorageWithBlockingQueue {
    private final int                         MAX_SIZE = 10;
    private       LinkedBlockingQueue<Object> list     = new LinkedBlockingQueue<Object>(MAX_SIZE);
    public void produce() {
        if (list.size() == MAX_SIZE) {
            System.out.println("緩衝區已滿,暫停生產");
        }
        try {
            list.put(new Object());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("生產了一個產品,現容量爲:" + list.size());
    }
    public void consume() {
        if (list.size() == 0) {
            System.out.println("緩衝區爲空,暫停消費");
        }
        try {
            list.take();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("消費了一個產品,現容量爲:" + list.size());
    }
}
複製代碼複製代碼

final、finally、finalize區別

final 能夠修飾類、變量和方法。修飾類表明這個類不可被繼承。修飾變量表明此變量不可被改變。修飾方法表示此方法不可被重寫 (override)。

finally 是保證重點代碼必定會執行的一種機制。一般是使用 try-finally 或者 try-catch-finally 來進行文件流的關閉等操做。

finalize 是 Object 類中的一個方法,它的設計目的是保證對象在垃圾收集前完成特定資源的回收。finalize 機制如今已經不推薦使用,而且在 JDK 9已經被標記爲 deprecated。

Java 中單例模式

Java 中常見的單例模式實現有這麼幾種:餓漢式、雙重判斷的懶漢式、靜態內部類實現的單例、枚舉實現的單例。 這裏着重講一下雙重判斷的懶漢式和靜態內部類實現的單例。

雙重判斷的懶漢式:

public class SingleTon {
    //須要注意的是volatile
    private static volatile SingleTon mInstance;
    private SingleTon() {
    }
    public static SingleTon getInstance() {
        if (mInstance == null) { 
            synchronized (SingleTon.class) {
                if (mInstance == null) {
                    mInstance=new SingleTon();
                }
            }
        }
        return mInstance;
    }
}
複製代碼複製代碼

雙重判斷的懶漢式單例既知足了延遲初始化,又知足了線程安全。經過 synchronized 包裹代碼來實現線程安全,經過雙重判斷來提升程序執行的效率。這裏須要注意的是單例對象實例須要有 volatile 修飾,若是沒有 volatile 修飾,在多線程狀況下可能會出現問題。緣由是這樣的,mInstance=new SingleTon()這一句代碼並非一個原子操做,它包含三個操做:

  1. 給 mInstance 分配內存
  2. 調用 SingleTon 的構造方法初始化成員變量
  3. 將 mInstance 指向分配的內存空間(在這一步 mInstance 已經不爲 null 了)

咱們知道 JVM 會發生指令重排,正常的執行順序是1-2-3,但發生指令重排後可能會致使1-3-2。咱們考慮這樣一種狀況,當線程 A 執行到1-3-2的3步驟暫停了,這時候線程 B 調用了 getInstance,走到了最外層的if判斷上,因爲最外層的 if 判斷並無 synchronized 包裹,因此能夠執行到這一句,這時候因爲線程 A 已經執行了步驟3,此時 mInstance 已經不爲 null 了,因此線程B直接返回了 mInstance。但其實咱們知道,完整的初始化必須走完這三個步驟,因爲線程 A 只走了兩個步驟,因此必定會報錯的。

解決的辦法就是使用 volatile 修飾 mInstance,咱們知道 volatile 有兩個做用:保證可見性和禁止指令重排,在這裏關鍵在於禁止指令重排,禁止指令重排後保證了不會發生上述問題。

靜態內部類實現的單例:

class SingletonWithInnerClass {
    private SingletonWithInnerClass() {
    }
    private static class SingletonHolder{
        private static SingletonWithInnerClass INSTANCE=new SingletonWithInnerClass();
    }
    public SingletonWithInnerClass getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
複製代碼複製代碼

因爲外部類的加載並不會致使內部類當即加載,只有當調用 getInstance 的時候纔會加載內部類,因此實現了延遲初始化。因爲類只會被加載一次,而且類加載也是線程安全的,因此知足咱們全部的需求。靜態內部類實現的單例也是最爲推薦的一種方式。

Java中引用類型的區別,具體的使用場景

Java中引用類型分爲四類:強引用、軟引用、弱引用、虛引用。

  • 強引用: 強引用指的是經過 new 對象建立的引用,垃圾回收器即便是內存不足也不會回收強引用指向的對象。
  • 軟引用: 軟引用是經過 SoftRefrence 實現的,它的生命週期比強引用短,在內存不足,拋出 OOM 以前,垃圾回收器會回收軟引用引用的對象。軟引用常見的使用場景是存儲一些內存敏感的緩存,當內存不足時會被回收。
  • 弱引用: 弱引用是經過 WeakRefrence 實現的,它的生命週期比軟引用還短,GC 只要掃描到弱引用的對象就會回收。弱引用常見的使用場景也是存儲一些內存敏感的緩存。
  • 虛引用: 虛引用是經過 FanttomRefrence 實現的,它的生命週期最短,隨時可能被回收。若是一個對象只被虛引用引用,咱們沒法經過虛引用來訪問這個對象的任何屬性和方法。它的做用僅僅是保證對象在 finalize 後,作某些事情。虛引用常見的使用場景是跟蹤對象被垃圾回收的活動,當一個虛引用關聯的對象被垃圾回收器回收以前會收到一條系統通知。

Exception 和 Error的區別

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

Exception 和 Error 體現了 Java 對不一樣異常狀況的分類。Exception 是程序正常運行中,能夠預料的意外狀況,可能而且應該被捕獲,進行相應的處理。

Error 是指在正常狀況下,不大可能出現的狀況,絕大部分 Error 都會使程序處於非正常、不可恢復的狀態。既然是非正常,因此不便於也不須要捕獲,常見的 OutOfMemoryError 就是 Error 的子類。

Exception 又分爲 checked Exception 和 unchecked Exception。

  • checked Exception 在代碼裏必須顯式的進行捕獲,這是編譯器檢查的一部分。
  • unchecked Exception 也就是運行時異常,相似空指針異常、數組越界等,一般是能夠避免的邏輯錯誤,具體根據需求來判斷是否須要捕獲,並不會在編譯器強制要求。

線程的幾個常見方法的比較

  1. Thread.sleep(long millis),必定是當前線程調用此方法,當前線程進入TIMED_WAITING狀態,但不釋放對象鎖,millis後線程自動甦醒進入就緒狀態。做用:給其它線程執行機會的最佳方式。
  2. Thread.yield(),必定是當前線程調用此方法,當前線程放棄獲取的CPU時間片,但不釋放鎖資源,由運行狀態變爲就緒狀態,讓OS再次選擇線程。做用:讓相同優先級的線程輪流執行,但並不保證必定會輪流執行。實際中沒法保證yield()達到讓步目的,由於讓步的線程還有可能被線程調度程序再次選中。Thread.yield()不會致使阻塞。該方法與sleep()相似,只是不能由用戶指定暫停多長時間。
  3. thread.join()/thread.join(long millis)當前線程裏調用其它線程thread的join方法,當前線程進入WAITING/TIMED_WAITING狀態,當前線程不會釋放已經持有的對象鎖。線程thread執行完畢或者millis時間到,當前線程進入就緒狀態。
  4. thread.interrupt(),當前線程裏調用其它線程thread的interrupt()方法,中斷指定的線程。
    若是指定線程調用了wait()方法組或者join方法組在阻塞狀態,那麼指定線程會拋出InterruptedException
  5. Thread.interrupted,必定是當前線程調用此方法,檢查當前線程是否被設置了中斷,該方法會重置當前線程的中斷標誌,返回當前線程是否被設置了中斷。
  6. thread.isInterrupted()當前線程裏調用其它線程thread的isInterrupted()方法,返回指定線程是否被中斷
  7. object.wait()當前線程調用對象的wait()方法,當前線程釋放對象鎖,進入等待隊列。依靠notify()/notifyAll()喚醒或者wait(long timeout) timeout時間到自動喚醒。
  8. object.notify()喚醒在此對象監視器上等待的單個線程,選擇是任意性的。notifyAll()喚醒在此對象監視器上等待的全部線程。

Object.wait() / Object.notify() Object.notifyAll()

任意一個Java對象,都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()、

wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,能夠

實現等待/通知模式

  1. 使用的前置條件
    當咱們想要使用Object的監視器方法時,須要或者該Object的鎖,代碼以下所示
    synchronized(obj){ .... //1 obj.wait();//2 obj.wait(long millis);//2 ....//3 }
    一個線程得到obj的鎖,作了一些時候事情以後,發現須要等待某些條件的發生,調用obj.wait(),該線程會釋放obj的鎖,並阻塞在上述的代碼2處
    obj.wait()和obj.wait(long millis)的區別在於
    obj.wait()是無限等待,直到obj.notify()或者obj.notifyAll()調用並喚醒該線程,該線程獲取鎖以後繼續執行代碼3
    obj.wait(long millis)是超時等待,我只等待long millis 後,該線程會本身醒來,醒來以後去獲取鎖,獲取鎖以後繼續執行代碼3
    obj.notify()是叫醒任意一個等待在該對象上的線程,該線程獲取鎖,線程狀態從BLOCKED進入RUNNABLE
    obj.notifyAll()是叫醒全部等待在該對象上的線程,這些線程會去競爭鎖,獲得鎖的線程狀態從BLOCKED進入RUNNABLE,其餘線程依然是BLOCKED,獲得鎖的線程執行代碼3完畢後釋放鎖,其餘線程繼續競爭鎖,如此反覆直到全部線程執行完畢。
    synchronized(obj){ .... //1 obj.notify();//2 obj.notifyAll();//2 }
    一個線程得到obj的鎖,作了一些時候事情以後,某些條件已經知足,調用obj.notify()或者obj.notifyAll(),該線程會釋放obj的鎖,並叫醒在obj上等待的線程,
    obj.notify()和obj.notifyAll()的區別在於
    obj.notify()叫醒在obj上等待的任意一個線程(由JVM決定)
    obj.notifyAll()叫醒在obj上等待的所有線程
  2. 使用範式
    synchronized(obj){ //判斷條件,這裏使用while,而不使用ifwhile(obj知足/不知足 某個條件){ obj.wait() } }
    放在while裏面,是防止處於WAITING狀態下線程監測的對象被別的緣由調用了喚醒(notify或者notifyAll)方法,可是while裏面的條件並無知足(也可能當時知足了,可是因爲別的線程操做後,又不知足了),就須要再次調用wait將其掛起

強軟弱虛引用以及使用場景

強引用:

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. 程序能夠經過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。若是程序發現某個虛引用已經被加入到引用隊列,那麼就能夠在所引用的對象的內存被回收以前採起必要的行動

Android

冷啓動與熱啓動是什麼,區別,如何優化,使用場景等

app冷啓動: 當應用啓動時,後臺沒有該應用的進程,這時系統會從新建立一個新的進程分配給該應用, 這個啓動方式就叫作冷啓動(後臺不存在該應用進程)。冷啓動由於系統會從新建立一個新的進程分配給它,因此會先建立和初始化Application類,再建立和初始化MainActivity類(包括一系列的測量、佈局、繪製),最後顯示在界面上。

app熱啓動: 當應用已經被打開, 可是被按下返回鍵、Home鍵等按鍵時回到桌面或者是其餘程序的時候,再從新打開該app時, 這個方式叫作熱啓動(後臺已經存在該應用進程)。熱啓動由於會從已有的進程中來啓動,因此熱啓動就不會走Application這步了,而是直接走MainActivity(包括一系列的測量、佈局、繪製),因此熱啓動的過程只須要建立和初始化一個MainActivity就好了,而沒必要建立和初始化Application

冷啓動的流程

當點擊app的啓動圖標時,安卓系統會從Zygote進程中fork建立出一個新的進程分配給該應用,以後會依次建立和初始化Application類、建立MainActivity類、加載主題樣式Theme中的windowBackground等屬性設置給MainActivity以及配置Activity層級上的一些屬性、再inflate佈局、當onCreate/onStart/onResume方法都走完了後最後才進行contentViewmeasure/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的主界面。

Activity啓動流程

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。

Activity生命週期

image.png

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。

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、鎖屏鍵、來電顯示等系統應用

activity橫豎屏切換時activity的生命週期,view的生命週期

  • 不配置configChanges時:切換橫豎屏時生命週期各自都會走一遍
  • 配置configChanges時:必須設置爲android:configChanges="orientation|screenSize"時,纔不會重走生命週期方法,只會回調onConfigurationChanged方法,注意,不配置configChanges或是配置了但不一樣時包含這兩個值時,都會重走一遍生命週期方法,而且不會回調onConfigurationChanged方法。
  • 另外重走生命週期方法時,還會調用onSaveInstanceState() onRestoreIntanceState(),資源相關的系統配置發生改變或者資源不足:例如屏幕旋轉,當前Activity會銷燬,而且在onStop以前回調onSaveInstanceState保存數據,在從新建立Activity的時候在onStart以後回調onRestoreInstanceState。其中Bundle數據會傳到onCreate(不必定有數據)和onRestoreInstanceState(必定有數據)。用戶或者程序員主動去銷燬一個Activity的時候不會回調,其餘狀況都會調用,來保存界面信息。如代碼中finish()或用戶按下back,不會回調。

五種進程

第一高:前臺進程

前臺進程是Android系統中最重要的進程,是與用戶正在交互的進程。

第二高:可見進程

可見進程指部分程序界面可以被用戶看見,卻不在前臺與用戶交互。

第三高:服務進程

一個包含已啓動服務的進程就是服務進程,服務沒有用戶界面,不與用戶直接交互,但可以在後臺長期運行,提供用戶所關心的重要功能。

第四高:後臺進程

若是一個進程不包含任何已經啓動的服務,並且沒有用戶可見的Activity,則這個進程就是後臺進程。

第五高:空進程

空進程是不包含任何活躍組件的進程。在系統資源緊張時會被首先清楚。

startService和bindService的區別,生命週期以及使用場景

image.png

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的狀況,結束服務時須要注意的事項:

  • Service的終止,須要unbindService和stopService都調用才行;

順便提一下IntentService,與Service的區別在於它內部封裝了一個工做線程,也就是說,在其內部onHandleIntent的代碼都是在子線程裏面工做的。

image.png


Android中IntentService有何優勢

IntentService是一個經過Context.startService(Intent)啓動能夠處理異步請求的Service,使用時你只須要繼承IntentService和重寫其中的onHandleIntent(Intent)方法接收一個Intent對象,在適當的時候會中止本身(通常在工做完成的時候). 全部的請求的處理都在一個工做線程中完成,它們會交替執行(但不會阻塞主線程的執行),一次只能執行一個請求。

這是一個基於消息的服務,每次啓動該服務並非立刻處理你的工做,而是首先會建立對應的Looper,Handler而且在MessageQueue中添加的附帶客戶Intent的Message對象,當Looper發現有Message的時候接着獲得Intent對象經過在onHandleIntent((Intent)msg.obj)中調用你的處理程序.處理完後即會中止本身的服務.意思是Intent的生命週期跟你的處理的任務是一致的.因此這個類用下載任務中很是好,下載任務結束後服務自身就會結束退出.

進程間通訊的方式有哪幾種

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


廣播靜態註冊和動態註冊的區別

  1. 動態註冊廣播不是常駐型廣播,也就是說廣播跟隨 Activity 的生命週期。注意在 Activity 結束前,移除廣播接收器。 靜態註冊是常駐型,也就是說當應用程序關閉後,若是有信息廣播來,程序也會被系統調用自動運行。
  2. 當廣播爲有序廣播時:優先級高的先接收(不分靜態和動態)。同優先級的廣播接收器,動態優先於靜態
  3. 同優先級的同類廣播接收器,靜態:先掃描的優先於後掃描的,動態:先註冊的優先於後註冊的。
  4. 當廣播爲默認廣播時:無視優先級,動態廣播接收器優先於靜態廣播接收器。同優先級的同類廣播接收器,靜態:先掃描的優先於後掃描的,動態:先註冊的優先於後冊的。

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

Android 中經常使用的性能優化工具包括這些:Android Studio 自帶的 Android Profiler、LeakCanary、BlockCanary

Android 自帶的 Android Profiler 其實就很好用,Android Profiler 能夠檢測三個方面的性能問題:CPU、MEMORY、NETWORK。

LeakCanary 是一個第三方的檢測內存泄漏的庫,咱們的項目集成以後 LeakCanary 會自動檢測應用運行期間的內存泄漏,並將之輸出給咱們。

BlockCanary 也是一個第三方檢測UI卡頓的庫,項目集成後Block也會自動檢測應用運行期間的UI卡頓,並將之輸出給咱們。

Android中的類加載器

  • PathClassLoader,只能加載系統中已經安裝過的 apk
  • DexClassLoader,能夠加載 jar/apk/dex,能夠從 SD卡中加載未安裝的 apk

Android中的動畫有哪幾類,它們的特色和區別是什麼

Android中動畫大體分爲3類:幀動畫、補間動畫(Tween Animation)、屬性動畫(Property Animation)。

  • 幀動畫:經過xml配置一組圖片,動態播放。不多會使用。
  • 補間動畫(Tween Animation):大體分爲旋轉、透明、縮放、位移四類操做。不多會使用。
  • 屬性動畫(Property Animation):屬性動畫是如今使用的最多的一種動畫,它比補間動畫更增強大。屬性動畫大體分爲兩種使用類型,分別是 ViewPropertyAnimator 和 ObjectAnimator。前者適合一些通用的動畫,好比旋轉、位移、縮放和透明,使用方式也很簡單經過 View.animate() 便可獲得 ViewPropertyAnimator,以後進行相應的動畫操做便可。後者適合用於爲咱們的自定義控件添加動畫,固然首先咱們應該在自定義 View 中添加相應的 getXXX()setXXX() 相應屬性的 getter 和 setter 方法,這裏須要注意的是在 setter 方法內改變了自定義 View 中的屬性後要調用 invalidate() 來刷新View的繪製。以後調用 ObjectAnimator.of 屬性類型()返回一個 ObjectAnimator,調用 start() 方法啓動動畫便可。

補間動畫與屬性動畫的區別:

  • 補間動畫是父容器不斷的繪製 view,看起來像移動了效果,其實 view 沒有變化,還在原地。
  • 是經過不斷改變 view 內部的屬性值,真正的改變 view。

TimeInterpolator(時間插值器)

做用:根據時間流逝的百分比計算出當前屬性值改變的百分比

系統已有的插值器:

  • LinearInterpolator(線性插值器):勻速動畫。
  • AccelerateDecelerateInterpolator(加速減速插值器):動畫兩頭慢,中間快。
  • DecelerateInterpolator(減速插值器):動畫愈來愈慢。

TypeEvaluator(類型估值算法,即估值器):

做用:根據當前屬性改變的百分比來計算改變後的屬性值。

系統已有的估值器:

  • IntEvaluator:針對整型屬性
  • FloatEvaluator:針對浮點型屬性
  • ArgbEvaluator:針對Color屬性

Handler 機制

說到 Handler,就不得不提與之密切相關的這幾個類:Message、MessageQueue,Looper。

  • Message。Message 中有兩個成員變量值得關注:target 和 callback。
  • target 其實就是發送消息的 Handler 對象
  • callback 是當調用 handler.post(runnable) 時傳入的 Runnable 類型的任務。post 事件的本質也是建立了一個 Message,將咱們傳入的這個 runnable 賦值給建立的Message的 callback 這個成員變量。
  • MessageQueue。 消息隊列很明顯是存放消息的隊列,值得關注的是 MessageQueue 中的 next() 方法,它會返回下一個待處理的消息。
  • Looper。Looper 消息輪詢器實際上是鏈接 Handler 和消息隊列的核心。首先咱們都知道,若是想要在一個線程中建立一個 Handler,首先要經過Looper.prepare()建立 Looper,以後還得調用Looper.loop()開啓輪詢。咱們着重看一下這兩個方法。
  • prepare() 這個方法作了兩件事:首先經過ThreadLocal.get()獲取當前線程中的Looper,若是不爲空,則會拋出一個RunTimeException,意思是一個線程不能建立2個Looper。若是爲null則執行下一步。第二步是建立了一個Looper,並經過 ThreadLocal.set(looper)。將咱們建立的Looper與當前線程綁定。這裏須要提一下的是消息隊列的建立其實就發生在Looper的構造方法中。
  • loop() 這個方法開啓了整個事件機制的輪詢。它的本質是開啓了一個死循環,不斷的經過 MessageQueue的next()方法獲取消息。拿到消息後會調用 msg.target.dispatchMessage()來作處理。其實咱們在說到 Message 的時候提到過,msg.target 其實就是發送這個消息的 handler。這句代碼的本質就是調用 handler的dispatchMessage()。
  • Handler。上面作了這麼多鋪墊,終於到了最重要的部分。Handler 的分析着重在兩個部分:發送消息和處理消息。*發送消息。其實發送消息除了 sendMessage 以外還有 sendMessageDelayed 和 post 以及 postDelayed 等等不一樣的方式。但它們的本質都是調用了 sendMessageAtTime。在 sendMessageAtTime 這個方法中調用了 enqueueMessage。在 enqueueMessage 這個方法中作了兩件事:經過msg.target = this實現了消息與當前 handler 的綁定。而後經過queue.enqueueMessage實現了消息入隊。
  • 處理消息。 消息處理的核心其實就是dispatchMessage()這個方法。這個方法裏面的邏輯很簡單,先判斷 msg.callback 是否爲 null,若是不爲空則執行這個 runnable。若是爲空則會執行咱們的handleMessage方法。

Handler面試知識點

www.yuque.com/docs/share/…

子線程更新ui會怎麼樣,爲何不讓子線程更新ui,在oncreate裏用子 線程更新ui爲何不會報錯

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 性能優化

Android 中的性能優化在我看來分爲如下幾個方面:內存優化、佈局優化、網絡優化、安裝包優化。

  • 內存優化: 下一個問題就是。
  • 佈局優化:佈局優化的本質就是減小 View 的層級。常見的佈局優化方案以下
  • 在 LinearLayout 和 RelativeLayout 均可以完成佈局的狀況下優先選擇 RelativeLayout,能夠減小 View 的層級
  • 將經常使用的佈局組件抽取出來使用 \< include \>標籤
  • 經過 \< ViewStub \>標籤來加載不經常使用的佈局
  • 使用 \< Merge \>標籤來減小布局的嵌套層次
  • 網絡優化:常見的網絡優化方案以下
  • 儘可能減小網絡請求,可以合併的就儘可能合併
  • 避免 DNS 解析,根據域名查詢可能會耗費上百毫秒的時間,也可能存在DNS劫持的風險。能夠根據業務需求採用增長動態更新 IP 的方式,或者在 IP 方式訪問失敗時切換到域名訪問方式。
  • 大量數據的加載採用分頁的方式
  • 網絡數據傳輸採用 GZIP 壓縮
  • 加入網絡數據的緩存,避免頻繁請求網絡
  • 上傳圖片時,在必要的時候壓縮圖片
  • 安裝包優化:安裝包優化的核心就是減小 apk 的體積,常見的方案以下
  • 使用混淆,能夠在必定程度上減小 apk 體積,但實際效果微乎其微
  • 減小應用中沒必要要的資源文件,好比圖片,在不影響 APP 效果的狀況下儘可能壓縮圖片,有必定的效果
  • 在使用了 SO 庫的時候優先保留 v7 版本的 SO 庫,刪掉其餘版本的SO庫。緣由是在 2018 年,v7 版本的 SO 庫能夠知足市面上絕大多數的要求,可能八九年前的手機知足不了,但咱們也不必去適配老掉牙的手機。實際開發中減小 apk 體積的效果是十分顯著的,若是你使用了不少 SO 庫,比方說一個版本的SO庫一共 10M,那麼只保留 v7 版本,刪掉 armeabi 和 v8 版本的 SO 庫,一共能夠減小 20M 的體積。

Android 內存優化

Android的內存優化在我看來分爲兩點:避免內存泄漏、擴大內存,其實就是開源節流。

其實內存泄漏的本質就是較長生命週期的對象引用了較短生命週期的對象。

常見的內存泄漏
  • 單例模式致使的內存泄漏。 最多見的例子就是建立這個單例對象須要傳入一個 Context,這時候傳入了一個 Activity 類型的 Context,因爲單例對象的靜態屬性,致使它的生命週期是從單例類加載到應用程序結束爲止,因此即便已經 finish 掉了傳入的 Activity,因爲咱們的單例對象依然持有 Activity 的引用,因此致使了內存泄漏。解決辦法也很簡單,不要使用 Activity 類型的 Context,使用 Application 類型的 Context 能夠避免內存泄漏。
  • 靜態變量致使的內存泄漏。 靜態變量是放在方法區中的,它的生命週期是從類加載到程序結束,能夠看到靜態變量生命週期是很是久的。最多見的因靜態變量致使內存泄漏的例子是咱們在 Activity 中建立了一個靜態變量,而這個靜態變量的建立須要傳入 Activity 的引用 this。在這種狀況下即便 Activity 調用了 finish 也會致使內存泄漏。緣由就是由於這個靜態變量的生命週期幾乎和整個應用程序的生命週期一致,它一直持有 Activity 的引用,從而致使了內存泄漏。
  • **非靜態內部類致使的內存泄漏。**非靜態內部類致使內存泄漏的緣由是非靜態內部類持有外部類的引用,最多見的例子就是在 Activity 中使用 Handler 和 Thread 了。使用非靜態內部類建立的 Handler 和 Thread 在執行延時操做的時候會一直持有當前Activity的引用,若是在執行延時操做的時候就結束 Activity,這樣就會致使內存泄漏。解決辦法有兩種:第一種是使用靜態內部類,在靜態內部類中使用弱引用調用Activity。第二種方法是在 Activity 的 onDestroy 中調用 handler.removeCallbacksAndMessages 來取消延時事件。
  • 使用資源未及時關閉致使的內存泄漏。常見的例子有:操做各類數據流未及時關閉,操做 Bitmap 未及時 recycle 等等。
  • 使用第三方庫未能及時解綁。有的三方庫提供了註冊和解綁的功能,最多見的就 EventBus 了,咱們都知道使用 EventBus 要在 onCreate 中註冊,在 onDestroy 中解綁。若是沒有解綁的話,EventBus 實際上是一個單例模式,他會一直持有 Activity 的引用,致使內存泄漏。一樣常見的還有 RxJava,在使用 Timer 操做符作了一些延時操做後也要注意在 onDestroy 方法中調用 disposable.dispose()來取消操做。
  • 屬性動畫致使的內存泄漏。常見的例子就是在屬性動畫執行的過程當中退出了 Activity,這時 View 對象依然持有 Activity 的引用從而致使了內存泄漏。解決辦法就是在 onDestroy 中調用動畫的 cancel 方法取消屬性動畫。
  • WebView 致使的內存泄漏。WebView 比較特殊,即便是調用了它的 destroy 方法,依然會致使內存泄漏。其實避免WebView致使內存泄漏的最好方法就是讓WebView所在的Activity處於另外一個進程中,當這個 Activity 結束時殺死當前 WebView 所處的進程便可,我記得阿里釘釘的 WebView 就是另外開啓的一個進程,應該也是採用這種方法避免內存泄漏。

擴大內存

爲何要擴大咱們的內存呢?有時候咱們實際開發中不可避免的要使用不少第三方商業的 SDK,這些 SDK 其實有好有壞,大廠的 SDK 可能內存泄漏會少一些,但一些小廠的 SDK 質量也就不太靠譜一些。那應對這種咱們沒法改變的狀況,最好的辦法就是擴大內存。

擴大內存一般有兩種方法:一個是在清單文件中的 Application 下添加largeHeap="true"這個屬性,另外一個就是同一個應用開啓多個進程來擴大一個應用的總內存空間。第二種方法其實就很常見了,比方說我使用過個推的 S DK,個推的 Service 其實就是處在另一個單獨的進程中。

Android 中的內存優化總的來講就是開源和節流,開源就是擴大內存,節流就是避免內存泄漏。

Binder 機制

在Linux中,爲了不一個進程對其餘進程的干擾,進程之間是相互獨立的。在一個進程中其實還分爲用戶空間和內核空間。這裏的隔離分爲兩個部分,進程間的隔離和進程內的隔離。

既然進程間存在隔離,那其實也是存在着交互。進程間通訊就是 IPC,用戶空間和內核空間的通訊就是系統調用。

Linux 爲了保證獨立性和安全性,進程之間不能直接相互訪問,Android 是基於 Linux 的,因此也是須要解決進程間通訊的問題。

其實 Linux 進程間通訊有不少方式,好比管道、socket 等等。爲何 Android 進程間通訊採用了Binder而不是 Linux

已有的方式,主要是有這麼兩點考慮:性能和安全

  • 性能。 在移動設備上對性能要求是比較嚴苛的。Linux傳統的進程間通訊好比管道、socket等等進程間通訊是須要複製兩次數據,而Binder則只須要一次。因此Binder在性能上是優於傳統進程通訊的。
  • 安全。 傳統的 Linux 進程通訊是不包含通訊雙方的身份驗證的,這樣會致使一些安全性問題。而Binder機制自帶身份驗證,從而有效的提升了安全性。

Binder 是基於 CS 架構的,有四個主要組成部分。

  • Client。 客戶端進程。
  • Server。 服務端進程。
  • ServiceManager。 提供註冊、查詢和返回代理服務對象的功能。
  • Binder 驅動。 主要負責創建進程間的 Binder 鏈接,進程間的數據交互等等底層操做。

Binder 機制主要的流程是這樣的:

  • 服務端經過Binder驅動在 ServiceManager 中註冊咱們的服務。
  • 客戶端經過Binder驅動查詢在 ServiceManager 中註冊的服務。
  • ServiceManager 經過 inder 驅動返回服務端的代理對象。
  • 客戶端拿到服務端的代理對象後便可進行進程間通訊。

LruCache的原理

LruCache 的核心原理就是對 LinkedHashMap 的有效利用,它的內部存在一個 LinkedHashMap 成員變量。值得咱們關注的有四個方法:構造方法、get、put、trimToSize。

  • 構造方法: 在 LruCache 的構造方法中作了兩件事,設置了 maxSize、建立了一個 LinkedHashMap。這裏值得注意的是 LruCache 將 LinkedHashMap的accessOrder 設置爲了 true,accessOrder 就是遍歷這個LinkedHashMap 的輸出順序。true 表明按照訪問順序輸出,false表明按添加順序輸出,由於一般都是按照添加順序輸出,因此 accessOrder 這個屬性默認是 false,但咱們的 LruCache 須要按訪問順序輸出,因此顯式的將 accessOrder 設置爲 true。
  • get方法: 本質上是調用 LinkedHashMap 的 get 方法,因爲咱們將 accessOrder 設置爲了 true,因此每調用一次get方法,就會將咱們訪問的當前元素放置到這個LinkedHashMap的尾部。
  • put方法: 本質上也是調用了 LinkedHashMap 的 put 方法,因爲 LinkedHashMap 的特性,每調用一次 put 方法,也會將新加入的元素放置到 LinkedHashMap 的尾部。添加以後會調用 trimToSize 方法來保證添加後的內存不超過 maxSize。
  • trimToSize方法: trimToSize 方法的內部實際上是開啓了一個 while(true)的死循環,不斷的從 LinkedHashMap 的首部刪除元素,直到刪除以後的內存小於 maxSize 以後使用 break 跳出循環。

其實到這裏咱們能夠總結一下,爲何這個算法叫 最近最少使用 算法呢?原理很簡單,咱們的每次 put 或者get均可以看作一次訪問,因爲 LinkedHashMap 的特性,會將每次訪問到的元素放置到尾部。當咱們的內存達到閾值後,會觸發 trimToSize 方法來刪除 LinkedHashMap 首部的元素,直到當前內存小於 maxSize。爲何刪除首部的元素,緣由很明顯:咱們最近常常訪問的元素都會放置到尾部,那首部的元素確定就是 最近最少使用 的元素了,所以當內存不足時應當優先刪除這些元素。

DiskLruCache原理

設計一個圖片的異步加載框架

設計一個圖片加載框架,確定要用到圖片加載的三級緩存的思想。三級緩存分爲內存緩存、本地緩存和網絡緩存。

內存緩存:將Bitmap緩存到內存中,運行速度快,可是內存容量小。 本地緩存:將圖片緩存到文件中,速度較慢,但容量較大。 網絡緩存:從網絡獲取圖片,速度受網絡影響。

若是咱們設計一個圖片加載框架,流程必定是這樣的:

  • 拿到圖片url後首先從內存中查找BItmap,若是找到直接加載。
  • 內存中沒有找到,會從本地緩存中查找,若是本地緩存能夠找到,則直接加載。
  • 內存和本地都沒有找到,這時會從網絡下載圖片,下載到後會加載圖片,而且將下載到的圖片放到內存緩存和本地緩存中。

上面是一些基本的概念,若是是具體的代碼實現的話,大概須要這麼幾個方面的文件:

  • 首先須要肯定咱們的內存緩存,這裏通常用的都是 LruCache。
  • 肯定本地緩存,一般用的是 DiskLruCache,這裏須要注意的是圖片緩存的文件名通常是 url 被 MD5 加密後的字符串,爲了不文件名直接暴露圖片的 url。
  • 內存緩存和本地緩存肯定以後,須要咱們建立一個新的類 MemeryAndDiskCache,固然,名字隨便起,這個類包含了以前提到的 LruCache 和 DiskLruCache。在 MemeryAndDiskCache 這個類中咱們定義兩個方法,一個是 getBitmap,另外一個是 putBitmap,對應着圖片的獲取和緩存,內部的邏輯也很簡單。getBitmap中按內存、本地的優先級去取 BItmap,putBitmap 中先緩存內存,以後緩存到本地。
  • 在緩存策略類肯定好以後,咱們建立一個 ImageLoader 類,這個類必須包含兩個方法,一個是展現圖片 displayImage(url,imageView),另外一個是從網絡獲取圖片downloadImage(url,imageView)。在展現圖片方法中首先要經過 ImageView.setTag(url),將 url 和 imageView 進行綁定,這是爲了不在列表中加載網絡圖片時會因爲ImageView的複用致使的圖片錯位的 bug。以後會從 MemeryAndDiskCache 中獲取緩存,若是存在,直接加載;若是不存在,則調用從網絡獲取圖片這個方法。從網絡獲取圖片方法不少,這裏我通常都會使用 OkHttp+Retrofit。當從網絡中獲取到圖片以後,首先判斷一下imageView.getTag()與圖片的 url 是否一致,若是一致則加載圖片,若是不一致則不加載圖片,經過這樣的方式避免了列表中異步加載圖片的錯位。同時在獲取到圖片以後會經過 MemeryAndDiskCache 來緩存圖片。

Android中的事件分發機制

在咱們的手指觸摸到屏幕的時候,事件實際上是經過 Activity -> ViewGroup -> View 這樣的流程到達最後響應咱們觸摸事件的 View。

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

咱們的手指觸摸到屏幕的時候,會觸發一個 Action_Down 類型的事件,當前頁面的 Activity 會首先作出響應,也就是說會走到 Activity 的 dispatchTouchEvent() 方法內。在這個方法內部簡單來講是這麼一個邏輯:

  • 調用 getWindow.superDispatchTouchEvent()。
  • 若是上一步返回 true,直接返回 true;不然就 return 本身的 onTouchEvent()。 這個邏輯很好理解,getWindow().superDispatchTouchEvent() 若是返回 true 表明當前事件已經被處理,無需調用本身的 onTouchEvent;不然表明事件並無被處理,須要 Activity 本身處理,也就是調用本身的 onTouchEvent。

getWindow()方法返回了一個 Window 類型的對象,這個咱們都知道,在 Android 中,PhoneWindow 是Window 的惟一實現類。因此這句本質上是調用了``PhoneWindow中的superDispatchTouchEvent()。`

而在 PhoneWindow 的這個方法中實際調用了mDecor.superDispatchTouchEvent(event)。這個 mDecor 就是 DecorView,它是 FrameLayout 的一個子類,在 DecorView 中的 superDispatchTouchEvent() 中調用的是 super.dispatchTouchEvent()。到這裏就很明顯了,DecorView 是一個 FrameLayout 的子類,FrameLayout 是一個 ViewGroup 的子類,本質上調用的仍是 ViewGroup的dispatchTouchEvent()

分析到這裏,咱們的事件已經從 Activity 傳遞到了 ViewGroup,接下來咱們來分析下 ViewGroup 中的這幾個事件處理方法。

在 ViewGroup 中的 dispatchTouchEvent()中的邏輯大體以下:

  • 經過 onInterceptTouchEvent() 判斷當前 ViewGroup 是否攔截事件,默認的 ViewGroup 都是不攔截的;
  • 若是攔截,則 return 本身的 onTouchEvent()
  • 若是不攔截,則根據 child.dispatchTouchEvent()的返回值判斷。若是返回 true,則 return true;不然 return 本身的 onTouchEvent(),在這裏實現了未處理事件的向上傳遞。

一般狀況下 ViewGroup 的 onInterceptTouchEvent()都返回 false,也就是不攔截。這裏須要注意的是事件序列,好比 Down 事件、Move 事件......Up事件,從 Down 到 Up 是一個完整的事件序列,對應着手指從按下到擡起這一系列的事件,若是 ViewGroup 攔截了 Down 事件,那麼後續事件都會交給這個 ViewGroup的onTouchEvent。若是 ViewGroup 攔截的不是 Down 事件,那麼會給以前處理這個 Down 事件的 View 發送一個 Action_Cancel 類型的事件,通知子 View 這個後續的事件序列已經被 ViewGroup 接管了,子 View 恢復以前的狀態便可。

這裏舉一個常見的例子:在一個 Recyclerview 鐘有不少的 Button,咱們首先按下了一個 button,而後滑動一段距離再鬆開,這時候 Recyclerview 會跟着滑動,並不會觸發這個 button 的點擊事件。這個例子中,當咱們按下 button 時,這個 button 接收到了 Action_Down 事件,正常狀況下後續的事件序列應該由這個 button處理。但咱們滑動了一段距離,這時 Recyclerview 察覺到這是一個滑動操做,攔截了這個事件序列,走了自身的 onTouchEvent()方法,反映在屏幕上就是列表的滑動。而這時 button 仍然處於按下的狀態,因此在攔截的時候須要發送一個 Action_Cancel 來通知 button 恢復以前狀態。

事件分發最終會走到 View 的 dispatchTouchEvent()中。在 View 的 dispatchTouchEvent() 中沒有 onInterceptTouchEvent(),這也很容易理解,View 不是 ViewGroup,不會包含其餘子 View,因此也不存在攔截不攔截這一說。忽略一些細節,View 的 dispatchTouchEvent()中直接 return 了本身的 onTouchEvent()。若是 onTouchEvent()返回 true 表明事件被處理,不然未處理的事件會向上傳遞,直到有 View 處理了事件或者一直沒有處理,最終到達了 Activity 的 onTouchEvent() 終止。

這裏常常有人問 onTouch 和 onTouchEvent 的區別。首先,這兩個方法都在 View 的 dispatchTouchEvent()中,是這麼一個邏輯:

  • 若是 touchListener 不爲 null,而且這個 View 是 enable 的,並且 onTouch 返回的是 true,知足這三個條件時會直接 return true,不會走 onTouchEvent()方法。
  • 上面只要有一個條件不知足,就會走到 onTouchEvent()方法中。因此 onTouch 的順序是在 onTouchEvent 以前的。

View

繪製流程

視圖繪製的起點在 ViewRootImpl 類的 performTraversals()方法,在這個方法內實際上是按照順序依次調用了 mView.measure()、mView.layout()、mView.draw()

View的繪製流程分爲3步:測量、佈局、繪製,分別對應3個方法 measure、layout、draw。

  • 測量階段。measure 方法會被父 View 調用,在measure 方法中作一些優化和準備工做後會調用 onMeasure 方法進行實際的自我測量。onMeasure方法在View和ViewGroup作的事情是不同的:
  • View。 View 中的 onMeasure 方法會計算本身的尺寸並經過 setMeasureDimension 保存。
  • ViewGroup。 ViewGroup 中的 onMeasure 方法會調用全部子 iew的measure 方法進行自我測量並保存。而後經過子View的尺寸和位置計算出本身的尺寸並保存。
  • 佈局階段。layout 方法會被父View調用,layout 方法會保存父 View 傳進來的尺寸和位置,並調用 onLayout 進行實際的內部佈局。onLayout 在 View 和 ViewGroup 中作的事情也是不同的:
  • View。 由於 View 是沒有子 View 的,因此View的onLayout裏面什麼都不作。
  • ViewGroup。 ViewGroup 中的 onLayout 方法會調用全部子 View 的 layout 方法,把尺寸和位置傳給他們,讓他們完成自個人內部佈局。
  • 繪製階段。draw 方法會作一些調度工做,而後會調用 onDraw 方法進行 View 的自我繪製。draw 方法的調度流程大體是這樣的:
  • **繪製背景。**對應 drawBackground(Canvas)方法。
  • **繪製主體。**對應 onDraw(Canvas)方法。
  • 繪製子View。 對應 dispatchDraw(Canvas)方法。
  • 繪製滑動相關和前景。 對應 onDrawForeground(Canvas)

MeasureSpec

MeasureSpec 是 View 的測量規則。一般父控件要測量子控件的時候,會傳給子控件 widthMeasureSpec 和 heightMeasureSpec 這兩個 int 類型的值。這個值裏面包含兩個信息,SpecModeSpecSize。一個 int 值怎麼會包含兩個信息呢?咱們知道 int 是一個4字節32位的數據,在這兩個 int 類型的數據中,前面高2位是 SpecMode ,後面低30位表明了 SpecSize

mode 有三種類型:UNSPECIFIEDEXACTLYAT_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);
複製代碼複製代碼

具體實現細節,能夠查看源碼

DecorView 的 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 來生成的。

事件衝突解決思路與方案

兩種方案:外部和內部

爲何說android UI操做不是線程安全的

可能在非UI線程中刷新界面的時候,UI線程(或者其餘非UI線程)也在刷新界面,這樣就致使多個界面刷新的操做不能同步,致使線程不安全。

ANR發生的緣由總結和解決辦法

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 源碼中常見的設計模式以及本身在開發中經常使用的設計模式

blog.csdn.net/zxc123e/art…

Android與 js 是如何交互的

在 Android 中,Android 與js 的交互分爲兩個方面:Android 調用 js 裏的方法、js 調用 Android 中的方法。

  • Android調js。Android 調 js 有兩種方法:
  • WebView.loadUrl("javascript:js中的方法名")。 這種方法的優勢是很簡潔,缺點是沒有返回值,若是須要拿到js方法的返回值則須要js調用Android中的方法來拿到這個返回值。
  • WebView.evaluateJavaScript("javascript:js中的方法名",ValueCallback)。 這種方法比 loadUrl 好的是能夠經過 ValueCallback 這個回調拿到 js方法的返回值。缺點是這個方法 Android4.4 纔有,兼容性較差。不過放在 2018 年來講,市面上絕大多數 App 都要求最低版本是 4.4 了,因此我認爲這個兼容性問題不大。
  • js 調 Android。js 調 Android有三種方法:
  • WebView.addJavascriptInterface()。 這是官方解決 js 調用 Android 方法的方案,須要注意的是要在供 js 調用的 Android 方法上加上 @JavascriptInterface 註解,以免安全漏洞。這種方案的缺點是 Android4.2 之前會有安全漏洞,不過在 4.2 之後已經修復了。一樣,在 2018 年來講,兼容性問題不大。
  • 重寫 WebViewClient的shouldOverrideUrlLoading()方法來攔截url, 拿到 url 後進行解析,若是符合雙方的規定,便可調用 Android 方法。優勢是避免了 Android4.2 之前的安全漏洞,缺點也很明顯,沒法直接拿到調用 Android 方法的返回值,只能經過 Android 調用 js 方法來獲取返回值。
  • 重寫 WebChromClient 的 onJsPrompt() 方法,同前一個方式同樣,拿到 url 以後先進行解析,若是符合雙方規定,便可調用Android方法。最後若是須要返回值,經過 result.confirm("Android方法返回值") 便可將 Android 的返回值返回給 js。方法的優勢是沒有漏洞,也沒有兼容性限制,同時還能夠方便的獲取 Android 方法的返回值。其實這裏須要注意的是在 WebChromeClient 中除 了 onJsPrompt 以外還有 onJsAlert 和 onJsConfirm 方法。那麼爲何不選擇另兩個方法呢?緣由在於 onJsAlert 是沒有返回值的,而 onJsConfirm 只有 true 和 false 兩個返回值,同時在前端開發中 prompt 方法基本不會被調用,因此纔會採用 onJsPrompt。

SparseArray 原理

SparseArray,一般來說是 Android 中用來替代 HashMap 的一個數據結構。 準確來說,是用來替換key爲 Integer 類型,value爲Object 類型的HashMap。須要注意的是 SparseArray 僅僅實現了 Cloneable 接口,因此不能用Map來聲明。 從內部結構來說,SparseArray 內部由兩個數組組成,一個是 int[]類型的 mKeys,用來存放全部的鍵;另外一個是 Object[]類型的 mValues,用來存放全部的值。 最多見的是拿 SparseArray 跟HashMap 來作對比,因爲 SparseArray 內部組成是兩個數組,因此佔用內存比 HashMap 要小。咱們都知道,增刪改查等操做都首先須要找到相應的鍵值對,而 SparseArray 內部是經過二分查找來尋址的,效率很明顯要低於 HashMap 的常數級別的時間複雜度。提到二分查找,這裏還須要提一下的是二分查找的前提是數組已是排好序的,沒錯,SparseArray 中就是按照key進行升序排列的。 綜合起來來講,SparseArray 所佔空間優於 HashMap,而效率低於 HashMap,是典型的時間換空間,適合較小容量的存儲。 從源碼角度來講,我認爲須要注意的是 SparseArray的remove()、put()gc()方法。

  • remove() SparseArray 的 remove() 方法並非直接刪除以後再壓縮數組,而是將要刪除的 value 設置爲 DELETE 這個 SparseArray 的靜態屬性,這個 DELETE 其實就是一個 Object 對象,同時會將 SparseArray 中的 mGarbage 這個屬性設置爲 true,這個屬性是便於在合適的時候調用自身的 gc()方法壓縮數組來避免浪費空間。這樣能夠提升效率,若是未來要添加的key等於刪除的key,那麼會將要添加的 value 覆蓋 DELETE。
  • gc()。 SparseArray 中的 gc() 方法跟 JVM 的 GC 其實徹底沒有任何關係。``gc()` 方法的內部實際上就是一個for循環,將 value 不爲 DELETE 的鍵值對往前移動覆蓋value 爲DELETE的鍵值對來實現數組的壓縮,同時將 mGarbage 置爲 false,避免內存的浪費。
  • put()。 put 方法是這麼一個邏輯,若是經過二分查找 在 mKeys 數組中找到了 key,那麼直接覆蓋 value 便可。若是沒有找到,會拿到與數組中與要添加的 key 最接近的 key 索引,若是這個索引對應的 value 爲 DELETE,則直接把新的 value 覆蓋 DELET 便可,在這裏能夠避免數組元素的移動,從而提升了效率。若是 value 不爲 DELETE,會判斷 mGarbage,若是爲 true,則會調用 gc()方法壓縮數組,以後會找到合適的索引,將索引以後的鍵值對後移,插入新的鍵值對,這個過程當中可能會觸發數組的擴容。

圖片加載如何避免 OOM

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

  • 等比縮小長寬。咱們知道 Bitmap 的建立是經過 BitmapFactory 的工廠方法,decodeFile()、decodeStream()、decodeByteArray()、decodeResource()。這些方法中都有一個 Options 類型的參數,這個 Options 是 BitmapFactory 的內部類,存儲着 BItmap 的一些信息。Options 中有一個屬性:inSampleSize。咱們經過修改 inSampleSize 能夠縮小圖片的長寬,從而減小 BItma p 所佔內存。須要注意的是這個 inSampleSize 大小須要是 2 的冪次方,若是小於 1,代碼會強制讓inSampleSize爲1。
  • 減小像素所佔內存。Options 中有一個屬性 inPreferredConfig,默認是 ARGB_8888,表明每一個像素所佔尺寸。咱們能夠經過將之修改成 RGB_565 或者 ARGB_4444 來減小一半內存。

大圖加載

加載高清大圖,好比清明上河圖,首先屏幕是顯示不下的,並且考慮到內存狀況,也不可能一次性所有加載到內存。這時候就須要局部加載了,Android中有一個負責局部加載的類:BitmapRegionDecoder。使用方法很簡單,經過BitmapRegionDecoder.newInstance()建立對象,以後調用decodeRegion(Rect rect, BitmapFactory.Options options)便可。第一個參數rect是要顯示的區域,第二個參數是BitmapFactory中的內部類Options。

Bitmap計算和內存優化

Bitmap內存佔用 ≈ 像素數據總大小 = 圖片寬 × 圖片高× (設備分辨率/資源目錄分辨率)^2 × 每一個像素的字節大小(能夠詳細闡述)

Bitmap內存優化

圖片佔用的內存通常會分爲運行時佔用的運存和存儲時本地開銷(反映在包大小上),這裏咱們只關注運行時佔用內存的優化。

在上一節中,咱們看到對於一張800 * 600 大小的圖片,不加任何處理直接解析到內存中,將近佔用了17.28M的內存大小。想象一下這樣的開銷發生在一個圖片列表中,內存佔用將達到很是誇張的地步。從以前Bitmap佔用內存的計算公式來看,減小內存主要能夠經過如下幾種方式:

  1. 使用低色彩的解析模式,如RGB565,減小單個像素的字節大小
  2. 資源文件合理放置,高分辨率圖片能夠放到高分辨率目錄下
  3. 圖片縮小,減小尺寸

第一種方式,大約能減小一半的內存開銷。Android默認是使用ARGB8888配置來處理色彩,佔用4字節,改用RGB565,將只佔用2字節,代價是顯示的色彩將相對少,適用於對色彩豐富程度要求不高的場景。

第二種方式,和圖片的具體分辨率有關,建議開發中,高分辨率的圖像應該放置到合理的資源目錄下,注意到Android默認放置的資源目錄是對應於160dpi,目前手機屏幕分辨率愈來愈高,此處能節省下來的開銷也是很可觀的。理論上,圖片放置的資源目錄分辨率越高,其佔用內存會越小,可是低分辨率圖片會所以被拉伸,顯示上出現失真。另外一方面,高分辨率圖片也意味着其佔用的本地儲存也變大。

第三種方式,理論上根據適用的環境,是能夠減小十幾倍的內存使用的,它基於這樣一個事實:源圖片尺寸通常都大於目標須要顯示的尺寸,所以能夠經過縮放的方式,來減小顯示時的圖片寬高,從而大大減小佔用的內存。

AsyncTask總結

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樹的底層,並減小嵌套

RecyclerView與ListView 對比:緩存機制

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 的區別?https 是如何工做的?


http 是超文本傳輸協議,而 https 能夠簡單理解爲安全的 http 協議。https 經過在 http 協議下添加了一層 ssl 協議對數據進行加密從而保證了安全。https 的做用主要有兩點:創建安全的信息傳輸通道,保證數據傳輸安全;確認網站的真實性。

http 與 https 的區別主要以下:

  • https 須要到 CA 申請證書,不多免費,於是須要必定的費用
  • http 是明文傳輸,安全性低;而 https 在 http 的基礎上經過 ssl 加密,安全性高
  • 兩者的默認端口不同,http 使用的默認端口是80;https使用的默認端口是 443

https 的工做流程

提到 https 的話首先要說到加密算法,加密算法分爲兩類:對稱加密和非對稱加密。

  • 對稱加密: 加密和解密用的都是相同的祕鑰,優勢是速度快,缺點是安全性低。常見的對稱加密算法有 DES、AES 等等。
  • 非對稱加密: 非對稱加密有一個祕鑰對,分爲公鑰和私鑰。通常來講,私鑰本身持有,公鑰能夠公開給對方,優勢是安全性比對稱加密高,缺點是數據傳輸效率比對稱加密低。採用公鑰加密的信息只有對應的私鑰能夠解密。常見的非對稱加密包括RSA等。

在正式的使用場景中通常都是對稱加密和非對稱加密結合使用,使用非對稱加密完成祕鑰的傳遞,而後使用對稱祕鑰進行數據加密和解密。兩者結合既保證了安全性,又提升了數據傳輸效率。

https 的具體流程以下:

  1. 客戶端(一般是瀏覽器)先向服務器發出加密通訊的請求
  • 支持的協議版本,好比 TLS 1.0版
  • 一個客戶端生成的隨機數 random1,稍後用於生成"對話密鑰"
  • 支持的加密方法,好比 RSA 公鑰加密
  • 支持的壓縮方法
  1. 服務器收到請求,而後響應
  • 確認使用的加密通訊協議版本,好比 TLS 1.0版本。若是瀏覽器與服務器支持的版本不一致,服務器關閉加密通訊
  • 一個服務器生成的隨機數 random2,稍後用於生成"對話密鑰"
  • 確認使用的加密方法,好比 RSA 公鑰加密
  • 服務器證書
  1. 客戶端收到證書以後會首先會進行驗證
  • 首先驗證證書的安全性
  • 驗證經過以後,客戶端會生成一個隨機數 pre-master secret,而後使用證書中的公鑰進行加密,而後傳遞給服務器端
  1. 服務器收到使用公鑰加密的內容,在服務器端使用私鑰解密以後得到隨機數 pre-master secret,而後根據 radom一、radom二、pre-master secret 經過必定的算法得出一個對稱加密的祕鑰,做爲後面交互過程當中使用對稱祕鑰。同時客戶端也會使用 radom一、radom二、pre-master secret,和一樣的算法生成對稱祕鑰。
  2. 而後再後續的交互中就使用上一步生成的對稱祕鑰對傳輸的內容進行加密和解密。

http頭部的字段以及含義

  • Accept : 瀏覽器(或者其餘基於HTTP的客戶端程序)能夠接收的內容類型(Content-types),例如 Accept: text/plain
  • Accept-Charset:瀏覽器能識別的字符集,例如 Accept-Charset: utf-8
  • Accept-Encoding:瀏覽器能夠處理的編碼方式,注意這裏的編碼方式有別於字符集,這裏的編碼方式一般指gzip,deflate等。例如 Accept-Encoding: gzip, deflate
  • Accept-Language:瀏覽器接收的語言,其實也就是用戶在什麼語言地區,例如簡體中文的就是 Accept-Language: zh-CN
  • Authorization:在HTTP中,服務器能夠對一些資源進行認證保護,若是你要訪問這些資源,就要提供用戶名和密碼,這個用戶名和密碼就是在Authorization頭中附帶的,格式是「username:password」字符串的base64編碼
  • Cache-Control:這個指令在request和response中都有,用來指示緩存系統(服務器上的,或者瀏覽器上的)應該怎樣處理緩存,由於這個頭域比較重要,特別是但願使用緩 存改善性能的時候
  • Connection:告訴服務器這個user agent(一般就是瀏覽器)想要使用怎樣的鏈接方式。值有keep-alive和close。http1.1默認是keep-alive。keep-alive就是瀏覽器和服務器 的通訊鏈接會被持續保存,不會立刻關閉,而close就會在response後立刻關閉。但這裏要注意一點,咱們說HTTP是無狀態的,跟這個是否keep-alive沒有關係,不要認爲keep-alive是對HTTP無狀態的特性的改進。
  • Cookie:瀏覽器向服務器發送請求時發送cookie,或者服務器向瀏覽器附加cookie,就是將cookie附近在這裏的。例如:Cookie:user=admin
  • Content-Length:一個請求的請求體的內存長度,單位爲字節(byte)。請求體是指在HTTP頭結束後,兩個CR-LF字符組以後的內容,常見的有POST提交的表單數據,這個Content-Length並不包含請求行和HTTP頭的數據長度。
  • Content-MD5:使用base64進行了編碼的請求體的MD5校驗和。例如:Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==
  • Content-Type:請求體中的內容的mime類型。一般只會用在POST和PUT方法的請求中。例如:Content-Type: application/x-www-form-urlencoded
  • Date:發送請求時的GMT時間。例如:Date: Tue, 15 Nov 1994 08:12:31 GMT
  • From:發送這個請求的用戶的email地址。例如:From: user@example.com
  • Host:被服務器的域名或IP地址,若是不是通用端口,還包含該端口號,例如:Host: www.some.com:182
  • Proxy-Authorization:鏈接到某個代理時使用的身份認證信息,跟Authorization頭差很少。例如:Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
  • User-Agent:一般就是用戶的瀏覽器相關信息。例如:User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/12.0
  • Warning:記錄一些警告信息。

Dart

1. Dart 當中的 「..」表示什麼意思?

Dart 當中的 「..」意思是 「級聯操做符」,爲了方便配置而使用。「..」和「.」不一樣的是 調用「..」後返回的至關因而 this,而「.」返回的則是該方法返回的值 。

變量聲明總結

  1. var: 若是沒有初始值,能夠變成任何類型
  2. dynamic:動態任意類型,編譯階段不檢查類型
  3. Object 動態任意類型,編譯階段檢查類型區別:
  1. 惟一區別 var 若是有初始值,類型被是鎖定

final 和 const 總結

共同點

  • 聲明的類型可省略
  • 初始化後不能再賦值
  • 不能和 var 同時使用

區別(須要注意的地方)

  • 類級別常量,使用 static ,const 。
  • const 可以使用其餘 const 常量的值來初始化其值
  • const 可以使用其餘 const 常量的值來初始化其值
  • 能夠更改非 final、非 const 變量的值,即便曾經具備 const 值
  • const 致使的不可變性是可傳遞的
  • 相同的 const 常量不會在內存中重複建立
  • const 須要是編譯時常量

操做符

後綴操做

  • 條件成員訪問 和 . 相似,可是左邊的操做對象不能爲 null,例如 foo?.bar 若是 foo 爲 null 則返回 null,不然返回 bar 成員

取商操做符

  • 被除數 ÷ 除數 = 商 ... 餘數,A ~/ B = C,這個C就是商。至關於Java裏的 /

類型斷定操做符

  • as、is、is! 在運行時斷定對象類型

條件表達式

  • 三目運算符 condition ? expr1 : expr2

流程控制語句

if else

for , forEach , for-in


2. Dart 的做用域

Dart 沒有 「public」「private」等關鍵字,默認就是公開的,私有變量使用 下劃線 _開頭。「_」的限制範圍並非類訪問級別的,而是庫訪問級別。

3. Dart 是否是單線程模型?是如何運行的?

Dart 是單線程模型,運行的的流程以下圖。

image.png

image.png

簡單來講,Dart 在單線程中是以消息循環機制來運行的,包含兩個任務隊列,一個是「微任務隊列」 microtask queue,另外一個叫作「事件隊列」 event queue。

當Flutter應用啓動後,消息循環機制便啓動了。首先會按照先進先出的順序逐個執行微任務隊列中的任務,當全部微任務隊列執行完後便開始執行事件隊列中的任務,事件任務執行完畢後再去執行微任務,如此循環往復,生生不息。

4. Dart 是如何實現多任務並行的?

Dart 是單線程的,不存在多線程,那如何進行多任務並行的呢?其實,Dart的多線程和前端的多線程有不少的類似之處。Flutter的多線程主要依賴Dart的併發編程、異步和事件驅動機制。


image.png

簡單的說,在Dart中,一個Isolate對象其實就是一個isolate執行環境的引用,通常來講咱們都是經過當前的isolate去控制其餘的isolate完成彼此之間的交互,而當咱們想要建立一個新的Isolate能夠使用Isolate.spawn方法獲取返回的一個新的isolate對象,兩個isolate之間使用SendPort相互發送消息,而isolate中也存在了一個與之對應的ReceivePort接受消息用來處理,可是咱們須要注意的是,ReceivePort和SendPort在每一個isolate都有一對,只有同一個isolate中的ReceivePort才能接受到當前類的SendPort發送的消息而且處理。


future、async 和 await

Future 是異步編程的解決方案,Future 是基於觀察者模式的,它有 3 種狀態:pending(進行中)、fulfilled(已成功)和 rejected(已失敗)能夠用 then 方法指定成功狀態的回調函數 then 方法還能夠接受一個可選命名參數,參數的名稱是 onError,即失敗狀態的回調函數 Future 實例的函數中拋出了異常,被 onError 回調函數捕獲到,而且能夠看出 then 方法返回的仍是一個 Future 對象,因此其實咱們還能夠利用 Future 對象的 cathError 進行鏈式調用從而捕獲異常

在Dart1.9中加入了asyncawait關鍵字,有了這兩個關鍵字,咱們能夠更簡潔的編寫異步代碼,而不須要調用Future相關的API

async 關鍵字做爲方法聲明的後綴時,具備以下意義

  • 被修飾的方法會將一個 Future 對象做爲返回值
  • 該方法會同步執行其中的方法的代碼直到第一個 await 關鍵字,而後它暫停該方法其餘部分的執行;
  • 一旦由 await 關鍵字引用的 Future 任務執行完成,await的下一行代碼將當即執行。

async 不是並行執行,它是遵循Dart 事件循環規則來執行的,它僅僅是一個語法糖,簡化Future API的使用。

Dart異步編程中的 Stream數據流?

在Dart中,Stream 和 Future 同樣,都是用來處理異步編程的工具。它們的區別在於,Stream 能夠接收多個異步結果,而Future 只有一個。

Stream 的建立能夠使用 Stream.fromFuture,也能夠使用 StreamController 來建立和控制。還有一個注意點是:普通的 Stream 只能夠有一個訂閱者,若是想要多訂閱的話,要使用 asBroadcastStream()。

Stream 有哪兩種訂閱模式?分別是怎麼調用的

Stream有兩種訂閱模式:單訂閱(single) 和 多訂閱(broadcast)。單訂閱就是隻能有一個訂閱者,而廣播是能夠有多個訂閱者。這就有點相似於消息服務(Message Service)的處理模式。單訂閱相似於點對點,在訂閱者出現以前會持有數據,在訂閱者出現以後就才轉交給它。而廣播相似於發佈訂閱模式,能夠同時有多個訂閱者,當有數據時就會傳遞給全部的訂閱者,而無論當前是否已有訂閱者存在。

Stream 默認處於單訂閱模式,因此同一個 stream 上的 listen 和其它大多數方法只能調用一次,調用第二次就會報錯。但 Stream 能夠經過 transform() 方法(返回另外一個 Stream)進行連續調用。經過 Stream.asBroadcastStream() 能夠將一個單訂閱模式的 Stream 轉換成一個多訂閱模式的 Stream,isBroadcast 屬性能夠判斷當前 Stream 所處的模式。

mixin機制

mixin 是Dart 2.1 加入的特性,之前版本一般使用abstract class代替。簡單來講,mixin是爲了解決繼承方面的問題而引入的機制,Dart爲了支持多重繼承,引入了mixin關鍵字,它最大的特殊處在於:mixin定義的類不能有構造方法,這樣能夠避免繼承多個類而產生的父類構造方法衝突。

mixins的對象是類,mixins毫不是繼承,也不是接口,而是一種全新的特性,能夠mixins多個類,mixins的使用須要知足必定條件。


JIT 與 AOT

藉助於先進的工具鏈和編譯器,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

簡單介紹下Flutter框架,以及它的優缺點?

Flutter是Google推出的一套開源跨平臺UI框架,能夠快速地在Android、iOS和Web平臺上構建高質量的原生用戶界面。同時,Flutter仍是Google新研發的Fuchsia操做系統的默認開發套件。在全世界,Flutter正在被愈來愈多的開發者和組織使用,而且Flutter是徹底免費、開源的。Flutter採用現代響應式框架構建,其中心思想是使用組件來構建應用的UI。當組件的狀態發生改變時,組件會重構它的描述,Flutter會對比以前的描述,以肯定底層渲染樹從當前狀態轉換到下一個狀態所須要的最小更改。

優勢

  • 熱重載(Hot Reload),利用Android Studio直接一個ctrl+s就能夠保存並重載,模擬器立馬就能夠看見效果,相比原生冗長的編譯過程強不少;
  • 一切皆爲Widget的理念,對於Flutter來講,手機應用裏的全部東西都是Widget,經過可組合的空間集合、豐富的動畫庫以及分層課擴展的架構實現了富有感染力的靈活界面設計;
  • 藉助可移植的GPU加速的渲染引擎以及高性能本地代碼運行時以達到跨平臺設備的高質量用戶體驗。簡單來講就是:最終結果就是利用Flutter構建的應用在運行效率上會和原生應用差很少。

缺點

  • 不支持熱更新;
  • 三方庫有限,須要本身造輪子;
  • Dart語言編寫,增長了學習難度,而且學習了Dart以後無其餘用處,相比JS和Java來講。

介紹下Flutter的理念架構

其實也就是下面這張圖。

image.png

由上圖可知,Flutter框架自下而上分爲Embedder、Engine和Framework三層。其中,Embedder是操做系統適配層,實現了渲染 Surface設置,線程設置,以及平臺插件等平臺相關特性的適配;Engine層負責圖形繪製、文字排版和提供Dart運行時,Engine層具備獨立虛擬機,正是因爲它的存在,Flutter程序才能運行在不一樣的平臺上,實現跨平臺運行;Framework層則是使用Dart編寫的一套基礎視圖庫,包含了動畫、圖形繪製和手勢識別等功能,是使用頻率最高的一層。

Flutter的FrameWork層和Engine層,以及它們的做用

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小不少。


介紹下Widget、State、Context 概念

  • Widget:在Flutter中,幾乎全部東西都是Widget。將一個Widget想象爲一個可視化的組件(或與應用可視化方面交互的組件),當你須要構建與佈局直接或間接相關的任何內容時,你正在使用Widget。
  • Widget樹:Widget以樹結構進行組織。包含其餘Widget的widget被稱爲父Widget(或widget容器)。包含在父widget中的widget被稱爲子Widget。
  • Context:僅僅是已建立的全部Widget樹結構中的某個Widget的位置引用。簡而言之,將context做爲widget樹的一部分,其中context所對應的widget被添加到此樹中。一個context只從屬於一個widget,它和widget同樣是連接在一塊兒的,而且會造成一個context樹。
  • State:定義了StatefulWidget實例的行爲,它包含了用於」交互/干預「Widget信息的行爲和佈局。應用於State的任何更改都會強制重建Widget。

StatelessWidget和StatefulWidget兩種狀態組件類

  • StatelessWidget: 一旦建立就不關心任何變化,在下次構建以前都不會改變。它們除了依賴於自身的配置信息(在父節點構建時提供)外再也不依賴於任何其餘信息。好比典型的Text、Row、Column、Container等,都是StatelessWidget。它的生命週期至關簡單:初始化、經過build()渲染。
  • StatefulWidget: 在生命週期內,該類Widget所持有的數據可能會發生變化,這樣的數據被稱爲State,這些擁有動態內部數據的Widget被稱爲StatefulWidget。好比複選框、Button等。State會與Context相關聯,而且此關聯是永久性的,State對象將永遠不會改變其Context,即便能夠在樹結構周圍移動,也仍將與該context相關聯。當state與context關聯時,state被視爲已掛載。StatefulWidget由兩部分組成,在初始化時必需要在createState()時初始化一個與之相關的State對象。

StatefulWidget 的生命週期


Flutter的Widget分爲StatelessWidget和StatefulWidget兩種。其中,StatelessWidget是無狀態的,StatefulWidget是有狀態的,所以實際使用時,更多的是StatefulWidget。StatefulWidget的生命週期以下圖


image.png

  • initState():Widget 初始化當前 State,在當前方法中是不能獲取到 Context 的,如想獲取,能夠試試 Future.delayed()
  • didChangeDependencies():在 initState() 後調用,State對象依賴關係發生變化的時候也會調用。
  • deactivate():當 State 被暫時從視圖樹中移除時會調用這個方法,頁面切換時也會調用該方法,和Android裏的 onPause 差很少。
  • dispose():Widget 銷燬時調用。
  • didUpdateWidget:Widget 狀態發生變化的時候調用。


Widgets、RenderObjects 和 Elements的關係

首先看一下這幾個對象的含義及做用。

  • Widget :僅用於存儲渲染所須要的信息。
  • RenderObject :負責管理佈局、繪製等操做。
  • Element :纔是這顆巨大的控件樹上的實體。

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的繪製流程

image.png

Flutter只關心向 GPU提供視圖數據,GPU的 VSync信號同步到 UI線程,UI線程使用 Dart來構建抽象的視圖結構,這份數據結構在 GPU線程進行圖層合成,視圖數據提供給 Skia引擎渲染爲 GPU數據,這些數據經過 OpenGL或者 Vulkan提供給 GPU。

Flutter的線程管理模型

默認狀況下,Flutter Engine層會建立一個Isolate,而且Dart代碼默認就運行在這個主Isolate上。必要時能夠使用spawnUri和spawn兩種方式來建立新的Isolate,在Flutter中,新建立的Isolate由Flutter進行統一的管理。

事實上,Flutter Engine本身不建立和管理線程,Flutter Engine線程的建立和管理是Embeder負責的,Embeder指的是將引擎移植到平臺的中間層代碼,Flutter Engine層的架構示意圖以下圖所示。

image.png

在Flutter的架構中,Embeder提供四個Task Runner,分別是Platform Task Runner、UI Task Runner Thread、GPU Task Runner和IO Task Runner,每一個Task Runner負責不一樣的任務,Flutter Engine不在意Task Runner運行在哪一個線程,可是它須要線程在整個生命週期裏面保持穩定

Flutter 是如何與原生Android、iOS進行通訊的?

Flutter 經過 PlatformChannel 與原生進行交互,其中 PlatformChannel 分爲三種:

  • BasicMessageChannel :用於傳遞字符串和半結構化的信息。
  • MethodChannel :用於傳遞方法調用(method invocation)。
  • EventChannel : 用於數據流(event streams)的通訊。

同時 Platform Channel 並不是是線程安全的 ,更多詳細可查閱閒魚技術的 《深刻理解Flutter Platform Channel》

簡述Flutter 的熱重載

Flutter 的熱重載是基於 JIT 編譯模式的代碼增量同步。因爲 JIT 屬於動態編譯,可以將 Dart 代碼編譯成生成中間代碼,讓 Dart VM 在運行時解釋執行,所以能夠經過動態更新中間代碼實現增量同步。

熱重載的流程能夠分爲 5 步,包括:掃描工程改動、增量編譯、推送更新、代碼合併、Widget 重建。Flutter 在接收到代碼變動後,並不會讓 App 從新啓動執行,而只會觸發 Widget 樹的從新繪製,所以能夠保持改動前的狀態,大大縮短了從代碼修改到看到修改產生的變化之間所須要的時間。

另外一方面,因爲涉及到狀態的保存與恢復,涉及狀態兼容與狀態初始化的場景,熱重載是沒法支持的,如改動先後 Widget 狀態沒法兼容、全局變量與靜態屬性的更改、main 方法裏的更改、initState 方法裏的更改、枚舉和泛型的更改等。

能夠發現,熱重載提升了調試 UI 的效率,很是適合寫界面樣式這樣須要反覆查看修改效果的場景。但因爲其狀態保存的機制所限,熱重載自己也有一些沒法支持的邊界。


Flutter 是怎麼運轉的?

與用於構建移動應用程序的其餘大多數框架不一樣,Flutter 是重寫了一整套包括底層渲染邏輯和上層開發語言的完整解決方案。這樣不只能夠保證視圖渲染在 Android 和 iOS 上的高度一致性(即高保真),在代碼執行效率和渲染性能上也能夠媲美原生 App 的體驗(即高性能)。這,就是

Flutter 和其餘跨平臺方案的本質區別:

React Native 之類的框架,只是經過 JavaScript 虛擬機擴展調用系統組件,由 Android 和 iOS 系統進行組件的渲染;

Flutter 則是本身完成了組件渲染的閉環。那麼,Flutter 是怎麼完成組件渲染的呢?這須要從圖像顯示的基本原理提及。在計算機系統中,圖像的顯示須要 CPU、GPU 和顯示器一塊兒配合完成:CPU 負責圖像數據計算,GPU 負責圖像數據渲染,而顯示器則負責最終圖像顯示。CPU 把計算好的、須要顯示的內容交給 GPU,由 GPU 完成渲染後放入幀緩衝區,隨後視頻控制器根據垂直同步信號(VSync)以每秒 60 次的速度,從幀緩衝區讀取幀數據交由顯示器完成圖像顯示。操做系統在呈現圖像時遵循了這種機制,而 Flutter 做爲跨平臺開發框架也採用了這種底層方案。下面有一張更爲詳盡的示意圖來解釋 Flutter 的繪製原理。

image.png Flutter 繪製原理能夠看到,Flutter 關注如何儘量快地在兩個硬件時鐘的 VSync 信號之間計算併合成視圖數據,而後經過 Skia 交給 GPU 渲染:UI 線程使用 Dart 來構建視圖結構數據,這些數據會在 GPU 線程進行圖層合成,隨後交給 Skia 引擎加工成 GPU 數據,而這些數據會經過 OpenGL 最終提供給 GPU 渲染。

架構

組件化

組件化又叫模塊化,即基於可重用的目的,將一個大型軟件系統(App)按照關注點分離的方式,拆分紅多個獨立的組件或模塊。每一個獨立的組件都是一個單獨的系統,能夠單獨維護、升級甚至直接替換,也能夠依賴於別的獨立組件,只要組件提供的功能不發生變化,就不會影響其餘組件和軟件系統的總體功能。


image.png

能夠看到,組件化的中心思想是將獨立的功能進行拆分,而在拆分粒度上,組件化的約束則較爲鬆散。一個獨立的組件能夠是一個軟件包(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 就會陷入不穩定的狀態,即所謂牽一髮而動全身


image.png

平臺化是組件化的升級,即在組件化的基礎上,對它們提供的功能進行分類,統一分層劃分,增長依賴治理的概念。爲了對這些功能單元在概念上進行更爲統一的分類,咱們按照四象限分析法,把應用程序的組件按照業務和 UI 分解爲 4 個維度,來分析組件能夠分爲哪幾類。


image.png

能夠看出,通過業務與 UI 的分解以後,這些組件能夠分爲 4 類:

具有 UI 屬性的獨立業務模塊;

不具有 UI 屬性的基礎業務功能;

不具有業務屬性的 UI 控件

不具有業務屬性的基礎功能


按照自身定義,這 4 類組件其實隱含着分層依賴的關係。好比,處於業務模塊中的首頁,依賴位於基礎業務模塊中的帳號功能;再好比,位於 UI 控件模塊中的輪播卡片,依賴位於基礎功能模塊中的存儲管理等功能。咱們將它們按照依賴的前後順序從上到下進行劃分,就是一個完整的 App 了


image.png

能夠看到,平臺化與組件化最大的差別在於增長了分層的概念,每一層的功能均基於同層和下層的功能之上,這使得各個組件之間既保持了獨立性,同時也具備必定的彈性,在不越界的狀況下按照功能劃分各司其職。


與組件化更關注組件的獨立性相比,平臺化更關注的是組件之間關係的合理性,而這也是在設計平臺化架構時須要重點考慮的單向依賴原則。


所謂單向依賴原則,指的是組件依賴的順序應該按照應用架構的層數從上到下依賴,不要出現下層模塊依賴上層模塊這樣循環依賴的現象。這樣能夠最大限度地避免複雜的耦合,減小組件化時的困難。若是咱們每一個組件都只是單向依賴其餘組件,各個組件之間的關係都是清晰的,代碼解耦也就會變得很是輕鬆了。平臺化強調依賴的順序性,除了不容許出現下層組件依賴上層組件的狀況,跨層組件和同層組件之間的依賴關係也應當嚴格控制,由於這樣的依賴關係每每會帶來架構設計上的混亂。


若是下層組件確實須要調用上層組件的代碼怎麼辦?


這時,咱們能夠採用增長中間層的方式,好比 Event Bus、Provider 或 Router,以中間層轉發的形式實現信息同步。好比,位於第 4 層的網絡引擎中,會針對特定的錯誤碼跳轉到位於第 1 層的統一錯誤頁,這時咱們就能夠利用 Router 提供的命名路由跳轉,在不感知錯誤頁的實現狀況下來完成。又好比,位於第 2 層的帳號組件中,會在用戶登入登出時主動刷新位於第 1 層的首頁和個人頁面,這時咱們就能夠利用 Event Bus 來觸發帳號切換事件,在不須要獲取頁面實例的狀況下通知它們更新界面。


總結

簡歷不要寫本身不會的

換工做要給本身留足夠的時間,否則很被動,由於不少公司流程太長,兩輪面試之間有可能會間隔一個星期

E5795B0F588652A5CBE43EB3B8473907.jpg

Android-Flutter面經二--算法

此次面試有不少想和你們分享的好比簡歷(很重要)、項目問題、回答技巧等,看後續你們感興趣

相關文章
相關標籤/搜索