Android面試題《思考與解答》11月刊

jimuzz發表於2020-12-02

又來更新啦,Android面試題《思考與解答》11月刊奉上。

說說View/ViewGroup的繪製流程

View的繪製流程是從ViewRootperformTraversals開始的,它經過measure,layout,draw三個過程最終將View繪製出來。
performTraversals會依次呼叫performMeasure,performLayout,performDraw三個方法,他們會依次呼叫measure,layout,draw方法,然後又呼叫了onMeasure,onLayout,dispatchDraw

  • measure :

對於自定義的單一view的測量,只需要根據父 view 傳遞的MeasureSpec進行計算大小。

對於ViewGroup的測量,一般要重寫onMeasure方法,在onMeasure方法中,父容器會對所有的子View進行Measure,子元素又會作為父容器,重複對它自己的子元素進行Measure,這樣Measure過程就從DecorView一級一級傳遞下去了,也就是要遍歷所有子View的的尺寸,最終得出出總的viewGroup的尺寸。Layout和Draw方法也是如此。

  • layout :根據 measure 子 View 所得到的佈局大小和佈局引數,將子View放在合適的位置上。

對於自定義的單一view,計算本身的位置即可。

對於ViewGroup來說,需要重寫onlayout方法。除了計算自己View的位置,還需要確定每一個子View在父容器的位置以及子view的寬高(getMeasuredWidth和getMeasuredHeight),最後呼叫所有子view的layout方法來設定子view的位置。

  • draw :把 View 物件繪製到螢幕上。

draw()會依次呼叫四個方法:

1)drawBackground(),根據在 layout 過程中獲取的 View 的位置引數,來設定背景的邊界。
2)onDraw(),繪製View本身的內容,一般自定義單一view會重寫這個方法,實現一些繪製邏輯。
3) dispatchDraw(),繪製子View
4) onDrawScrollBars(canvas),繪製裝飾,如 滾動指示器、滾動條、和前景

說說你理解的MeasureSpec

MeasureSpec是由父View的MeasureSpec和子View的LayoutParams通過簡單的計算得出一個針對子View的測量要求,這個測量要求就是MeasureSpec。

  • 首先,MeasureSpec是一個大小跟模式的組合值,MeasureSpec中的值是一個整型(32位)將size和mode打包成一個Int型,其中高兩位是mode,後面30位存的是size
    // 獲取測量模式
    int specMode = MeasureSpec.getMode(measureSpec)

    // 獲取測量大小
    int specSize = MeasureSpec.getSize(measureSpec)

    // 通過Mode 和 Size 生成新的SpecMode
    int measureSpec=MeasureSpec.makeMeasureSpec(size, mode);
  • 其次,每個子View的MeasureSpec值根據子View的佈局引數和父容器的MeasureSpec值計算得來的,所以就有一個父佈局測量模式,子檢視佈局引數,以及子view本身的MeasureSpec關係圖:

其實也就是getChildMeasureSpec方法的原始碼邏輯,會根據子View的佈局引數和父容器的MeasureSpec計算出來單個子view的MeasureSpec。

  • 最後是實際應用時:

對於自定義的單一view,一般可以不處理onMeasure方法,如果要對寬高進行自定義,就重寫onMeasure方法,並將算好的寬高通過setMeasuredDimension方法傳進去。
對於自定義的ViewGroup,一般需要重寫onMeasure方法,並且呼叫measureChildren方法遍歷所有子View並進行測量(measureChild方法是測量具體某一個view的寬高),然後可以通過getMeasuredWidth/getMeasuredHeight獲取寬高,最後通過setMeasuredDimension方法儲存本身的總寬高。

Scroller是怎麼實現View的彈性滑動?

  • MotionEvent.ACTION_UP事件觸發時呼叫startScroll()方法,該方法並沒有進行實際的滑動操作,而是記錄滑動相關量(滑動距離、滑動時間)
  • 接著呼叫invalidate/postInvalidate()方法,請求View重繪,導致View.draw方法被執行
  • 當View重繪後會在draw方法中呼叫computeScroll方法,而computeScroll又會去向Scroller獲取當前的scrollX和scrollY;然後通過scrollTo方法實現滑動;接著又呼叫postInvalidate方法來進行第二次重繪,和之前流程一樣,如此反覆導致View不斷進行小幅度的滑動,而多次的小幅度滑動就組成了彈性滑動,直到整個滑動過成結束。

mScroller = new Scroller(context);


@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                // 滾動開始時X的座標,滾動開始時Y的座標,橫向滾動的距離,縱向滾動的距離
                mScroller.startScroll(getScrollX(), 0, dx, 0);
                invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }

@Override
    public void computeScroll() {
        // 重寫computeScroll()方法,並在其內部完成平滑滾動的邏輯
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }

OKHttp有哪些攔截器,分別起什麼作用

OKHTTP的攔截器是把所有的攔截器放到一個list裡,然後每次依次執行攔截器,並且在每個攔截器分成三部分:

  • 預處理攔截器內容
  • 通過proceed方法把請求交給下一個攔截器
  • 下一個攔截器處理完成並返回,後續處理工作。

這樣依次下去就形成了一個鏈式呼叫,看看原始碼,具體有哪些攔截器:

  Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }

根據原始碼可知,一共七個攔截器:

  • addInterceptor(Interceptor),這是由開發者設定的,會按照開發者的要求,在所有的攔截器處理之前進行最早的攔截處理,比如一些公共引數,Header都可以在這裡新增。
  • RetryAndFollowUpInterceptor,這裡會對連線做一些初始化工作,以及請求失敗的充實工作,重定向的後續請求工作。跟他的名字一樣,就是做重試工作還有一些連線跟蹤工作。
  • BridgeInterceptor,這裡會為使用者構建一個能夠進行網路訪問的請求,同時後續工作將網路請求回來的響應Response轉化為使用者可用的Response,比如新增檔案型別,content-length計算新增,gzip解包。
  • CacheInterceptor,這裡主要是處理cache相關處理,會根據OkHttpClient物件的配置以及快取策略對請求值進行快取,而且如果本地有了可⽤的Cache,就可以在沒有網路互動的情況下就返回快取結果。
  • ConnectInterceptor,這裡主要就是負責建立連線了,會建立TCP連線或者TLS連線,以及負責編碼解碼的HttpCodec
  • networkInterceptors,這裡也是開發者自己設定的,所以本質上和第一個攔截器差不多,但是由於位置不同,所以用處也不同。這個位置新增的攔截器可以看到請求和響應的資料了,所以可以做一些網路除錯。
  • CallServerInterceptor,這裡就是進行網路資料的請求和響應了,也就是實際的網路I/O操作,通過socket讀寫資料。

OkHttp怎麼實現連線池

  • 為什麼需要連線池?

頻繁的進行建立Sokcet連線和斷開Socket是非常消耗網路資源和浪費時間的,所以HTTP中的keepalive連線對於降低延遲和提升速度有非常重要的作用。keepalive機制是什麼呢?也就是可以在一次TCP連線中可以持續傳送多份資料而不會斷開連線。所以連線的多次使用,也就是複用就變得格外重要了,而複用連線就需要對連線進行管理,於是就有了連線池的概念。

OkHttp中使用ConectionPool實現連線池,預設支援5個併發KeepAlive,預設鏈路生命為5分鐘。

  • 怎麼實現的?

1)首先,ConectionPool中維護了一個雙端佇列Deque,也就是兩端都可以進出的佇列,用來儲存連線。
2)然後在ConnectInterceptor,也就是負責建立連線的攔截器中,首先會找可用連線,也就是從連線池中去獲取連線,具體的就是會呼叫到ConectionPool的get方法。

RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    return null;
  }

也就是遍歷了雙端佇列,如果連線有效,就會呼叫acquire方法計數並返回這個連線。

3)如果沒找到可用連線,就會建立新連線,並會把這個建立的連線加入到雙端佇列中,同時開始執行執行緒池中的執行緒,其實就是呼叫了ConectionPool的put方法。

public final class ConnectionPool {
    void put(RealConnection connection) {
        if (!cleanupRunning) {
        	//沒有連線的時候呼叫
            cleanupRunning = true;
            executor.execute(cleanupRunnable);
        }
        connections.add(connection);
    }
}

3)其實這個執行緒池中只有一個執行緒,是用來清理連線的,也就是上述的cleanupRunnable

private final Runnable cleanupRunnable = new Runnable() {
        @Override
        public void run() {
            while (true) {
                //執行清理,並返回下次需要清理的時間。
                long waitNanos = cleanup(System.nanoTime());
                if (waitNanos == -1) return;
                if (waitNanos > 0) {
                    long waitMillis = waitNanos / 1000000L;
                    waitNanos -= (waitMillis * 1000000L);
                    synchronized (ConnectionPool.this) {
                        //在timeout時間內釋放鎖
                        try {
                            ConnectionPool.this.wait(waitMillis, (int) waitNanos);
                        } catch (InterruptedException ignored) {
                        }
                    }
                }
            }
        }
    };

這個runnable會不停的呼叫cleanup方法清理執行緒池,並返回下一次清理的時間間隔,然後進入wait等待。

怎麼清理的呢?看看原始碼:

long cleanup(long now) {
    synchronized (this) {
      //遍歷連線
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        //檢查連線是否是空閒狀態,
        //不是,則inUseConnectionCount + 1
        //是 ,則idleConnectionCount + 1
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

        // If the connection is ready to be evicted, we're done.
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

      //如果超過keepAliveDurationNs或maxIdleConnections,
      //從雙端佇列connections中移除
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {      
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {      //如果空閒連線次數>0,返回將要到期的時間
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // 連線依然在使用中,返回保持連線的週期5分鐘
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
  }

也就是當如果空閒連線maxIdleConnections超過5個或者keepalive時間大於5分鐘,則將該連線清理掉。

4)這裡有個問題,怎樣屬於空閒連線?

其實就是有關剛才說到的一個方法acquire計數方法:

  public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();

    this.connection = connection;
    this.reportedAcquired = reportedAcquired;
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
  }

RealConnection中,有一個StreamAllocation虛引用列表allocations。每建立一個連線,就會把連線對應的StreamAllocationReference新增進該列表中,如果連線關閉以後就將該物件移除。

5)連線池的工作就這麼多,並不負責,主要就是管理雙端佇列Deque<RealConnection>,可以用的連線就直接用,然後定期清理連線,同時通過對StreamAllocation的引用計數實現自動回收。

