Android主流框架面試要點(一)
Android面試經常問到的就是一些主流開源框架,但是一般一個框架可以說的特色部分不多,要抓住要點說,所以特地整理了一些常見框架的要點,整理於2017年中旬,可能與現在的版本有所出入。如果要開始看相關源碼的同學可以作為一個切入點來看,如有錯誤請及時指出,水平有限,敬請諒解。
第一次整理了四個主流框架:(1)EventBus(2)OkHttp(3)Retrofit(4)RxJava
1、EventBus
(1)通過註解+反射來進行方法的獲取
註解的使用:@Retention(RetentionPolicy.RUNTIME)表示此註解在運行期可知,否則使用CLASS或者SOURCE在運行期間會被丟棄。
通過反射來獲取類和方法:因為映射關係實際上是類映射到所有此類的對象的方法上的,所以應該通過反射來獲取類以及被註解過的方法,並且將方法和對象保存為一個調用實體。
(2)使用ConcurrentHashMap來保存映射關係
調用實體的構建:調用實體中對於Object,也就是實際執行方法的對象不應該使用強引用而是應該使用弱引用,因為Map的static的,生命周期有可能長於被調用的對象,如果使用強引用就會出現內存泄漏的問題。
(3)方法的執行
使用Dispatcher進行方法的分派,非同步則使用線程池來處理,同步就直接執行,而UI線程則使用MainLooper創建一個Handler,投遞到主線程中去執行。
2、Okhttp
(1)任務隊列
Okhttp使用了一個線程池來進行非同步網路任務的真正執行,而對於任務的管理採用了任務隊列的模型來對任務執行進行相應的管理,有點類似伺服器的反向代理模型。Okhttp使用分發器Dispatcher來維護一個正在運行任務隊列和一個等待隊列。如果當前並發任務數量小於64,就放入執行隊列中並且放入線程池中執行。而如果當前並發數量大於64就放入等待隊列中,在每次有任務執行完成之後就在finally塊中調用分發器的finish函數,在等待隊列中查看是否有空餘任務,如果有就進行入隊執行。Okhttp就是使用任務隊列的模型來進行任務的執行和調度的。
(2)復用連接池
Http使用的TCP連接有長連接和短連接之分,對於訪問某個伺服器的頻繁通信,使用短連接勢必會造成在建立連接上大量的時間消耗;而長連接的長時間無用保持又會造成資源你的浪費。Okhttp底層是採用Socket建立流連接,而連接如果不手動close掉,就會造成內存泄漏,那我們使用Okhttp時也沒有做close操作,其實是Okhttp自己來進行連接池的維護的。在Okhttp中,它使用類似引用計數的方式來進行連接的管理,這裡的計數對象是StreamAllocation,它被反覆執行aquire與release操作,這兩個函數其實是在改變Connection中的List<WeakReference<StreamAllocation>>大小。List中Allocation的數量也就是物理socket被引用的計數(Refference Count),如果計數為0的話,說明此連接沒有被使用,是空閑的,需要通過淘汰演算法實現回收。
在連接池內部維護了一個線程池,這個線程池運行的cleanupRunnable實際上是一個阻塞的runnable,內部有一個無限循環,在清理完成之後調用wait進行等待,等待的時間由cleanup的返回值決定,在等待時間到了之後再進行清理任務。
while (true) {//執行清理並返回下場需要清理的時間long waitNanos = cleanup(System.nanoTime());if (waitNanos == -1) return;if (waitNanos > 0) { synchronized (ConnectionPool.this) { try { //在timeout內釋放鎖與時間片 ConnectionPool.this.wait(TimeUnit.NANOSECONDS.toMillis(waitNanos)); } catch (InterruptedException ignored) { } }}}
Cleanup的過程如下所示:
- 遍歷Deque中所有的RealConnection,標記泄漏的連接
- 如果被標記的連接滿足(空閑socket連接超過5個&&keepalive時間大於5分鐘),就將此連接從Deque中移除,並關閉連接,返回0,也就是將要執行wait(0),提醒立刻再次掃描
- 如果(目前還可以塞得下5個連接,但是有可能泄漏的連接(即空閑時間即將達到5分鐘)),就返回此連接即將到期的剩餘時間,供下次清理
- 如果(全部都是活躍的連接),就返回默認的keep-alive時間,也就是5分鐘後再執行清理
- 如果(沒有任何連接),就返回-1,跳出清理的死循環
再次注意:這裡的「並發」==(「空閑」+「活躍」)==5,而不是說並發連接就一定是活躍的連接
如何標記空閑的連接呢?我們前面也說了,如果一個連接身上的引用為0,那麼就說明它是空閑的,那麼就要使用pruneAndGetAllocationCount來計算它身上的引用數,如同引用計數過程。
過程其實很簡單,就是遍歷它的List<Reference<StreamAllocation>>,刪除所有已經為null的弱引用,剩下的數量就是現在它的引用數量,如下段代碼所示。
//類似於引用計數法,如果引用全部為空,返回立刻清理private int pruneAndGetAllocationCount(RealConnection connection, long now) {//虛引用列表List<Reference<StreamAllocation>> references = connection.allocations;//遍歷弱引用列表for (int i = 0; i < references.size(); ) { Reference<StreamAllocation> reference = references.get(i); //如果正在被使用,跳過,接著循環 //是否置空是在上文`connectionBecameIdle`的`release`控制的 if (reference.get() != null) { //非常明顯的引用計數 i++; continue; } //否則移除引用 references.remove(i); connection.noNewStreams = true; //如果所有分配的流均沒了,標記為已經距離現在空閑了5分鐘 if (references.isEmpty()) { connection.idleAtNanos = now - keepAliveDurationNs; return 0; }}return references.size();}
3、Retrofit
(1)Retrofit中的動態代理
Java中的動態代理:
首先動態代理是區別於靜態代理的,代理模式中需要代理類和實際執行類同時實現一個相同的介面,並且在每個介面定義的方法前後都要加入相同的代碼,這樣有可能很多方法代理類都需要重複。而動態代理就是將這個步驟放入運行時的過程,一個代理類只需要實現InvocationHandler介面中的invoke方法,當需要動態代理時只需要根據介面和一個實現了InvocationHandler的代理對象A生成一個最終的自動生成的代理對象A*。這樣最終的代理對象A*無論調用什麼方法,都會執行InvocationHandler的代理對象A的invoke函數,你就可以在這個invoke函數中實現真正的代理邏輯。
動態代理的實現機制實際上就是使用Proxy.newProxyInstance函數為動態代理對象A生成一個代理對象A*的類的位元組碼從而生成具體A*對象過程,這個A*類具有幾個特點,一是它需要實現傳入的介面,第二就是所有介面的實現中都會調用A的invoke方法,並且傳入相應的調用實際方法(即介面中的方法)。
Retrofit中的動態代理
Retrofit中使用了動態代理是不錯,但是並不是為了真正的代理才使用的,它只是為了動態代理一個非常重要的功能,就是「攔截」功能。我們知道動態代理中自動生成的A*對象的所有方法執行都會調用實際代理類A中的invoke方法,再由我們在invoke中實現真正代理的邏輯,實際上也就是A*的所有方法都被A對象給攔截了。而Retrofit最重要的是什麼?就是把一個網路執行變成像方法調用一樣方便的過程:
public interface ZhuanLanApi { @GET("/api/columns/{user} ") Call<ZhuanLanAuthor> getAuthor(@Path("user") String user)}
再用這個retrofit對象創建一個ZhuanLanApi對象:
ZhuanLanApi api = retrofit.create(ZhuanLanApi.class);Call<ZhuanLanAuthor> call = api.getAuthor("qinchao");
也就是一個網路調用你只需要在你創建的介面裡面通過註解進行設置,然後通過retrofit創建一個api然後調用,就可以自動完成一個Okhttp的Call的創建。通過create的源碼的查看:
public <T> T create(final Class<T> service) {Utils.validateServiceInterface(service);if (validateEagerly) { eagerlyValidateMethods(service);}return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service }, new InvocationHandler() { private final Platform platform = Platform.get(); @Override public Object invoke(Object proxy, Method method, Object... args) throws Throwable { // If the method is a method from Object then defer to normal invocation. if (method.getDeclaringClass() == Object.class) { return method.invoke(this, args); } if (platform.isDefaultMethod(method)) { return platform.invokeDefaultMethod(method, service, proxy, args); } ServiceMethod serviceMethod = loadServiceMethod(method); OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args); return serviceMethod.callAdapter.adapt(okHttpCall); } });
我們可以看出怎麼從介面類創建成一個API對象?就是使用了動態代理中的攔截技術,通過創建一個符合此介面的動態代理對象A*,那A呢?就是這其中創建的這個匿名類了,它在內部實現了invoke函數,這樣A*調用的就是A中的invoke函數,也就是被攔截了,實際運行invoke。而invoke就是根據調用的method的註解(前面紅色標註的,會傳入相應實際函數),從而生成一個符合條件的Okhttp的Call對象,供你使用(調用進行真正網路請求)。
(2)Retrofit實際作用
Retrofit實際上是為了更方便的使用Okhttp,因為Okhttp的使用就是構建一個Call,而構建Call的大部分過程都是相似的,而Retrofit正是利用了代理機制帶我們動態的創建Call,而Call的創建信息就來自於你的註解。並且還可以根據配置Adapter等等對網路請求進行相應的處理和改變,這種插件式的解耦方式也提供了很大的擴展性。
4、RxJava
1、觀察者與被觀察者通信
(1)Observable的create函數
public final static <T> Observable<T> create(OnSubscribe<T> f) { return new Observable<T>(hook.onCreate(f));}
構造函數如下
protected Observable(OnSubscribe<T> f) { this.onSubscribe = f;}
創建了一個Observable我們記為Observable1,保存了傳入的OnSubscribe對象為onSubscribe,這個很重要,後面會說到。
(2)onSubscribe方法
public final Subscription subscribe(Subscriber<? super T> subscriber) { return Observable.subscribe(subscriber, this);}private static <T> Subscription subscribe(Subscriber<? super T> subscriber, Observable<T> observable) { ... subscriber.onStart(); onSubscribe.call(subscriber); return hook.onSubscribeReturn(subscriber);}
重點在加粗部分,實際上調用的就是之前我們傳入的onSubscribe的call方法,這樣就實現了被觀察者和觀察者之間的通信邏輯,運行我們寫好的call函數。
2、變換過程(lift)
(1)map函數
public final <R> Observable<R> map(Func1<? super T, ? extends R> func) { return lift(new OperatorMap<T, R>(func));}
map函數直接調用了lift函數並且把我們的func傳了進去,func就是我們所做的具體變換操作,我們看一下map平時使用的方式,加粗部分就是我們傳進去的call函數的實現:
Observable.from(students) .map(new Func1<Student, String>() { @Override public String call(Student student) { return student.getName(); } }).subscribe(subscriber);
而這裡的Subscriber,我們記為Subscriber1。
(2)lift函數
public <R> Observable<R> lift(Operator<? extends R, ? super T> operator) { return Observable.create(new OnSubscribe<R>() { @Overridepublic void call(Subscriber subscriber) {Subscriber newSubscriber = operator.call(subscriber); newSubscriber.onStart(); onSubscribe.call(newSubscriber); } });}
我們可以看到這裡我們又創建了一個新的Observable對象,我們記為Observable2,也就是說當我們執行map時,實際上返回了一個新的Observable對象,我們之後的subscribe函數實際上執行再我們新創建的Observable2上,這時他調用的就是我們新的call函數,也就是Observable2的call函數(加粗部分),我們來看一下這個operator的call的實現。這裡call傳入的就是我們的Subscriber1對象,也就是調用最終的subscribe的處理對象。
(3)Operator的call函數
public Subscriber<? super T> call(final Subscriber<? super R> o) { return new Subscriber<T>(o) { @Override public void onNext(T t) { o.onNext(transformer.call(t)); } };}
這裡的transformer就是我們在map調用是傳進去的func函數,也就是變換的具體過程。那看之後的onSubscribe.call(回到call中),這裡的onSubscribe是誰呢?就是我們Observable1保存的onSubscribe對象,也就是我們前面說很重要的那個對象。而這個o(又回來了)就是我們的Subscriber1,這裡可以看出,在調用了轉換函數之後我們還是調用了一開始的Subscriber1的onNext,最終事件經過轉換傳給了我們的結果。
(4)總結
圖上給出了直觀的結果,實際上我們通過lift創建了一個新的Observable對象,記為Observable2,我們之後的subscribe實際上執行在了它身上。它執行了之前Observable1的call函數,並且創建了一個新的Subscriber對象,記為Subscriber2,它的作用就是接受原來Observable1的事件,然後經過轉換,傳遞給最終的Subscriber1,執行它的onNext函數(這個邏輯在他的onNext中可以看出來)。我們這樣就跑通了變換的整個邏輯了,我們也可以發現這個邏輯類似於攔截,通過攔截subscribe函數,再把原始Observable的subscribe攔截到新的Subscriber2對象中來執行,從而實現轉換的邏輯。
3、線程切換過程(Scheduler)
RxJava最好用的特點就是提供了方便的線程切換,但它的原理歸根結底還是lift,使用subscribeOn()的原理就是創建一個新的Observable,把它的call過程開始的執行投遞到需要的線程中;而 observeOn() 則是把線程切換的邏輯放在自己創建的Subscriber中來執行。把對於最終的Subscriber1的執行過程投遞到需要的線程中來進行。
(1)區別
從圖中可以看出,subscribeOn() 和 observeOn() 都做了線程切換的工作(圖中的 "schedule..." 部位)。不同的是, subscribeOn()的線程切換髮生在 OnSubscribe 中,即在它通知上一級 OnSubscribe 時,這時事件還沒有開始發送,因此 subscribeOn() 的線程式控制制可以從事件發出的開端就造成影響;而 observeOn() 的線程切換則發生在它內建的 Subscriber 中,即發生在它即將給下一級 Subscriber 發送事件時,因此 observeOn() 控制的是它後面的線程。
(2)為什麼subscribeOn()只有第一個有效?
因為它是從通知開始將後面的執行全部投遞到需要的線程來執行,但是之後的投遞會受到在它的上級的(但是執行在它之後)的影響,如果上面還有subscribeOn() ,又會投遞到不同的線程中去,這樣就不受到它的控制了。所以只有第一個有效果:
圖中共有 5 處含有對事件的操作。由圖中可以看出,①和②兩處受第一個 subscribeOn() 影響,運行在紅色線程;③和④處受第一個 observeOn() 的影響,運行在綠色線程;⑤處受第二個 onserveOn() 影響,運行在紫色線程;而第二個 subscribeOn() ,由於在通知過程中線程就被第一個 subscribeOn() 截斷,因此對整個流程並沒有任何影響。這裡也就回答了前面的問題:當使用了多個 subscribeOn() 的時候,只有第一個 subscribeOn() 起作用。
推薦閱讀: