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、說說你理解的MeasureSpecMeasureSpec是由父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);@Overridepublic 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);}@Overridepublic 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連線,以及負責編碼解碼的HttpCodecnetworkInterceptors,這裡也是開發者自己設定的,所以本質上和第一個攔截器差不多,但是由於位置不同,所以用處也不同。這個位置新增的攔截器可以看到請求和響應的資料了,所以可以做一些網路除錯。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、具體說說你理解的MVVM1)先說說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方法。透過該方法把訂閱者和被觀察者關聯起來,形成觀察者模式。
簡單看看原始碼:
@MainThreadpublic 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看起:
@MainThreadprotected 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介面即可,簡單寫個例子:
@MainThreadprotected 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虛擬機器原理+反射與類載入+動態代理+高效IOAndroid高階UI與FrameWork原始碼:高階UI晉升+Framework核心解析+Android元件核心+資料持久化360°全方面效能調優:設計思想與程式碼質量最佳化+程式效能最佳化+開發效率最佳化解讀開源框架設計思想:熱修復設計+外掛化框架解讀+元件化框架設計+圖片載入框架+網路訪問框架設計+RXJava響應式程式設計框架設計+IOC架構設計+Android架構元件JetpackNDK模組開發:NDK基礎知識體系+底層圖片處理+音影片開發微信小程式:小程式介紹+UI開發+API操作+微信對接Hybrid 開發與Flutter:Html5專案實戰+Flutter進階知識梳理完之後,就需要進行查漏補缺,所以針對這些知識點,我手頭上也準備了不少的電子書和筆記,這些筆記將各個知識點進行了完美的總結。
3、讀原始碼,看實戰筆記,學習大神思路
“程式語言是程式設計師的表達的方式,而架構是程式設計師對世界的認知”。所以,程式設計師要想快速認知並學習架構,讀原始碼是必不可少的。閱讀原始碼,是解決問題 + 理解事物,更重要的:看到原始碼背後的想法;程式設計師說:讀萬行原始碼,行萬種實踐。
4、面試前夕,刷題衝刺
面試的前一週時間內,就可以開始刷題衝刺了。請記住,刷題的時候,技術的優先,演算法的看些基本的,比如排序等即可,而智力題,除非是校招,否則一般不怎麼會問。
關於面試刷題,我個人也準備了一套系統的面試題,幫助你舉一反三:
總結改變人生,沒有什麼捷徑可言,這條路需要自己親自去走一走,只有深入思考,不斷反思總結,保持學習的熱情,一步一步構建自己完整的知識體系,才是最終的制勝之道,也是程式設計師應該承擔的使命。