OkHttp裡面用到了什麼設計模式

  • 責任鏈模式

這個不要太明顯,可以說是okhttp的精髓所在了,主要體現就是攔截器的使用,具體程式碼可以看看上述的攔截器介紹。

  • 建造者模式

在Okhttp中,建造者模式也是用的挺多的,主要用處是將物件的建立與表示相分離,用Builder組裝各項配置。
比如Request:

public class Request {
  public static class Builder {
    @Nullable HttpUrl url;
    String method;
    Headers.Builder headers;
    @Nullable RequestBody body;
    public Request build() {
      return new Request(this);
    }
  }
}
  • 工廠模式

工廠模式和建造者模式類似,區別就在於工廠模式側重點在於物件的生成過程,而建造者模式主要是側重物件的各個引數配置。
例子有CacheInterceptor攔截器中又個CacheStrategy物件:

    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

    public Factory(long nowMillis, Request request, Response cacheResponse) {
      this.nowMillis = nowMillis;
      this.request = request;
      this.cacheResponse = cacheResponse;

      if (cacheResponse != null) {
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
        Headers headers = cacheResponse.headers();
        for (int i = 0, size = headers.size(); i < size; i++) {
          String fieldName = headers.name(i);
          String value = headers.value(i);
          if ("Date".equalsIgnoreCase(fieldName)) {
            servedDate = HttpDate.parse(value);
            servedDateString = value;
          } else if ("Expires".equalsIgnoreCase(fieldName)) {
            expires = HttpDate.parse(value);
          } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
            lastModified = HttpDate.parse(value);
            lastModifiedString = value;
          } else if ("ETag".equalsIgnoreCase(fieldName)) {
            etag = value;
          } else if ("Age".equalsIgnoreCase(fieldName)) {
            ageSeconds = HttpHeaders.parseSeconds(value, -1);
          }
        }
      }
    }

  • 觀察者模式

之前我寫過一篇文章,是關於Okhttp中websocket的使用,由於webSocket屬於長連線,所以需要進行監聽,這裡是用到了觀察者模式:

  final WebSocketListener listener;
  @Override public void onReadMessage(String text) throws IOException {
    listener.onMessage(this, text);
  }

  • 單例模式

這個就不舉例了,每個專案都會有

  • 另外有的部落格還說到了策略模式,門面模式等,這些大家可以網上搜搜,畢竟每個人的想法看法都會不同,細心找找可能就會發現。

介紹一下你們之前做的專案的架構

這個問題大家就真實回答就好,重點是要說完後提出對自己專案架構的認同或不認同的觀點,也就是要有自己的思考和想法。

MVP,MVVM,MVC 區別

MVC

  • 架構介紹

Model:資料模型,比如我們從資料庫或者網路獲取資料
View:檢視,也就是我們的xml佈局檔案
Controller:控制器,也就是我們的Activity

  • 模型聯絡

View --> Controller,也就是反應View的一些使用者事件(點選觸控事件)到Activity上。
Controller --> Model, 也就是Activity去讀寫一些我們需要的資料。
Controller --> View, 也就是Activity在獲取資料之後,將更新內容反映到View上。

這樣一個完整的專案架構就出來了,也是我們早期進行開發比較常用的專案架構。

  • 優缺點

這種缺點還是比較明顯的,主要表現就是我們的Activity太重了,經常一寫就是幾百上千行了。
造成這種問題的原因就是Controller層和View層的關係太過緊密,也就是Activity中有太多操作View的程式碼了。

但是!但是!其實Android這種並稱不上傳統的MVC結構,因為Activity又可以叫View層又可以叫Controller層,所以我覺得這種Android預設的開發結構,其實稱不上什麼MVC專案架構,因為他本身就是Android一開始預設的開發形式,所有東西都往Activity中丟,然後能封裝的封裝一下,根本分不出來這些層級。當然這是我個人看法,可以都來討論下。

MVP

  • 架構介紹

之前不就是因為Activity中有操作view,又做Controller工作嗎。
所以其實MVP架構就是從原來的Activity層把viewController區分開,單獨抽出來一層Presenter作為原來Controller的職位。
然後最後演化成,將View層寫成介面的形式,然後Activity去實現View介面,最後在Presenter類中去實現方法。

Model:資料模型,比如我們從資料庫或者網路獲取資料。
View:檢視,也就是我們的xml佈局檔案和Activity。
Presenter:主持人,單獨的類,只做排程工作。

  • 模型聯絡

View --> Presenter,反應View的一些使用者事件到Presenter上。
Presenter --> Model, Presenter去讀寫操作一些我們需要的資料。
Controller --> View, Presenter在獲取資料之後,將更新內容反饋給Activity,進行view更新。

  • 優缺點

這種的優點就是確實大大減少了Activity的負擔,讓Activity主要承擔一個更新View的工作,然後把跟Model互動的工作轉移給了Presenter,從而由Presenter方來控制和互動Model方以及View方。所以讓專案更加明確簡單,順序性思維開發。

缺點也很明顯:
首先就是程式碼量大大增加了,每個頁面或者說功能點,都要專門寫一個Presenter類,並且由於是面向介面程式設計,需要增加大量介面,會有大量繁瑣的回撥。
其次,由於Presenter裡持有了Activity物件,所以可能會導致記憶體洩漏或者view空指標,這也是需要注意的地方。

MVVM

  • 架構介紹

MVVM的特點就是雙向繫結,並且有Google官方加持,更新了Jetpack中很多架構元件,比如ViewModel,Livedata,DataBinding等等,所以這個是現在的主流框架和官方推崇的框架。

Model:資料模型,比如我們從資料庫或者網路獲取資料。
View:檢視,也就是我們的xml佈局檔案和Activity。
ViewModel:關聯層,將Model和View繫結,使他們之間可以相互繫結實時更新

  • 模型聯絡

View --> ViewModel -->View,雙向繫結,資料改動可以反映到介面,介面的修改可以反映到資料。
ViewModel --> Model, 操作一些我們需要的資料。

  • 優缺點

優點就是官方大力支援,所以也更新了很多相關庫,讓MVVM架構更強更好用,而且雙向繫結的特點可以讓我們省去很多View和Model的互動。也基本解決了上面兩個架構的問題。

具體說說你理解的MVVM

1)先說說MVVM是怎麼解決了其他兩個架構所在的缺陷和問題:

  • 解決了各個層級之間耦合度太高的問題,也就是更好的完成了解耦。MVP層中,Presenter還是會持有View的引用,但是在MVVM中,View和Model進行雙向繫結,從而使viewModel基本只需要處理業務邏輯,無需關係介面相關的元素了。

  • 解決了程式碼量太多,或者模式化程式碼太多的問題。由於雙向繫結,所以UI相關的程式碼就少了很多,這也是程式碼量少的關鍵。而這其中起到比較關鍵的元件就是DataBinding,使所有的UI變動都交給了被觀察的資料模型。

  • 解決了可能會有的記憶體洩漏問題。MVVM架構元件中有一個元件是LiveData,它具有生命週期感知能力,可以感知到Activity等的生命週期,所以就可以在其關聯的生命週期遭到銷燬後自行清理,就大大減少了記憶體洩漏問題。

  • 解決了因為Activity停止而導致的View空指標問題。在MVVM中使用了LiveData,那麼在需要更新View的時候,如果觀察者的生命週期處於非活躍狀態(如返回棧中的 Activity),則它不會接收任何 LiveData 事件。也就是他會保證在介面可見的時候才會進行響應,這樣就解決了空指標問題。

  • 解決了生命週期管理問題。這主要得益於Lifecycle元件,它使得一些控制元件可以對生命週期進行觀察,就能隨時隨地進行生命週期事件。

2)再說說響應式程式設計

響應式程式設計,說白了就是我先構建好事物之間的關係,然後就可以不用管了。他們之間會因為這層關係而互相驅動。
其實也就是我們常說的觀察者模式,或者說訂閱釋出模式。

為什麼說這個呢,因為MVVM的本質思想就是類似這種。不管是雙向繫結,還是生命週期感知,其實都是一種觀察者模式,使所有事物變得可觀察,那麼我們只需要把這種觀察關係給穩定住,那麼專案也就穩健了。

3)最後再說說MVVM為什麼這麼強大?

我個人覺得,MVVM強大不是因為這個架構本身,而是因為這種響應式程式設計的優勢比較大,再加上Google官方的大力支援,出了這麼多支援的元件,來維繫MVVM架構,其實也是官方想進行專案架構的統一。

優秀的架構思想+官方支援=強大

ViewModel 是什麼,說說你所理解的ViewModel?

如果看過我上一篇文章的小夥伴應該都有所瞭解,ViewModel是MVVM架構的一個層級,用來聯絡View和model之間的關係。而我們今天要說的就是官方出的一個框架——ViewModel

ViewModel 類旨在以注重生命週期的方式儲存和管理介面相關的資料

官方是這麼介紹的,這裡面有兩個資訊:

  • 注重生命週期的方式。
    由於ViewModel的生命週期是作用於整個Activity的,所以就節省了一些關於狀態維護的工作,最明顯的就是對於螢幕旋轉這種情況,以前對資料進行儲存讀取,而ViewModel則不需要,他可以自動保留資料。

其次,由於ViewModel在生命週期內會保持區域性單例,所以可以更方便Activity的多個Fragment之間通訊,因為他們能獲取到同一個ViewModel例項,也就是資料狀態可以共享了。

  • 儲存和管理介面相關的資料。

ViewModel層的根本職責,就是負責維護介面上UI的狀態,其實就是維護對應的資料,因為資料會最終體現到UI介面上。所以ViewModel層其實就是對介面相關的資料進行管理,儲存等操作。

ViewModel 為什麼被設計出來,解決了什麼問題?

  • ViewModel元件被設計出來之前,MVVM又是怎麼實現ViewModel這一層級的呢?

