android面試——開源框架的原始碼解析
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的過程如下所示:
1. 遍歷Deque中所有的RealConnection,標記洩漏的連線
2. 如果被標記的連線滿足(空閒socket連線超過5個&&keepalive時間大於5分鐘),就將此連線從Deque中移除,並關閉連線,返回0,也就是將要執行wait(0),提醒立刻再次掃描
3. 如果(目前還可以塞得下5個連線,但是有可能洩漏的連線(即空閒時間即將達到5分鐘)),就返回此連線即將到期的剩餘時間,供下次清理
4. 如果(全部都是活躍的連線),就返回預設的keep-alive時間,也就是5分鐘後再執行清理
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>() {
@Override
public 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() 起作用。
相關文章
- Android 面試開源框架篇Android面試框架
- Android開源框架原始碼鑑賞:VirtualAPKAndroid框架原始碼APK
- Android開源框架原始碼鑑賞:EventBusAndroid框架原始碼
- android經典原始碼,很不錯的開源框架Android原始碼框架
- Android 網路框架 Retrofit 原始碼解析Android框架原始碼
- Android開源原始碼分析Android原始碼
- Android八門神器(一):OkHttp框架原始碼解析AndroidHTTP框架原始碼
- 面試必備:SparseArray原始碼解析面試原始碼
- Android 原始碼分析之 EventBus 的原始碼解析Android原始碼
- Java面試寶典之開源框架!Java面試框架
- Android開發社招面試經驗:深入解析android核心元件和應用框架,3面直接拿到offerAndroid面試元件框架
- Android Retrofit原始碼解析Android原始碼
- Android原始碼解析-LiveDataAndroid原始碼LiveData
- Android setContentView原始碼解析AndroidView原始碼
- Android Handler 原始碼解析Android原始碼
- Android——LruCache原始碼解析Android原始碼
- OkHttp 開源庫使用與原始碼解析HTTP原始碼
- ARouter路由框架原始碼解析路由框架原始碼
- weex eros框架原始碼解析ROS框架原始碼
- Android 8.1 Handler 原始碼解析Android原始碼
- Android LayoutInflater Factory 原始碼解析Android原始碼
- [Android] Retrofit原始碼:流程解析Android原始碼
- 【移動開發】Checkout開源庫原始碼解析移動開發原始碼
- 直播app原始碼開源,Android 滾動的公告欄APP原始碼Android
- 迷你 JS 框架 Hyperapp 原始碼解析JS框架APP原始碼
- Android AccessibilityService機制原始碼解析Android原始碼
- Android View 原始碼解析(一) - setContentViewAndroidView原始碼
- Android中使用AndroidTagGroup開源框架Android框架
- 阿里開源 iOS 協程開發框架 coobjc原始碼分析阿里iOS框架OBJ原始碼
- React Native 0.55.4 Android 原始碼分析(Java層原始碼解析)React NativeAndroid原始碼Java
- Android原始碼完全解析——View的Measure過程Android原始碼View
- Andriod 網路框架 OkHttp 原始碼解析框架HTTP原始碼
- Android面試複習之View事件體系(原始碼分析)Android面試View事件原始碼
- Android系統原始碼目錄解析Android原始碼
- weex原始碼解析(四)- android引入sdk原始碼Android
- Android開發實戰講解!GitHub上標星13k的《Android面試突擊版》,面試真題解析AndroidGithub面試
- 美團外賣開源路由框架 WMRouter 原始碼分析路由框架原始碼
- Android示例應用:開源框架Glide的使用Android框架IDE