日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

安卓面試題到處攢,一到面試就忘個干凈?來看看這份超詳細的整理

 

穩住,今天周一,長文面試題奉上。
更多BAT面試解析資料包內容私信我【資料】查看

1、說說View/ViewGroup的繪制流程

View的繪制流程是從ViewRoot的performTraversals開始的,它經過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),繪制裝飾,如 滾動指示器、滾動條、和前景.

2、說說你理解的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方法存儲本身的總寬高。

3、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();
    }
}

4、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讀寫數據。

5、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);
    }
}

4)其實這個線程池中只有一個線程,是用來清理連接的,也就是上述的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分鐘,則將該連接清理掉。

5)這里有個問題,怎樣屬于空閑連接?

其實就是有關剛才說到的一個方法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添加進該列表中,如果連接關閉以后就將該對象移除。

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

6、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);
      }
    }
  }
}

觀察者模式

由于webSocket屬于長連接,所以需要進行監聽,這里是用到了觀察者模式:

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

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

7、介紹一下你們之前做的項目的架構

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

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層把view和Controller區分開,單獨抽出來一層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的交互。也基本解決了上面兩個架構的問題。

8、具體說說你理解的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架構,其實也是官方想進行項目架構的統一。

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

..............

12、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")

13、LiveData 為什么被設計出來,解決了什么問題?

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

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

  • 不會發生內存泄漏 觀察者會綁定到 Lifecycle對象,并在其關聯的生命周期遭到銷毀后進行自我清理。
  • 不會因 Activity 停止而導致崩潰 如果觀察者的生命周期處于非活躍狀態(如返回棧中的 Activity),則它不會接收任何 LiveData 事件。
  • 自動判斷生命周期并回調方法 如果觀察者的生命周期處于 STARTED 或 RESUMED狀態,則 LiveData 會認為該觀察者處于活躍狀態,就會調用onActive方法,否則,如果 LiveData 對象沒有任何活躍觀察者時,會調用 onInactive()方法。

14、說說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方法即可。

15、說說DNS,以及存在的問題

DNS用來做域名解析工作的,當輸入一個域名后,需要把域名轉化為IP地址,這個轉換過程就是DNS解析。

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

  • 域名緩存問題本地做一個緩存,直接返回緩存數據。可能會導致全局負載均衡失敗,因為上次進行的緩存,不一定是這次離客戶最近的地方,可能會繞遠路。
  • 域名轉發問題如果是A運營商將解析的請求轉發給B運營商,B去權威DNS服務器查詢的話,權威服務器會認為你是B運營商的,就返回了B運營商的網站地址,結果每次都會跨運營商。
  • 出口NAT問題做了網絡地址轉化后,權威的DNS服務器,沒法通過地址來判斷客戶到底是哪個運營商,極有可能誤判運營商,導致跨運營商訪問。
  • 域名更新問題本地DNS服務器是由不同地區,不同運營商獨立部署的,對域名解析緩存的處理上,有區別,有的會偷懶忽略解析結果TTL的時間限制,導致服務器沒有更新新的ip而是指向舊的ip。
  • 解析延遲DNS的查詢過程需要遞歸遍歷多個DNS服務器,才能獲得最終結果。可能會帶來一定的延時。
  • 域名劫持DNS域名解析服務器有可能會被劫持,或者被偽造,那么正常的訪問就會被解析到錯誤的地址。
  • 不可靠由于DNS解析是運行在UDP協議之上的,而UDP我之前也說過是一種不可靠的協議,他的優勢在于實時性,但是有丟包的可能。

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

16、怎么優化DNS解析

安全優化

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

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

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

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

@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);
}

速度優化

如果在測試環境,其實我們可以直接配置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);
        }
    }
}

...................

19、Activity從創建到我們看到界面,發生了哪些事

首先是通過setContentView加載布局,這其中創建了一個DecorView,然后根據然后根據activity設置的主題(theme)或者特征(Feature)加載不同的根布局文件,最后再通過inflate方法加載layoutResID資源文件,其實就是解析了xml文件,根據節點生成了View對象。流程圖:

安卓面試題到處攢,一到面試就忘個干凈?來看看這份超詳細的整理

 

其次就是進行view繪制到界面上,這個過程發生在handleResumeActivity方法中,也就是觸發onResume的方法。在這里會創建一個ViewRootImpl對象,作為DecorView的parent然后對DecorView進行測量布局和繪制三大流程。流程圖:

安卓面試題到處攢,一到面試就忘個干凈?來看看這份超詳細的整理

 

20、Activity、PhoneWindow、DecorView、ViewRootImpl 的關系?

  • PhoneWindow是Window 的唯一子類,每個Activity都會創建一個PhoneWindow對象,你可以理解它為一個窗口,但不是真正的可視窗口,而是一個管理類,是Activity和整個View系統交互的接口,是Activity和View交互系統的中間層。
  • DecorView是PhoneWindow的一個內部類,是整個View層級的最頂層,一般包括標題欄和內容欄兩部分,會根據不同的主題特性調整不同的布局。它是在setContentView方法中被創建,具體點來說是在PhoneWindow的installDecor方法中被創建。
  • ViewRootImpl是DecorView的parent,用來控制View的各種事件,在handleResumeActivity方法中被創建。

........................

22、系統為什么提供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方法:

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

首先看看set方法,獲取到當前線程,然后取出線程中的threadLocals變量,是一個ThreadLocalMap類,然后將當前的ThreadLocal作為key,要設置的值作為value存到這個map中。