其實就是自己編寫類,然後通過介面,內部依賴實現View和資料的雙向繫結。
所以Google出這個ViewModel元件,無非就是為了規範MVVM架構的實現,並儘量讓ViewModel這一層級只觸及到業務程式碼,不去關心VIew層級的引用等。然後配合其他的元件,包括livedata,databindingrang等讓MVVM架構更加完善,規範,健碩。

  • 解決了什麼問題呢?

其實上面已經說過一些了,比如:

1)不會因為螢幕旋轉而銷燬,減少了維護狀態的工作
2)由於在作用域內單一例項的特性,使得多個fragment之間可以方便通訊,並且維護同一個資料狀態。
3)完善了MVVM架構,使得解耦更加純粹。

說說ViewModel原理。

  • 首先說說是怎麼儲存生命週期

ViewModel2.0之前呢,其實原理是在Activity上add一個HolderFragment,然後設定setRetainInstance(true)方法就能讓這個Fragment在Activity重建時存活下來,也就保證了ViewModel的狀態不會隨Activity的狀態所改變。

2.0之後,其實是用到了Activity的onRetainNonConfigurationInstance()getLastNonConfigurationInstance()這兩個方法,相當於在橫豎屏切的時候會儲存ViewModel的例項,然後恢復,所以也就保證了ViewModel的資料。

  • 再說說怎麼保證作用域內唯一例項

首先,ViewModel的例項是通過反射獲取的,反射的時候帶上application的上下文,這樣就保證了不會持有Activity或者Fragment等View的引用。然後例項建立出來會儲存到一個ViewModelStore容器裡面,其實也就是一個集合類,這個ViewModelStore 類其實就是儲存在介面上的那個例項,而我們的ViewModel就是裡面的一個集合類的子元素。

所以我們每次獲取的時候,首先看看這個集合裡面有無我們的ViewModel,如果沒有就去例項化,如果有就直接拿到例項使用,這樣就保證了唯一例項。最後在介面銷燬的時候,會去執行ViewModelStore的clear方法,去清除集合裡面的ViewModel資料。一小段程式碼說明下:

public <T extends ViewModel> T get(Class<T> modelClass) {
      // 先從ViewModelStore容器中去找是否存在ViewModel的例項
      ViewModel viewModel = mViewModelStore.get(key);
     
      // 若ViewModel已經存在,就直接返回
      if (modelClass.isInstance(viewModel)) {
            return (T) viewModel;
      }
       
      // 若不存在,再通過反射的方式例項化ViewModel,並儲存進ViewModelStore
      viewModel = modelClass.getConstructor(Application.class).newInstance(mApplication);
      mViewModelStore.put(key, viewModel);
      return (T) viewModel;
 }


public class ViewModelStore {
    private final HashMap<String, ViewModel> mMap = new HashMap<>();

     public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.onCleared();
        }
        mMap.clear();
    }
}



 @Override
protected void onDestroy() {
    super.onDestroy();

   if (mViewModelStore != null && !isChangingConfigurations()) {
        mViewModelStore.clear();
    }

}

ViewModel怎麼實現自動處理生命週期?為什麼在旋轉螢幕後不會丟失狀態?為什麼ViewModel可以跟隨Activity/Fragment的生命週期而又不會造成記憶體洩漏呢?

這三個問題很類似,都是關於生命週期的問題,其實也就是問為什麼ViewModel能管理生命週期,並且不會因為重建等情況造成影響。

  • ViewModel2.0之前

利用一個無view 的HolderFragment來維持它的生命週期,我們知道ViewModel例項是儲存到一個ViewModelStore容器裡的,那麼這個空的fragment就可以用來管理這個容器,只要Activity處於活動狀態,HolderFragment也就不會被銷燬,就保證了ViewModel的生命週期。

而且設定setRetainInstance(true)方法可以保證configchange時的生命週期不被改變,讓這個Fragment在Activity重建時存活下來。總結來說就是用一個空的fragment來管理維護ViewModelStore,然後對應的activity銷燬的時候就去把viewmodel的對映刪除。就讓ViewModel的生命週期保持和Activity一樣了。這也是很多三方庫用到的巧妙方法,比如Glide,也是建立空的Fragment來管理。

  • 2.0之後,有了androidx支援

其實是用到了Activity的一個子類ComponentActivity,然後重寫了onRetainNonConfigurationInstance()方法儲存ViewModelStore,並在需要的時候,也就是重建的Activity中去通過getLastNonConfigurationInstance()方法獲取到ViewModelStore例項。這樣也就保證了ViewModelStore中的ViewModel不會隨Activity的重建而改變。

同時由於實現了LifecycleOwner介面,所以能利用Lifecycles元件元件感知每個頁面的生命週期,就可以通過它來訂閱當Activity銷燬時,且不是因為配置導致的destory情況下,去清除ViewModel,也就是呼叫ViewModelStore的clear方法。


getLifecycle().addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                // 判斷是否因為配置更改導致的destroy
                if (!isChangingConfigurations()) {
                    getViewModelStore().clear();
                }
            }
        }
    });

這裡的onRetainNonConfigurationInstance方法再說下,是會在Activity因為配置改變而被銷燬時被呼叫,跟onSaveInstanceState方法呼叫時機比較相像,不同的是onSaveInstanceState儲存的是Bundle,Bundle是有型別限制和大小限制的,而且需要在主執行緒進行序列號。而onRetainNonConfigurationInstance方法都沒有限制,所以更傾向於用它。

所以,到這裡,第三個問題應該也可以回答了,2.0之前呢,都是通過他們建立了一個空的fragment,然後跟隨這個fragment的生命週期。2.0之後呢,是因為不管是Activity或者Fragment,都實現了LifecycleOwner介面,所以ViewModel是可以通過Lifecycles感知到他們的生命週期,從而進行例項管理的。

ViewModelScope瞭解嗎

這裡主要就是考ViewModel和其他一些元件的關係了。關於協程,之前也專門說過一篇,主要用作執行緒切換。如果在多個協程中,需要停止某些任務,就必須對這些協程進行管理,一般是加入一個CoroutineScope,如果需要取消協程,就可以去取消這個CoroutineScope,他所跟蹤的所有協程都會被取消。

GlobalScope.launch {
    longRunningFunction()
    anotherLongRunningFunction()
}

但是這種全域性使用方法,是不被推薦使用的,如果要限定作用域的時候,一般推薦viewModelScope。

viewModelScope 是一個 ViewModel 的 Kotlin 擴充套件屬性。它能在ViewModel銷燬時 (onCleared() 方法呼叫時) 退出。所以只要使用了 ViewModel,就可以使用 viewModelScope 在 ViewModel 中啟動各種協程,而不用擔心任務洩漏。


class MyViewModel() : ViewModel() {

    fun initialize() {
        viewModelScope.launch {
            processBitmap()
        }
    }

    suspend fun processBitmap() = withContext(Dispatchers.Default) {
        // 在這裡做耗時操作
    }

}

LiveData 是什麼?

LiveData 是一種可觀察的資料儲存器類。與常規的可觀察類不同,LiveData 具有生命週期感知能力,意指它遵循其他應用元件(如 Activity、Fragment 或 Service)的生命週期。這種感知能力可確保 LiveData 僅更新處於活躍生命週期狀態的應用元件觀察者。

官方介紹如下,其實說的比較清楚了,主要作用在兩點:

  • 資料儲存器類。也就是一個用來儲存資料的類。

  • 可觀察。這個資料儲存類是可以觀察的,也就是比一般的資料儲存類多了這麼一個功能,對於資料的變動能進行響應。

主要思想就是用到了觀察者模式思想,讓觀察者和被觀察者解耦,同時還能感知到資料的變化,所以一般被用到ViewModel中,ViewModel負責觸發資料的更新,更新會通知到LiveData,然後LiveData再通知活躍狀態的觀察者。

        var liveData = MutableLiveData<String>()

        liveData.observe(this, object : Observer<String> {
            override fun onChanged(t: String?) {
            }
        })

        liveData.setVaile("xixi")
        //子執行緒呼叫
        liveData.postValue("test")

LiveData 為什麼被設計出來,解決了什麼問題?

LiveData作為一種觀察者模式設計思想,常常被和Rxjava一起比較,觀察者模式的最大好處就是事件發射的上游 和 接收事件的下游 互不干涉,大幅降低了互相持有的依賴關係所帶來的強耦合性

其次,LiveData還能無縫銜接到MVVM架構中,主要體現在其可以感知到Activity等生命週期,這樣就帶來了很多好處:

  • 不會發生記憶體洩漏
    觀察者會繫結到 Lifecycle 物件,並在其關聯的生命週期遭到銷燬後進行自我清理。

  • 不會因 Activity 停止而導致崩潰
    如果觀察者的生命週期處於非活躍狀態(如返回棧中的 Activity),則它不會接收任何 LiveData 事件。

  • 自動判斷生命週期並回撥方法
    如果觀察者的生命週期處於 STARTEDRESUMED狀態,則 LiveData 會認為該觀察者處於活躍狀態,就會呼叫onActive方法,否則,如果 LiveData 物件沒有任何活躍觀察者時,會呼叫 onInactive()方法。

說說LiveData原理。

說到原理,其實就是兩個方法:

  • 訂閱方法,也就是observe方法。通過該方法把訂閱者和被觀察者關聯起來,形成觀察者模式。

簡單看看原始碼:

    @MainThread
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
        assertMainThread("observe");
        //...
        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        if (existing != null && !existing.isAttachedTo(owner)) {
            throw new IllegalArgumentException("Cannot add the same observer"
                    + " with different lifecycles");
        }
        if (existing != null) {
            return;
        }
        owner.getLifecycle().addObserver(wrapper);
    }

      public V putIfAbsent(@NonNull K key, @NonNull V v) {
        Entry<K, V> entry = get(key);
        if (entry != null) {
            return entry.mValue;
        }
        put(key, v);
        return null;
    }

這裡putIfAbsent方法是講生命週期相關的wrapper和觀察者observer作為key和value存到了mObservers中。

  • 回撥方法,也就是onChanged方法。通過改變儲存值,來通知到觀察者也就是呼叫onChanged方法。從改變儲存值方法setValue看起:
@MainThread
protected void setValue(T value) {
    assertMainThread("setValue");
    mVersion++;
    mData = value;
    dispatchingValue(null);
}


private void dispatchingValue(@Nullable ObserverWrapper initiator) {
    //...
    do {
        mDispatchInvalidated = false;

        if (initiator != null) {
            considerNotify(initiator);
            initiator = null;
        } else {
            for (Iterator<Map.Entry<Observer<T>, ObserverWrapper>> iterator =
                    mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                considerNotify(iterator.next().getValue());
                if (mDispatchInvalidated) {
                    break;
                }
            }
        }
    } while (mDispatchInvalidated);
    mDispatchingValue = false;
}


private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) {
        return;
    }
    // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
    //
    // we still first check observer.active to keep it as the entrance for events. So even if
    // the observer moved to an active state, if we've not received that event, we better not
    // notify for a more predictable notification order.
    if (!observer.shouldBeActive()) {
        observer.activeStateChanged(false);
        return;
    }
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    //noinspection unchecked
    observer.mObserver.onChanged((T) mData);
}

這一套下來邏輯還是比較簡單的,遍歷剛才的map——mObservers,然後找到觀察者observer,如果觀察者不在活躍狀態(活躍狀態,也就是可見狀態,處於 STARTED 或 RESUMED狀態),則直接返回,不去通知。否則正常通知到觀察者的onChanged方法。

當然,如果想任何時候都能監聽到,都能獲取回撥,呼叫observeForever方法即可。

依賴注入是啥?為什麼需要她?

簡單的說,依賴注入就是內部的類在外部例項化了。也就是不需要自己去做例項化工作了,而是交給外部容器來完成,最後注入到呼叫者這邊,形成依賴注入。

舉個例子:
Activity中有一個user類,正常情況下要使用這個user肯定是需要例項化它,不然他是個空值,但是用了依賴注入後,就不需要在Activity內部再去例項化,就可以直接使用它了。


@AndroidEntryPoint
class MainActivity : BaseActivity() {
    @Inject
    lateinit var user: User
}

這個user就可以直接使用了,是不是有點神奇,都不需要手動依賴了,當然程式碼沒寫完,後面再去完善。只是表達了這麼一個意思,也就是依賴注入的含義。

那麼這種由外部容器來例項化物件的方式到底有什麼好處呢?最大的好處就是減少了手動依賴,對類進行了解耦。具體主要有以下幾點:

  • 依賴注入庫會自動釋放不再使用的物件,減少資源的過度使用。
  • 在配置 scopes範圍內,可重用依賴項和建立的例項,提高程式碼的可重用性,減少了很多模板程式碼。
  • 程式碼變得更具可讀性。
  • 易於構建物件。
  • 編寫低耦合程式碼,更容易測試。

Hilt是啥,怎麼用?

很明顯,Hilt就是一個依賴注入庫,一個封裝了Dagger,在Dagger的基礎上進行構建的一個依賴注入庫。Dagger我們都知道是一個早期的依賴注入庫,但確實不好用,需要配置很多東西,那麼Hilt簡單到哪了呢?我們繼續完善上面的例子:

@HiltAndroidApp
public class MainApplication extends Application {
}

@AndroidEntryPoint
class HiltActivitiy : AppCompatActivity() {

    @Inject
    lateinit var user: UserData

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        showToast(user.name)
    }
}


data class UserData(var name: String) {
    @Inject
    constructor() : this("bob")
}

說下幾個註釋的含義:

  • @HiltAndroidApp。所有使用Hilt的App必須包含一個使用 @HiltAndroidApp 註解的 Application,相當於Hilt的初始化,會觸發Hilt程式碼的生成。
  • @AndroidEntryPoint。用於提供類的依賴,也就是代表這個類會用到注入的例項。
  • @Inject。這個註解是用來告訴 Hilt 如何提供該類的例項,它常用於建構函式、非私有欄位、方法中。

Hilt支援哪些類的依賴注入。

1) 如果是 Hilt 支援的 Android 元件,直接使用 @AndroidEntryPoint註解即可。比如Activity,Fragment,Service等等。

  • 如果是ComponentActivity的子類Activity,那麼直接使用@AndroidEntryPoint就可以了,比如上面的例子。
  • 如果是其他的Android類,必須在它依賴的Android類新增同樣的註解,例如在 Fragment 中新增@AndroidEntryPoint註解,必須在Fragment依賴的Activity上也新增@AndroidEntryPoint註解。

2)如果是需要注入第三方的依賴,可以使用@Module註解,使用 @Module註解的普通類,在其中建立第三方依賴的物件。比如獲取okhttp的例項

@Module
@InstallIn(ApplicationComponent::class)
object NetworkModule {

    /**
     * @Provides 
     * @Singleton 提供單例
     */
    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .build()
    }

}

這裡又有幾個新的註解了:

  • @Module。用於建立依賴類的物件
  • @InstallIn。使用 @Module 注入的類,需要使用 @InstallIn 註解指定 module 的範圍,例如使用 @InstallIn(ActivityComponent::class) 註解的 module 會繫結到 activity 的生命週期上。
  • @Provides。用於被 @Module註解標記類的內部的方法,並提供依賴項物件。
  • @Singleton。提供單例

3)為ViewModel提供的專門的註解

@ViewModelInject,在Viewmodel物件的建構函式中使用 @ViewModelInject 註解可以提供一個 ViewModel。

class HiltViewModel @ViewModelInject constructor() : ViewModel() {}

private val mHitViewModule: HiltViewModel by viewModels()

說說DNS,以及存在的問題

之前看過我說的網路問題應該知道DNS用來做域名解析工作的,當輸入一個域名後,需要把域名轉化為IP地址,這個轉換過程就是DNS解析

但是傳統的DSN解析會有一些問題,比如:

  • 域名快取問題
    本地做一個快取,直接返回快取資料。可能會導致全域性負載均衡失敗,因為上次進行的快取,不一定是這次離客戶最近的地方,可能會繞遠路。

  • 域名轉發問題
    如果是A運營商將解析的請求轉發給B運營商,B去權威DNS伺服器查詢的話,權威伺服器會認為你是B運營商的,就返回了B運營商的網站地址,結果每次都會跨運營商。

  • 出口NAT問題
    做了網路地址轉化後,權威的DNS伺服器,沒法通過地址來判斷客戶到底是哪個運營商,極有可能誤判運營商,導致跨運營商訪問。

  • 域名更新問題
    本地DNS伺服器是由不同地區,不同運營商獨立部署的,對域名解析快取的處理上,有區別,有的會偷懶忽略解析結果TTL的時間限制,導致伺服器沒有更新新的ip而是指向舊的ip。

  • 解析延遲
    DNS的查詢過程需要遞迴遍歷多個DNS伺服器,才能獲得最終結果。可能會帶來一定的延時。

  • 域名劫持
    DNS域名解析伺服器有可能會被劫持,或者被偽造,那麼正常的訪問就會被解析到錯誤的地址。

  • 不可靠
    由於DNS解析是執行在UDP協議之上的,而UDP我之前也說過是一種不可靠的協議,他的優勢在於實時性,但是有丟包的可能。

這些問題不僅會讓訪問速度變慢,還有可能會導致訪問異常,訪問頁面被替換等等。

怎麼優化DNS解析

  • 安全優化

總之DNS還是會有各種問題吧,怎麼解決呢?就是用HTTPDNS

HTTPDNS是一個新概念,他會繞過傳統的運營商DNS伺服器,不走傳統的DNS解析。而是換成HTTP協議,直接通過HTTP協議進行請求某個DNS伺服器叢集,獲取地址。

  • 由於繞過了運營商,所以可以避免域名被劫持。
  • 它是基於訪問的來源ip,所以能獲得更準確的解析結果
  • 會有預解析解析快取等功能,所以解析延遲也很小

所以首先的優化,針對安全方面,就是要替換成HTTPDNS解析方式,就要借用阿里雲和騰訊雲等服務,但是這些服務可不是免費的,有沒有免費的呢?有的,七牛雲的 happy-dns。新增依賴庫,然後去實現okhttp的DNS介面即可,簡單寫個例子:


//匯入庫
    implementation 'com.qiniu:happy-dns:0.2.13'
    implementation 'com.qiniu.pili:pili-android-qos:0.8'


//實現DNS介面
public class HttpDns implements Dns {

    private DnsManager dnsManager;

    public HttpDns() {
        IResolver[] resolvers = new IResolver[1];
        try {
            resolvers[0] = new Resolver(InetAddress.getByName("119.29.29.29"));
            dnsManager = new DnsManager(NetworkInfo.normal, resolvers);
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }

    @Override
    public List<InetAddress> lookup(String hostname) throws UnknownHostException {
        if (dnsManager == null)  //當構造失敗時使用預設解析方式
            return Dns.SYSTEM.lookup(hostname);

        try {
            String[] ips = dnsManager.query(hostname);  //獲取HttpDNS解析結果
            if (ips == null || ips.length == 0) {
                return Dns.SYSTEM.lookup(hostname);
            }

            List<InetAddress> result = new ArrayList<>();
            for (String ip : ips) {  //將ip地址陣列轉換成所需要的物件列表
                result.addAll(Arrays.asList(InetAddress.getAllByName(ip)));
            }
            //在返回result之前,我們可以新增一些其他自己知道的IP
            return result;
        } catch (IOException e) {
            e.printStackTrace();
        }
        //當有異常發生時,使用預設解析
        return Dns.SYSTEM.lookup(hostname);
    }
}


//替換okhttp的dns解析
OkHttpClient okHttpClient = new OkHttpClient.Builder().dns(new HttpDns()).build();

  • 速度優化

如果在測試環境,其實我們可以直接配置ip白名單,然後跳過DNS解析流程,直接獲取ip地址。比如:

    private static class TestDNS implements Dns{
        @Override
        public List<InetAddress> lookup(@NotNull String hostname) throws UnknownHostException {
            if ("www.test.com".equalsIgnoreCase(hostname)){
                InetAddress byAddress=InetAddress.getByAddress(hostname,new byte[]{(byte)192,(byte)168,1,1});
                return Collections.singletonList(byAddress);
            }else {
                return Dns.SYSTEM.lookup(hostname);
            }
        }
    }

DNS解析超時怎麼辦

當我們在用OKHttp做網路請求時,如果網路裝置切換路由,訪問網路出現長時間無響應,很久之後會丟擲 UnknownHostException。雖然我們在OkHttp中設定了connectTimeout超時時間,但是它其實對DNS的解析是不起作用的。

這種情況我們就需要在自定義的Dns類中做超時判斷:

public class TimeDns implements Dns {
    private long timeout;