get方法就同理了,還是獲取到當前線程,然后取出線程中的ThreadLocalMap實例,然后從中取到當前ThreadLocal對應的值。

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

畫個圖方便理解記憶:

安卓面試題到處攢,一到面試就忘個干凈?來看看這份超詳細的整理

 

當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事件,如果當前沒有可用的事件則阻塞調用線程。

23、Binder通信過程和原理

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

安卓面試題到處攢,一到面試就忘個干凈?來看看這份超詳細的整理

 

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

總體通信流程就是:

  • 客戶端通過代理對象向服務器發送請求。
  • 代理對象通過Binder驅動發送到服務器進程
  • 服務器進程處理請求,并通過Binder驅動返回處理結果給代理對象
  • 代理對象將結果返回給客戶端。

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

安卓面試題到處攢,一到面試就忘個干凈?來看看這份超詳細的整理

 

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

  • 服務端跨進程的類都要繼承Binder類,所以也就是服務端對應的Binder實體。這個類并不是實際真實的遠程Binder對象,而是一個Binder引用(即服務端的類引用),會在Binder驅動里還要做一次映射。
  • 客戶端要調用遠程對象函數時,只需把數據寫入到Parcel,在調用所持有的Binder引用的transact()函數
  • transact函數執行過程中會把參數、標識符(標記遠程對象及其函數)等數據放入到Client的共享內存,Binder驅動從Client的共享內存中讀取數據,根據這些數據找到對應的遠程進程的共享內存。
  • 然后把數據拷貝到遠程進程的共享內存中,并通知遠程進程執行onTransact()函數,這個函數也是屬于Binder類。
  • 遠程進程Binder對象執行完成后,將得到的寫入自己的共享內存中,Binder驅動再將遠程進程的共享內存數據拷貝到客戶端的共享內存,并喚醒客戶端線程。

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

看到這里可能有的人疑惑了,圖中線程池怎么沒用到啊?

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

......................

系統的面試復習路線

多余的話就不講了,接下來將分享我之前面試的復習過程,如果你也在準備面試但是不知道怎么高效復習,可以參考一下我的復習路線,有任何問題也歡迎一起互相交流,加油吧!

這里給大家提供一個方向,進行體系化的學習:

1、看視頻進行系統學習

這幾年的Crud經歷,讓我明白自己真的算是菜雞中的戰斗機,也正因為Crud,導致自己技術比較零散,也不夠深入不夠系統,所以重新進行學習是很有必要的。我差的是系統知識,差的結構框架和思路,所以通過視頻來學習,效果更好,也更全面。關于視頻學習,個人可以推薦去B站進行學習,B站上有很多學習視頻,唯一的缺點就是免費的容易過時。

另外,我自己也珍藏了好幾套視頻,有需要的我也可以分享給你。

2、進行系統梳理知識,提升儲備

客戶端開發的知識點就那么多,面試問來問去還是那么點東西。所以面試沒有其他的訣竅,只看你對這些知識點準備的充分程度。so,出去面試時先看看自己復習到了哪個階段就好。

系統學習方向:

  • 架構師筑基必備技能:深入Java泛型+注解深入淺出+并發編程+數據傳輸與序列化+Java虛擬機原理+反射與類加載+動態代理+高效IO
  • Android高級UI與FrameWork源碼:高級UI晉升+Framework內核解析+Android組件內核+數據持久化
  • 360°全方面性能調優:設計思想與代碼質量優化+程序性能優化+開發效率優化
  • 解讀開源框架設計思想:熱修復設計+插件化框架解讀+組件化框架設計+圖片加載框架+網絡訪問框架設計+RXJava響應式編程框架設計+IOC架構設計+Android架構組件Jetpack
  • NDK模塊開發:NDK基礎知識體系+底層圖片處理+音視頻開發
  • 微信小程序:小程序介紹+UI開發+API操作+微信對接
  • Hybrid 開發與Flutter:html5項目實戰+Flutter進階
安卓面試題到處攢,一到面試就忘個干凈?來看看這份超詳細的整理

 

知識梳理完之后,就需要進行查漏補缺,所以針對這些知識點,我手頭上也準備了不少的電子書和筆記,這些筆記將各個知識點進行了完美的總結。

3、讀源碼,看實戰筆記,學習大神思路

“編程語言是程序員的表達的方式,而架構是程序員對世界的認知”。所以,程序員要想快速認知并學習架構,讀源碼是必不可少的。閱讀源碼,是解決問題 + 理解事物,更重要的:看到源碼背后的想法;程序員說:讀萬行源碼,行萬種實踐。

主要內含微信 MMKV 源碼、AsyncTask 源碼、Volley 源碼、Retrofit源碼、OkHttp 源碼等等。

安卓面試題到處攢,一到面試就忘個干凈?來看看這份超詳細的整理

 

4、面試前夕,刷題沖刺

面試的前一周時間內,就可以開始刷題沖刺了。請記住,刷題的時候,技術的優先,算法的看些基本的,比如排序等即可,而智力題,除非是校招,否則一般不怎么會問。

關于面試刷題,我個人也準備了一套系統的面試題,幫助你舉一反三:

安卓面試題到處攢,一到面試就忘個干凈?來看看這份超詳細的整理

 

總結

改變人生,沒有什么捷徑可言,這條路需要自己親自去走一走,只有深入思考,不斷反思總結,保持學習的熱情,一步一步構建自己完整的知識體系,才是最終的制勝之道,也是程序員應該承擔的使命。

分享到:
標簽:面試
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定