    public TimeDns(long timeout) {
        this.timeout = timeout;
    }

    @Override
    public List<InetAddress> lookup(final String hostname) throws UnknownHostException {
        if (hostname == null) {
            throw new UnknownHostException("hostname == null");
        } else {
            try {
                FutureTask<List<InetAddress>> task = new FutureTask<>(
                        new Callable<List<InetAddress>>() {
                            @Override
                            public List<InetAddress> call() throws Exception {
                                return Arrays.asList(InetAddress.getAllByName(hostname));
                            }
                        });
                new Thread(task).start();
                return task.get(timeout, TimeUnit.MILLISECONDS);
            } catch (Exception var4) {
                UnknownHostException unknownHostException =
                        new UnknownHostException("Broken system behaviour for dns lookup of " + hostname);
                unknownHostException.initCause(var4);
                throw unknownHostException;
            }
        }
    }
}

//替換okhttp的dns解析
OkHttpClient okHttpClient = new OkHttpClient.Builder().dns(new TimeDns(5000)).build();

註解是什麼?有哪些元註解

註解,在我看來它是一種資訊描述,不影響程式碼執行,但是可以用來配置一些程式碼或者功能。

常見的註解比如@Override,代表重寫方法,看看它是怎麼生成的:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

可以看到Override被@interface所修飾,代表註解,同時上方還有兩個註解@Target和@Retention,這種修飾註解的註解叫做元註解,很好理解吧,就是最基本的註解唄。java中一共有四個元註解:

  • @Target:表示註解物件的作用範圍。
  • @Retention:表示註解保留的生命週期
  • @Inherited:表示註解型別能被類自動繼承。
  • @Documented:表示含有該註解型別的元素(帶有註釋的)會通過javadoc或類似工具進行文件化。

具體說下這幾個元註解都是怎麼用的

  • @Target

target,表示註解物件的作用範圍,比如Override註解所標示的就是ElementType.METHOD,即所作用的範圍是方法範圍,也就是隻能在方法頭上加這個註解。另外還有以下幾個修飾範圍引數:

  • TYPE:類、介面、列舉、註解型別。
  • FIELD:類成員(構造方法、方法、成員變數)。
  • METHOD:方法。
  • PARAMETER:引數。
  • CONSTRUCTOR:構造器。
  • LOCAL_VARIABLE:區域性變數。
  • ANNOTATION_TYPE:註解。
  • PACKAGE:包宣告。
  • TYPE_PARAMETER:型別引數。
  • TYPE_USE:型別使用宣告。

比如ANNOTATION_TYPE就是表示該註解的作用範圍就是註解,哈哈,有點繞吧,看看Target註解的程式碼:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

帶了一個ElementType型別的引數,也就是上面說到的作用範圍引數,另外還被Target註解修飾了,傳的引數就是ANNOTATION_TYPE,也就是我註解我自己,我設定我自己的作用範圍是註解。大家自己繞一下。。

  • @Retention

表示註解保留的生命週期,或者說表示該註解所保留的時長,主要有以下幾個可選引數:

  • SOURCE:僅存在Java原始檔,經過編譯器後便丟棄相應的註解。適用於一些檢查性的操作,比如@Override。

  • CLASS:編譯class檔案時生效,存在Java原始檔,以及經編譯器後生成的Class位元組碼檔案,但在執行時VM不再保留註釋。這個也是預設的引數。適用於在編譯時進行一些預處理操作,比如ButterKnife的@BindView,可以在編譯時生成一些輔助的程式碼或者完成一些功能。

  • RUNTIME:存在原始檔、編譯生成的Class位元組碼檔案,以及保留在執行時VM中,可通過反射性地讀取註解。適用於一些需要執行時動態獲取註解資訊,類似反射獲取註解等。

  • @Inherited

表示註解型別能被類自動繼承。這裡需要注意兩點:

  • 。也就是說只有在類整合關係中,子類才會整合父類使用的註解中被@Inherited所修飾的那個註解。其他的介面整合關係,類實現介面關係中,都不會存在自動繼承註解。

  • 自動繼承。也就是說如果父類有@Inherited所修飾的那個註解,那麼子類不需要去寫這個註解,就會自動有了這個註解。

還是看個例子:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface MyInheritedAnnotation {
	//註解1,有Inherited註解修飾
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
	//註解2,沒有Inherited註解修飾
}


@MyInheritedAnnotation
@MyAnnotation
public class BaseClass {
	//父類,有以上兩個註解
}
 
public class ExtendClass extends BaseClass  {

	//子類會繼承父類的MyInheritedAnnotation註解,
	//而不會繼承MyAnnotation註解
}


  • @Documented

表示擁有該註解的元素可通過javadoc此類的工具進行文件化,也就是說生成JavaAPI文件的時候會被寫進文件中。

註解可以用來做什麼

主要有以下幾個用處:

  • 降低專案的耦合度。
  • 自動完成一些規律性的程式碼
  • 自動生成java程式碼,減輕開發者的工作量。

序列化指的是什麼?有什麼用

序列化指的是講物件變成有序的位元組流,變成位元組流之後才能進行傳輸儲存等一系列操作。
反序列化就是序列化的相反操作,也就是把序列化生成的位元組流轉為我們記憶體的物件。

介紹下Android中兩種序列化介面

  • Serializable

Java提供的一個序列化介面,是一個空介面,專門為物件提供序列化和反序列化操作。具體使用如下:

public class User implements Serializable {
    private static final long serialVersionUID=519067123721561165l;
    
    private int id;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

實現Serializable介面,宣告一個serialVersionUID

到這裡可能有人就問了,不對啊,平時沒有這個serialVersionUID啊。沒錯,serialVersionUID不是必須的,因為不寫的話,系統會自動生成這個變數。它有什麼用呢?當序列化的時候,系統會把當前類的serialVersionUID寫入序列化的檔案中,當反序列化的時候會去檢測這個serialVersionUID,看他是否和當前類的serialVersionUID一致,一樣則可以正常反序列化,如果不一樣就會報錯了。

所以這個serialVersionUID就是序列化和反序列化過程中的一個標識,代表一致性。不加的話會有什麼影響?如果我們序列化後,改動了這個類的某些成員變數,那麼serialVersionUID就會改變,這時候再拿之前序列化的資料來反序列化就會報錯。所以如果我們手動指定serialVersionUID就能保證最大限度來恢復資料。

  • Parcelable

Android自帶的介面,使用起來要麻煩很多:需要實現Parcelable介面,重寫describeContents(),writeToParcel(Parcel dest, @WriteFlags int flags),並新增一個靜態成員變數CREATOR並實現Parcelable.Creator介面

public class User implements Parcelable {
    
    private int id;

    protected User(Parcel in) {
        id = in.readInt();
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(id);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    public static final Creator<User> CREATOR = new Creator<User>() {
        @Override
        public User createFromParcel(Parcel in) {
            return new User(in);
        }

        @Override
        public User[] newArray(int size) {
            return new User[size];
        }
    };

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

  • createFromParcel,User(Parcel in) ,代表從序列化的物件中建立原始物件
  • newArray,代表建立指定長度的原始物件陣列
  • writeToParcel,代表將當前物件寫入到序列化結構中。
  • describeContents,代表返回當前物件的內容描述。如果還有檔案描述符,返回1,否則返回0。

兩者有什麼區別,該怎麼使用選擇

Serializable是Java提供的序列化介面,使用簡單但是開銷很大,序列化和反序列化過程都需要大量I/O操作。
Parcelable是Android中提供的,也是Android中推薦的序列化方式。雖然使用麻煩,但是效率很高。

所以,如果是記憶體序列化層面,那麼還是建議Parcelable,因為他效率會比較高。
如果是網路傳輸和儲存磁碟情況,就推薦Serializable,因為序列化方式比較簡單,而且Parcelable不能保證,當外部條件發生變化時資料的連續性。

  • Serializable

Serializable的實質其實是是把Java物件序列化為二進位制檔案,然後就能在程式之間傳遞,並且用於網路傳輸或者本地儲存等一系列操作,因為他的本質就儲存了檔案。可以看看原始碼:


private void writeObject0(Object obj, boolean unshared)
    throws IOException
{
    ...
    try {
     
        Object orig = obj;
        Class<?> cl = obj.getClass();
        ObjectStreamClass desc;
       
        desc = ObjectStreamClass.lookup(cl, true);
   
        if (obj instanceof Class) {
            writeClass((Class) obj, unshared);
        } else if (obj instanceof ObjectStreamClass) {
            writeClassDesc((ObjectStreamClass) obj, unshared);
        // END Android-changed:  Make Class and ObjectStreamClass replaceable.
        } else if (obj instanceof String) {
            writeString((String) obj, unshared);
        } else if (cl.isArray()) {
            writeArray(obj, desc, unshared);
        } else if (obj instanceof Enum) {
            writeEnum((Enum<?>) obj, desc, unshared);
        } else if (obj instanceof Serializable) {
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            if (extendedDebugInfo) {
                throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
            } else {
                throw new NotSerializableException(cl.getName());
            }
        }
    } 
    ...
}


private void writeOrdinaryObject(Object obj,
                                     ObjectStreamClass desc,
                                     boolean unshared)
        throws IOException
    {
        ...
        try {
            desc.checkSerialize();
            
            //寫入二進位制檔案,普通物件開頭的魔數0x73
            bout.writeByte(TC_OBJECT);
            //寫入對應的類的描述符,見底下原始碼
            writeClassDesc(desc, false);
            
            handles.assign(unshared ? null : obj);
            if (desc.isExternalizable() && !desc.isProxy()) {
                writeExternalData((Externalizable) obj);
            } else {
                writeSerialData(obj, desc);
            }
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }

    public long getSerialVersionUID() {
        // 如果沒有定義serialVersionUID,序列化機制就會呼叫一個函式根據類內部的屬性等計算出一個hash值
        if (suid == null) {
            suid = AccessController.doPrivileged(
                new PrivilegedAction<Long>() {
                    public Long run() {
                        return computeDefaultSUID(cl);
                    }
                }
            );
        }
        return suid.longValue();
    }

可以看到是通過反射獲取物件以及物件屬性的相關資訊,然後將資料寫到了一個二進位制檔案,並且寫入了序列化協議版本等等。
而獲取·serialVersionUID·的邏輯也體現出來,如果id為空則會生成計算一個hash值。

  • Parcelable

Parcelable的儲存是通過Parcel儲存到記憶體的,簡單地說,Parcel提供了一套機制,可以將序列化之後的資料寫入到一個共享記憶體中,其他程式通過Parcel可以從這塊共享記憶體中讀出位元組流,並反序列化成物件。

這其中實際又是通過native方法實現的。具體邏輯我就沒有去分析了,如果有大神朋友可以在評論區解析下。

當然,Parcelable也是可以持久化的,涉及到Parcel中的unmarshallmarshall方法。 這裡簡單貼一下程式碼:

protected void saveParce() {
        FileOutputStream fos;
        try {
            fos = getApplicationContext().openFileOutput(TAG,
                    Context.MODE_PRIVATE);
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            Parcel parcel = Parcel.obtain();
            parcel.writeParcelable(new ParceData(), 0);

            bos.write(parcel.marshall());
            bos.flush();
            bos.close();
            fos.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    protected void loadParce() {
        FileInputStream fis;
        try {
            fis = getApplicationContext().openFileInput(TAG);
            byte[] bytes = new byte[fis.available()];
            fis.read(bytes);
            Parcel parcel = Parcel.obtain();
            parcel.unmarshall(bytes, 0, bytes.length);
            parcel.setDataPosition(0);

            ParceData data = parcel.readParcelable(ParceData.class.getClassLoader());
            fis.close();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

序列化總結

1)對於記憶體序列化方面建議用Parcelable,為什麼呢?

  • 因為Serializable是儲存了一個二進位制檔案,所以會有頻繁的IO操作,消耗也比較大,而且用到了大量反射,反射操作也是耗時的。相比之下Parcelable就要效率高很多。

2)對於資料持久化還是建議用Serializable,為什麼呢?

  • 首先,Serializable本身就是儲存到二進位制檔案,所以用於持久化比較方便。而Parcelable序列化是在記憶體中操作,如果程式關閉或者重啟的時候,記憶體中的資料就會消失,那麼Parcelable序列化用來持久化就有可能會失敗,也就是資料不會連續完整。而且Parcelable還有一個問題是相容性,每個Android版本可能內部實現都不一樣,知識用於記憶體中也就是傳遞資料的話是不影響的,但是如果持久化可能就會有問題了,低版本的資料拿到高版本可能會出現相容性問題。 所以還是建議用Serializable進行持久化。

3)Parcelable一定比Serializable快嗎?

  • 有個比較有趣的例子是:當序列化一個超級大的物件圖表(表示通過一個物件,擁有通過某路徑能訪問到其他很多的物件),並且每個物件有10個以上屬性時,並且Serializable實現了writeObject()以及readObject(),在平均每檯安卓裝置上,Serializable序列化速度大於Parcelable 3.6倍,反序列化速度大於1.6倍.

具體原因就是因為Serilazable的實現方式中,是有快取的概念的,當一個物件被解析過後,將會快取在HandleTable中,當下一次解析到同一種型別的物件後,便可以向二進位制流中,寫入對應的快取索引即可。但是對於Parcel來說,沒有這種概念,每一次的序列化都是獨立的,每一個物件,都當作一種新的物件以及新的型別的方式來處理。

LruCache介紹

LruCache 是Android3.1提供的一個快取類,用於資料快取,一般用於圖片的記憶體快取。Lru的英文是Least Recently Used,也就是近期最少使用演算法,核心思想是當快取滿時,會優先淘汰那些近期最少使用的快取物件。

當我們進行網路載入圖片的時候,肯定要對圖片進行快取,這樣下次載入圖片就可以直接從快取中取。三級快取大家應該都比較熟悉,記憶體,硬碟和網路。所以一般要進行記憶體快取和硬碟快取,其中記憶體快取就是用的LruCache。

LruCache使用

public class MyImageLoader {
    private LruCache<String, Bitmap> mLruCache;

    public MyImageLoader() {
        int maxMemory = (int) (Runtime.getRuntime().maxMemory())/1024;
        int cacheSize = maxMemory / 8;
        mLruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes()*value.getHeight()/1024;
            }
        };

    }

    /**
     * 新增圖片快取
     */
    public void addBitmap(String key, Bitmap bitmap) {
            mLruCache.put(key, bitmap);
    }

    /**
     * 從快取中獲取圖片
     *
     */
    public Bitmap getBitmap(String key) {
        return mLruCache.get(key);
    }

}

使用方法如上,只需要提供快取的總容量大小並重寫sizeOf方法計算快取物件大小即可。這裡總容量的大小也是通用方法,即程式可用記憶體的1/8,單位kb。然後就可以使用put方法來新增快取物件,get方法來獲取快取物件。

LruCache原理

原理其實也很簡單,就是用到了LRU演算法,內部使用LinkedHashMap 進行儲存。在快取滿了之後,會將最近最少使用的元素移除。怎麼保證找到這個最近最少的元素呢?就是每次使用get方法訪問了元素或者增加了一個元素,就把元素移動到LinkedHashMap的尾部,這樣第一個元素就是最不經常使用的元素,在容量滿了之後就可以將它移除。

簡單看看原始碼:


 public LruCache(int maxSize) {
       if (maxSize <= 0) {
           throw new IllegalArgumentException("maxSize <= 0");
       }
       this.maxSize = maxSize;
       this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
   }

   public final V put(K key, V value) {
       if (key == null || value == null) {
           throw new NullPointerException("key == null || value == null");
       }

       V previous; //查詢是否已經存在key對應的元素
       synchronized (this) {
           putCount++;
           //計算entry的大小
           size += safeSizeOf(key, value); 
           previous = map.put(key, value);
           if (previous != null) {
             //如果之前存在,這先減去之前那個entry所佔用的記憶體大小
               size -= safeSizeOf(key, previous);
           }
       }

       if (previous != null) {
       //如果之前存在則呼叫entryRemoved回撥子類重寫的此方法,做一些處理
           entryRemoved(false, key, previous, value);
       }
       //根據最大的容量,計算是否需要淘汰掉最不常使用的entry
       trimToSize(maxSize);
       return previous;
   }


    public final V get(K key) {
      if (key == null) {
          throw new NullPointerException("key == null");
      }

      V mapValue;
      //根據key來查詢符合條件的etnry
      synchronized (this) {
          mapValue = map.get(key);
          if (mapValue != null) {
              hitCount++;
              return mapValue;
          }
          missCount++;
      }

      /*
       * Attempt to create a value. This may take a long time, and the map
       * may be different when create() returns. If a conflicting value was
       * added to the map while create() was working, we leave that value in
       * the map and release the created value.
       */

      V createdValue = create(key);
      if (createdValue == null) {
          return null;
      }

      synchronized (this) {
          createCount++;
          //mapValue返回的是已經存在相同key的entry
          mapValue = map.put(key, createdValue);

          if (mapValue != null) {
              // There was a conflict so undo that last put
              map.put(key, mapValue);
          } else {
              size += safeSizeOf(key, createdValue);
          }
      }

      if (mapValue != null) {
          entryRemoved(false, key, createdValue, mapValue);
          return mapValue;
      } else {
          trimToSize(maxSize);
          return createdValue;
      }
  }


其實可以看到,LruCache類本身做的事情不多,限定了快取map的大小,然後利用了LinkHashMap完成了LRU的快取策略。所以主要的實現LRU邏輯部分還是在LinkHashMap中。LinkedHashMap是hashmap和連結串列的結合體,通過連結串列來記錄元素的順序和連結關係,通過HashMap來儲存資料,它可以控制元素的被遍歷時候輸出的順序。他是一個雙向連結串列,上面說過他會把最近訪問的元素放到佇列的尾部,有興趣的可以看看LinkHashMap的原始碼。

Activity從建立到我們看到介面,發生了哪些事

  • 首先是通過setContentView載入佈局,這其中建立了一個DecorView,然後根據然後根據activity設定的主題(theme)或者特徵(Feature)載入不同的根佈局檔案,最後再通過inflate方法載入layoutResID資原始檔,其實就是解析了xml檔案,根據節點生成了View物件。流程圖:

載入佈局流程

  • 其次就是進行view繪製到介面上,這個過程發生在handleResumeActivity方法中,也就是觸發onResume的方法。在這裡會建立一個ViewRootImpl物件,作為DecorView的parent然後對DecorView進行測量佈局和繪製三大流程。流程圖:

繪製流程

Activity、PhoneWindow、DecorView、ViewRootImpl 的關係?

  • PhoneWindow是Window 的唯一子類,每個Activity都會建立一個PhoneWindow物件,你可以理解它為一個視窗,但不是真正的可視視窗,而是一個管理類,是Activity和整個View系統互動的介面,是Activity和View互動系統的中間層。

  • DecorView是PhoneWindow的一個內部類,是整個View層級的最頂層,一般包括標題欄和內容欄兩部分,會根據不同的主題特性調整不同的佈局。它是在setContentView方法中被建立,具體點來說是在PhoneWindow的installDecor方法中被建立。

  • ViewRootImpl是DecorView的parent,用來控制View的各種事件,在handleResumeActivity方法中被建立。

requestLayout和invalidate

  • requestLayout方法是用來觸發繪製流程,他會會一層層呼叫 parent 的 requestLayout,一直到最上層也就是ViewRootImpl的requestLayout,這裡也就是判斷執行緒的地方了,最後會執行到performMeasure -> performLayout -> performDraw 三個繪製流程,也就是測量——佈局——繪製。
    @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();//執行繪製流程
        }
    }

其中performMeasure方法會執行到View的measure方法,用來測量大小。performLayout方法會執行到view的layout方法,用來計算位置。performDraw方法需要注意下,他會執行到view的draw方法,但是並不一定會進行繪製,只有只有 flag 被設定為 PFLAG_DIRTY_OPAQUE 才會進行繪製。

  • invalidate方法也是用來觸發繪製流程,主要表現就是會呼叫draw()方法。雖然他也會走到scheduleTraversals方法,也就是會走到三大流程,但是View會通過mPrivateFlags來判斷是否進行onMeasure和onLayout操作。而在用invalidate方法時,更新了mPrivateFlags,所以不會進行measure和layout。同時他也會設定Flag為PFLAG_DIRTY_OPAQUE,所以肯定會執行onDraw方法。

private void invalidateRectOnScreen(Rect dirty) {
        final Rect localDirty = mDirty;
        //...
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            scheduleTraversals();//執行繪製流程
        }
    }

最後看一下scheduleTraversals方法中三大繪製流程邏輯,是不是我們之前說的那樣,FORCE_LAYOUT標誌才會onMeasure和onLayout,PFLAG_DIRTY_OPAQUE標誌才會onDraw:


  public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
    // 只有mPrivateFlags為PFLAG_FORCE_LAYOUT的時候才會進行onMeasure方法
    if (forceLayout || needsLayout) {
      onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    // 設定 LAYOUT_REQUIRED flag
    mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
  }


  public void layout(int l, int t, int r, int b) {
    ...
    //判斷標記位為PFLAG_LAYOUT_REQUIRED的時候才進行onLayout方法
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
    	}
	}



public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    // flag 是 PFLAG_DIRTY_OPAQUE 則需要繪製
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
    if (!dirtyOpaque) onDraw(canvas);
    // 繪製 Child
    dispatchDraw(canvas);
    // foreground 不管 dirtyOpaque 標誌,每次都會繪製
    onDrawForeground(canvas);
}	


參考文章中有一段總結挺好的:

雖然兩者都是用來觸發繪製流程,但是在measure和layout過程中,只會對 flag 設定為 FORCE_LAYOUT 的情況進行重新測量和佈局,而draw方法中只會重繪flag為 dirty 的區域。requestLayout 是用來設定FORCE_LAYOUT標誌,invalidate 用來設定 dirty 標誌。所以 requestLayout 只會觸發 measure 和 layout,invalidate 只會觸發 draw。

系統為什麼提供Handler

  • 這點大家應該都知道一些,就是為了切換執行緒,主要就是為了解決在子執行緒無法訪問UI的問題。

那麼為什麼系統不允許在子執行緒中訪問UI呢?

  • 因為Android的UI控制元件不是執行緒安全的,所以採用單執行緒模型來處理UI操作,通過Handler切換UI訪問的執行緒即可。

那麼為什麼不給UI控制元件加鎖呢?

  • 因為加鎖會讓UI訪問的邏輯變得複雜,而且會降低UI訪問的效率,阻塞執行緒執行。

Handler是怎麼獲取到當前執行緒的Looper的

  • 大家應該都知道Looper是繫結到執行緒上的,他的作用域就是執行緒,而且不同執行緒具有不同的Looper,也就是要從不同的執行緒取出執行緒中的Looper物件,這裡用到的就是ThreadLocal

假設我們不知道有這個類,如果要完成這樣一個需求,從不同的執行緒獲取執行緒中的Looper,是不是可以採用一個全域性物件,比如hashmap,用來儲存執行緒和對應的Looper?所以需要一個管理Looper的類,但是,執行緒中並不止這一個要儲存和獲取的資料,還有可能有其他的需求,也是跟執行緒所繫結的。所以,我們的系統就設計出了ThreadLocal這種工具類。

ThreadLocal的工作流程是這樣的:我們從不同的執行緒可以訪問同一個ThreadLocal的get方法,然後ThreadLocal會從各自的執行緒中取出一個陣列,然後再陣列中通過ThreadLocal的索引找出對應的value值。具體邏輯呢,我們還是看看程式碼,分別是ThreadLocal的get方法和set方法:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    } 
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }    
    
 	public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }    
    

首先看看set方法,獲取到當前執行緒,然後取出執行緒中的threadLocals變數,是一個ThreadLocalMap類,然後將當前的ThreadLocal作為key,要設定的值作為value存到這個map中。

get方法就同理了,還是獲取到當前執行緒,然後取出執行緒中的ThreadLocalMap例項,然後從中取到當前ThreadLocal對應的值。

其實可以看到,操作的物件都是執行緒中的ThreadLocalMap例項,也就是讀寫操作都只限制線上程內部,這也就是ThreadLocal故意設計的精妙之處了,他可以在不同的執行緒進行讀寫資料而且執行緒之間互不干擾。

畫個圖方便理解記憶:

ThreadLocal.PNG

當MessageQueue 沒有訊息的時候,在幹什麼,會佔用CPU資源嗎。

  • MessageQueue 沒有訊息時,便阻塞在 loop 的 queue.next() 方法這裡。具體就是會呼叫到nativePollOnce方法裡,最終呼叫到epoll_wait()進行阻塞等待。

這時,主執行緒會進行休眠狀態,也就不會消耗CPU資源。當下個訊息到達的時候,就會通過pipe管道寫入資料然後喚醒主執行緒進行工作。

這裡涉及到阻塞和喚醒的機制叫做 epoll 機制

先說說檔案描述符和I/O多路複用

在Linux作業系統中,可以將一切都看作是檔案,而檔案描述符簡稱fd,當程式開啟一個現有檔案或者建立一個新檔案時,核心向程式返回一個檔案描述符,可以理解為一個索引值。

I/O多路複用是一種機制,讓單個程式可以監視多個檔案描述符,一旦某個描述符就緒(一般是讀就緒或寫就緒),能夠通知程式進行相應的讀寫操作

所以I/O多路複用其實就是一種監聽讀寫的通知機制,而Linux提供的三種 IO 複用方式分別是:select、poll 和 epoll 。而這其中epoll是效能最好的多路I/O就緒通知方法。

所以,這裡用到的epoll其實就是一種I/O多路複用方式,用來監控多個檔案描述符的I/O事件。通過epoll_wait方法等待I/O事件,如果當前沒有可用的事件則阻塞呼叫執行緒。

Binder是什麼

先借用神書《Android開發藝術探索》中的一段話:

直觀的說,Binder是一個類,實現了IBinder介面。

從IPC(Inter-Process Communication,程式間通訊)角度來說,Binder是Android中一種跨程式通訊方式。

還可以理解為一種虛擬的物理裝置,它的裝置驅動是/dev/binder。

從Android FrameWork角度來說,Binder是ServiceManager連線各種Manager(ActivityManager,WindowManager等等)和響應ManagerService的橋樑。

從Android應用層來說,Binder是客戶端和服務端進行通訊的媒介。

挺多概念的是吧,其實就說了一件事,Binder就是用來程式間通訊的,是一種IPC方式。後面所有的解釋都是Binder實際應用涉及到的內容。

不管是獲取其他的系統服務,亦或是服務端和客戶端的通訊,都是源於Binder的程式間通訊能力。

Binder通訊過程和原理

首先,還是看一張圖,原圖也是出自神書中:

image

首先要明確的是客戶端程式是無法直接操作服務端中的類和方法的,因為不同程式直接是不共享資源的。所以客戶端這邊操作的只是服務端程式的一個代理物件,也就是一個服務端的類引用,也就是Binder引用。

總體通訊流程就是:

  • 客戶端通過代理物件向伺服器傳送請求。
  • 代理物件通過Binder驅動傳送到伺服器程式
  • 伺服器程式處理請求,並通過Binder驅動返回處理結果給代理物件
  • 代理物件將結果返回給客戶端。

再看看在我們應用中常常用到的工作模型,上圖:

image

這就是在應用層面我們常用的工作模型,通過ServiceManager去獲取各種系統程式服務。這裡的通訊過程如下:

  • 服務端跨程式的類都要繼承Binder類,所以也就是服務端對應的Binder實體。這個類並不是實際真實的遠端Binder物件,而是一個Binder引用(即服務端的類引用),會在Binder驅動裡還要做一次對映。
  • 客戶端要呼叫遠端物件函式時,只需把資料寫入到Parcel,在呼叫所持有的Binder引用的transact()函式
  • transact函式執行過程中會把引數、識別符號(標記遠端物件及其函式)等資料放入到Client的共享記憶體,Binder驅動從Client的共享記憶體中讀取資料,根據這些資料找到對應的遠端程式的共享記憶體。
  • 然後把資料拷貝到遠端程式的共享記憶體中,並通知遠端程式執行onTransact()函式,這個函式也是屬於Binder類。
  • 遠端程式Binder物件執行完成後,將得到的寫入自己的共享記憶體中,Binder驅動再將遠端程式的共享記憶體資料拷貝到客戶端的共享記憶體,並喚醒客戶端執行緒。

所以通訊過程中比較重要的就是這個服務端的Binder引用,通過它來找到服務端並與之完成通訊。

看到這裡可能有的人疑惑了,圖中執行緒池怎麼沒用到啊?

  • 可以從第一張圖中看出,Binder執行緒池位於服務端,它的主要作用就是將每個業務模組的Binder請求統一轉發到遠端Servie中去執行,從而避免了重複建立Service的過程。也就是服務端只有一個,但是可以處理多個不同客戶端的Binder請求。

在Android中的應用

Binder在Android中的應用除了剛才的ServiceManager,你還想到了什麼呢?

  • 系統服務是用過getSystemService獲取的服務,內部也就是通過ServiceManager。例如四大元件的啟動排程等工作,就是通過Binder機制傳遞給ActivityManagerService,再反饋給Zygote。而我們自己平時應用中獲取服務也是通過getSystemService(getApplication().WINDOW_SERVICE)程式碼獲取。
  • AIDL(Android Interface definition language)。例如我們定義一個IServer.aidl檔案,aidl工具會自動生成一個IServer.java的java介面類(包含Stub,Proxy等內部類)。
  • 前臺程式通過bindService繫結後臺服務程式時,onServiceConnected(ComponentName name, IBinder service)傳回IBinder物件,並且可以通過IServer.Stub.asInterface(service)獲取IServer的內部類Proxy的物件,其實現了IServer介面。

Binder優勢

在Linux中,程式通訊的方式肯定不止Binder這一種,還有以下這些:

管道(Pipe)
訊號(Signal)
訊息佇列(Message)
共享記憶體(Share Memory)
套接字(Socket)
Binder

Binder在這之後主要有以下優點:

  • 效能高,效率高:傳統的IPC(套接字、管道、訊息佇列)需要拷貝兩次記憶體、Binder只需要拷貝一次記憶體、共享記憶體不需要拷貝記憶體。
  • 安全性好:接收方可以從資料包中獲取傳送發的程式Id和使用者Id,方便驗證傳送方的身份,其他IPC想要實驗只能夠主動存入,但是這有可能在傳送的過程中被修改。

熟悉Zygote的朋友可能知道,在fork()程式的時候,也就是向Zygote程式發出建立程式的訊息的時候,用到的程式間通訊方式就不是Binder了,而換成了Socket,這主要是因為fork不允許存在多執行緒,Binder通訊偏偏就是多執行緒。

所以具體的情況還是要去具體選擇合適的IPC方式。

講一下RecyclerView的快取機制,滑動10個,再滑回去,會有幾個執行onBindView。快取的是什麼?cachedView會執行onBindView嗎?

RecyclerView預取機制

這兩個問題都是關於快取的,我就一起說了。

1)首先說下RecycleView的快取結構:

Recycleview有四級快取,分別是mAttachedScrap(螢幕內),mCacheViews(螢幕外),mViewCacheExtension(自定義快取),mRecyclerPool(快取池)

  • mAttachedScrap(螢幕內),用於螢幕內itemview快速重用,不需要重新createView和bindView
  • mCacheViews(螢幕外),儲存最近移出螢幕的ViewHolder,包含資料和position資訊,複用時必須是相同位置的ViewHolder才能複用,應用場景在那些需要來回滑動的列表中,當往回滑動時,能直接複用ViewHolder資料,不需要重新bindView。
  • mViewCacheExtension(自定義快取),不直接使用,需要使用者自定義實現,預設不實現。
  • mRecyclerPool(快取池),當cacheView滿了後或者adapter被更換,將cacheView中移出的ViewHolder放到Pool中,放之前會把ViewHolder資料清除掉,所以複用時需要重新bindView。

2)四級快取按照順序需要依次讀取。所以完整快取流程是:

  1. 儲存快取流程:
  • 插入或是刪除itemView時,先把螢幕內的ViewHolder儲存至AttachedScrap
  • 滑動螢幕的時候,先消失的itemview會儲存到CacheView,CacheView大小預設是2,超過數量的話按照先入先出原則,移出頭部的itemview儲存到RecyclerPool快取池(如果有自定義快取就會儲存到自定義快取裡),RecyclerPool快取池會按照itemview的itemtype進行儲存,每個itemType快取個數為5個,超過就會被回收。
  1. 獲取快取流程:
  • AttachedScrap中獲取,通過pos匹配holder——>獲取失敗,從CacheView中獲取,也是通過pos獲取holder快取
    ——>獲取失敗,從自定義快取中獲取快取——>獲取失敗,從mRecyclerPool中獲取
    ——>獲取失敗,重新建立viewholder——createViewHolder並bindview。

3)瞭解了快取結構和快取流程,我們再來看看具體的問題
滑動10個,再滑回去,會有幾個執行onBindView?

  • 由之前的快取結構可知,需要重新執行onBindView的只有一種快取區,就是快取池mRecyclerPool

所以我們假設從載入RecyclView開始盤的話(頁面假設可以容納7條資料):

  • 首先,7條資料會依次呼叫onCreateViewHolderonBindViewHolder
  • 往下滑一條(position=7),那麼會把position=0的資料放到mCacheViews中。此時mCacheViews快取區數量為1,mRecyclerPool數量為0。然後新出現的position=7的資料通過postion在mCacheViews中找不到對應的ViewHolder,通過itemtype也在mRecyclerPool中找不到對應的資料,所以會呼叫onCreateViewHolderonBindViewHolder方法。
  • 再往下滑一條資料(position=8),如上。
  • 再往下滑一條資料(position=9),position=2的資料會放到mCacheViews中,但是由於mCacheViews快取區預設容量為2,所以position=0的資料會被清空資料然後放到mRecyclerPool快取池中。而新出現的position=9資料由於在mRecyclerPool中還是找不到相應type的ViewHolder,所以還是會走onCreateViewHolderonBindViewHolder方法。所以此時mCacheViews快取區數量為2,mRecyclerPool數量為1。
  • 再往下滑一條資料(position=10),這時候由於可以在mRecyclerPool中找到相同viewtype的ViewHolder了。所以就直接複用了,並呼叫onBindViewHolder方法繫結資料。
  • 後面依次類推,剛消失的兩條資料會被放到mCacheViews中,再出現的時候是不會呼叫onBindViewHolder方法,而複用的第三條資料是從mRecyclerPool中取得,就會呼叫onBindViewHolder方法了。

4)所以這個問題就得出結論了(假設mCacheViews容量為預設值2):

  • 如果一開始滑動的是新資料,那麼滑動10個,就會走10個bindview方法。然後滑回去,會走10-2個bindview方法。一共18次呼叫。

  • 如果一開始滑動的是老資料,那麼滑動10-2個,就會走8個bindview方法。然後滑回去,會走10-2個bindview方法。一共16次呼叫。

但是但是,實際情況又有點不一樣。因為Recycleview在v25版本引入了一個新的機制,預取機制

預取機制,就是在滑動過程中,會把將要展示的一個元素提前快取到mCachedViews中,所以滑動10個元素的時候,第11個元素也會被建立,也就多走了一次bindview方法。但是滑回去的時候不影響,因為就算提前取了一個快取資料,只是把bindview方法提前了,並不影響總的繫結item數量。

所以滑動的是新資料的情況下就會多一次呼叫bindview方法。

5)總結,問題怎麼答呢?

  • 四級快取和流程說一下。
  • 滑動10個,再滑回去,bindview可以是19次呼叫,可以是16次呼叫。
  • 快取的其實就是快取item的view,在Recycleview中就是viewholder
  • cachedView就是mCacheViews快取區中的view,是不需要重新繫結資料的。

如何實現RecyclerView的區域性更新,用過payload嗎,notifyItemChange方法中的引數?

關於RecycleView的資料更新,主要有以下幾個方法:

  • notifyDataSetChanged(),重新整理全部可見的item。
    *notifyItemChanged(int),重新整理指定item。
  • notifyItemRangeChanged(int,int),從指定位置開始重新整理指定個item。
  • notifyItemInserted(int)、notifyItemMoved(int)、notifyItemRemoved(int)。插入、移動一個並自動重新整理。
  • notifyItemChanged(int, Object),區域性重新整理。

可以看到,關於view的區域性重新整理就是notifyItemChanged(int, Object)方法,下面具體說說:

notifyItemChange有兩個構造方法:

  • notifyItemChanged(int position, @Nullable Object payload)
  • notifyItemChanged(int position)

其中payload引數可以認為是你要重新整理的一個標示,比如我有時候只想重新整理itemView中的textview,有時候只想重新整理imageview?又或者我只想某一個view的文字顏色進行高亮設定?那麼我就可以通過payload引數來標示這個特殊的需求了。

具體怎麼做呢?比如我呼叫了notifyItemChanged(14,"changeColor"),那麼在onBindViewHolder回撥方法中做下判斷即可:

    @Override
    public void onBindViewHolder(ViewHolderholder, int position, List<Object> payloads) {
        if (payloads.isEmpty()) {
            // payloads為空,說明是更新整個ViewHolder
            onBindViewHolder(holder, position);
        } else {
            // payloads不為空,這隻更新需要更新的View即可。
            String payload = payloads.get(0).toString();
            if ("changeColor".equals(payload)) {
                holder.textView.setTextColor("");
            }
        }
    }

RecyclerView巢狀RecyclerView滑動衝突,NestScrollView巢狀RecyclerView。

1)RecyclerView巢狀RecyclerView的情況下,如果兩者都要上下滑動,那麼就會引起滑動衝突。預設情況下外層的RecycleView可滑,內層不可滑。

之前說過解決滑動衝突的辦法有兩種:內部攔截法和外部攔截法
這裡我提供一種內部攔截法,還有一些其他的辦法大家可以自己思考下。

   holder.recyclerView.setOnTouchListener { v, event ->
            when(event.action){
                //當按下操作的時候,就通知父view不要攔截,拿起操作就設定可以攔截,正常走父view的滑動。
                MotionEvent.ACTION_DOWN,MotionEvent.ACTION_MOVE -> v.parent.requestDisallowInterceptTouchEvent(true)
                MotionEvent.ACTION_UP -> v.parent.requestDisallowInterceptTouchEvent(false)
            }
            false}

2)關於ScrclerView的滑動衝突還是同樣的解決辦法,就是進行事件攔截。
還有一個辦法就是用Nestedscrollview代替ScrollViewNestedscrollview是官方為了解決滑動衝突問題而設計的新的View。它的定義就是支援巢狀滑動的ScrollView。

所以直接替換成Nestedscrollview就能保證兩者都能正常滑動了。但是要注意設定RecyclerView.setNestedScrollingEnabled(false)這個方法,用來取消RecyclerView本身的滑動效果。

這是因為RecyclerView預設是setNestedScrollingEnabled(true),這個方法的含義是支援巢狀滾動的。也就是說當它巢狀在NestedScrollView中時,預設會隨著NestedScrollView滾動而滾動,放棄了自己的滾動。所以給我們的感覺就是滯留、卡頓。所以我們將它設定為false就解決了卡頓問題,讓他正常的滑動,不受外部影響。

參考

https://www.jianshu.com/p/1dab927b2f36
https://juejin.im/post/6844903748574117901
https://juejin.im/post/6844903729414537223
https://blog.csdn.net/quwei3930921/article/details/85336552
https://www.jianshu.com/p/aac6fcfae1e8
https://mp.weixin.qq.com/s/wy9V4wXUoEFZ6ekzuLJySQ
https://www.cnblogs.com/hustcser/p/10228843.html

拜拜

為了新朋友,老朋友方便檢視,我把面試題《思考與解答》以往期刊整理成PDF了。大家到公.眾.號主頁回覆訊息"111"即可獲得下載連結
有一起學習的小夥伴可以關注下❤️我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識。

相關文章