Xutils3.0技術分享
1.這個技術分享的目的
1.首先要讓大家了解Xutil3.0是什么
Xtuils3.0的前身是Xutils,是兩年前就很火的一個開源框架,包含了很多實用的Android開發工具,支持大文件的上傳以及下載,更加全面的Http請求協議支持(Get,Post,Delete等等),并且擁有更加靈活的ORM框架,更多的事件注解能夠讓你的代碼更加簡潔高效,目前之前的Xutils已經停止維護 所有的維護工作已經在Xutils3.0中繼續 值得注意的是 Xutils3.0最低兼容android4.0
2.Xutils3.0的基本使用方式
在這邊技術文檔里面 會介紹Xtuils3.0幾大模塊的基本使用方式:
1.如何發起一個Http請求
2.如何使用DB模塊實現數據庫的增刪改查
3.如何使用BitMapUtils實現網路圖片以及本地控件的綁定以及顯示
4.如何給控件設置各種事件(這里主要是通過注解的方式)
3.Xutils3.0的幾大模塊的介紹以及實現原理
大家通過這篇文章會對Xtuils3.0的四大模塊有一定的了解 并且會大致了解每個模塊的底層實現,這樣大家在使用的過程中基本上能夠做到心中有數
4.如何在項目中使用Xtuils3.0
當然我們學習框架的主要原因就是要在項目中去使用,通過這篇文檔大家就能夠了解Xtuils3.0的基本使用規則了,然后就大膽放心的去項目中實踐吧,因為理論結合實踐才能真正理解這個框架的設計原理以及精髓之處。
2.Xutils3.0的背景介紹
Xtutils的作者是 wyouflf 一個很牛的android開源工作者 ,目前Xutils的六個500人QQ群已經全部爆滿 可見這個框架的火熱程度以及大家對這個框架的關注 我們感謝作者讓我們有了避免重復造輪子的前提 對于進度要求很緊的團隊 真的可以直接拿來用 并且該框架的維護還是很及時的。目前由于Android系統的不斷更新 Xtuils框架也更新到了3.0版本 之前的版本已經不再維護 所以建議大家使用過的時候盡量使用最新的版本。
該開源框架的下載地址:https://github.com/wyouflf/xUtils3
3.我們選擇Xutils3.0的原因
我們為什么選擇Xtuis3.0呢?因為它里面包含的四大模塊基本上能夠解決我們開發中所遇到的問題了,比如我們Android開發中經常涉及到的就是請求網絡數據 加載網絡圖片 緩存本地數據庫 以及響應用戶事件等等 所有的框架設計的初衷基本上都是一致的 那就是封裝一些常見的操作 避免代碼的冗余以及程序結構的臃腫。當然也有很多一些其他的框架,比如Afinal ,AndroidOne等 也都很優秀我們選擇Xtuils的原因主要是因為下面幾個方面:
1.我們之前的程序中使用的是Xtuils1.0版本 通過實踐證明這個框架穩定性方面得到了我們的認可
2.Xtuils的入門成本比較低 主要是在于作者封裝的比較好,比如我們要請求一條網絡數據,一行代碼就可以了 這就省去了我們平時寫代碼的很多事情了,比如我們要設置請求頭
、設置請求參數以及請求方式 但是通過該框架我們只需要一句代碼 將必要的參數穿進去就OK了
3.Xtuils的更新速度快 基本上問題被拋出來后 作者以及團隊成員就會很快跟近并且更新版本。
基于以上幾點 我們選擇使用Xtuils框架 當然目前我們打算替換為最新的3.0版本
4.Xutils3.0的技術細節分解
4.1 Xutils3.0較之前的版本有了哪些改進
4.1.1 HTTP實現替換HttpClient為UrlConnection, 自動解析回調泛型, 更安全的斷點續傳策略.
4.1.2 支持標準的Cookie策略, 區分domain, path...
4.1.3 事件注解去除不常用的功能, 提高性能.
4.1.4 數據庫api簡化提高性能, 達到和greenDao一致的性能.
4.1.5 圖片綁定支持gif, webp; 支持圓角, 圓形, 方形等裁剪, 支持自動旋轉等等
4.2 Xutils3.0為什么最低兼容4.0
我們通過最新的2016年的Android版本分布狀況來看一下:
如果這個數據不夠直接的話 我們來看一下Umeng統計關于版本分布的情況吧
通過這兩組數據 大家覺得我們還有必要去維護4.0以下的版本嗎
這么低的活躍度甚至最新的Umeng統計已經沒有4.0一下的統計了, 那些2.X的版本要么是應用后臺自啟動, 要么都是各個軟件公司的測試機.
現在2.3的測試機都買不到了, 沒法保證上線的穩定性.
為兼容2.3話費巨大的人力和資源, 幾乎沒有回報, 不值得.
4.3 Xutils3.0能夠提供什么功能
這個其實之前我們已經提前介紹了,其實Xtuils3.0能提供的主要功能就是四大模塊對開發的支持 比如對HTTP請求 、圖片的處理、數據庫的簡化、事件處理的注解機制 等四大功能模塊
4.4 Xutils3.0幾大模塊介紹
4.4.1 DbUtils模塊
由于個人時間問題,這一模塊我就暫時不分析了 有興趣的同學可以根據其他模塊的邏輯進行自行分析 如果我有時間 會將這一塊補上去的。其實這一塊的大致邏輯 跟之前的Xutils我個人認為也不會改變太大 所以大家可以作為對Xutils3.0的深入認識的一次鍛煉 自己分析一下
4.4.2 ViewUtils模塊
其實這個模塊是基于注解實現的 我們首先來看下這個模塊能給我們帶來什么好處而吸引這么多人去使用它呢?
我們來做一個對比:
首先是我們傳統的寫法:
public void initView() {
2.mPager = (CustomViewPager) findViewById(R.id.home_viewPager);
3.paid_tab_ll = (LinearLayout) findViewById(R.id.paid_tab_ll);
4.good_tab_ll = (LinearLayout) findViewById(R.id.good_tab_ll);
5.user_tab_ll = (LinearLayout) findViewById(R.id.user_tab_ll);
6.user_tab_img = (ImageButton) findViewById(R.id.user_tab_img);
7.good_tab_img = (ImageButton) findViewById(R.id.good_tab_img);
8.paid_tab_img = (ImageButton) findViewById(R.id.paid_tab_img);
9.paid_tab_tv = (TextView) findViewById(R.id.paid_tab_tv);
10.good_tab_tv = (TextView) findViewById(R.id.good_tab_tv);
11.user_tab_tv = (TextView) findViewById(R.id.user_tab_tv);
13.}
這段代碼相信Android的小伙伴不會陌生其實就是針對我們在布局文件中書寫的控件的一些初始化操作來找到對應的組件
接下來我們來看下使用Xutils3.0之后我們的代碼的寫法:
@ViewInject(R.id.viewPager)
2.private CustomViewPager mPager 這里我們只寫一個就OK了 其他的類似
發現沒有我們不用再去重復的編寫findViewById了這一大長串的功能了 有對比才有有差距 如果讓你去選擇 ,你肯定也會傾向于使用第二種方式了 是吧 相信大家也猜到了 肯定是這個ViewInject注解里面做了些什么事情 而省去了我們重復編寫findviewById方法的麻煩 了解注解的人應該已經有所領悟了
那我們就來揭開這個神秘的面紗吧
1.private static void injectObject(Object handler, Class<?> handlerType, ViewFinder finder) {
if (handlerType == null || IGNORED.contains(handlerType)) {
return;
}
// 從父類到子類遞歸
injectObject(handler, handlerType.getSuperclass(), finder);
// inject view
Field[] fields = handlerType.getDeclaredFields();
if (fields != null && fields.length > 0) {
for (Field field : fields) {
Class<?> fieldType = field.getType();
if (
/* 不注入靜態字段 */ Modifier.isStatic(field.getModifiers()) ||
/* 不注入final字段 */ Modifier.isFinal(field.getModifiers()) ||
/* 不注入基本類型字段 */ fieldType.isPrimitive() ||
/* 不注入數組類型字段 */ fieldType.isArray()) {
continue;
}
ViewInject viewInject = field.getAnnotation(ViewInject.class);
if (viewInject != null) {
try {
View view = finder.findViewById(viewInject.value(), viewInject.parentId());
if (view != null) {
field.setAccessible(true);
field.set(handler, view);
} else {
throw new RuntimeException("Invalid @ViewInject for "
+ handlerType.getSimpleName() + "." + field.getName());
}
} catch (Throwable ex) {
LogUtil.e(ex.getMessage(), ex);
}
}
}
} // end inject view
怎么樣是不是看到了上面紅色標記的一行27行很熟悉啊 原來前端的簡潔 是因為后端已經幫我們處理了麻煩的查找邏輯了
但是我們要想使他生效 的話 必須要執行這一行代碼的
x.view().inject(holder, view);
這樣系統加載你這個類的時候才會去初始化你所設置的注解的值以及初始化工作
其實這個模塊就是要求你對反射以及注解有一個基本的認識和理解并且能夠在代碼中去使用他們 其實現在很多框架都是基于反射結合注解實現的
4.4.3 HTTP 模塊
首先我們來說一下Xutils3.0中關于Http模塊優秀于其他框架的原因
1.Xutils3.0支持大文件的上傳和下載 當然肯定是支持斷點續傳以及斷點下載的 這是現在上傳下載的必備功能了
2.Xutils3.0支持Http緩存和Cookie緩存 我們來看一下源碼中的表現吧
從這里看出我們的HttpCache以及Cookie都是通過數據庫 來進行緩存的 一般我們使用最多大概就是HttpCache了 這是在DBConfig這個類里面的 并且在構造LRUDiskCache的時候初始化的。
1.首先我們我們知道HTTP支持多種謂詞比如GET POST等等,Xutils支持11種謂詞 我們從其源碼中就可以看出 我們此處來看一下源碼中如何表示的:
public enum HttpMethod {
2.GET("GET"),
3.POST("POST"),
4.PUT("PUT"),
5.PATCH("PATCH"),
6.HEAD("HEAD"),
7.MOVE("MOVE"),
8.COPY("COPY"),
9.DELETE("DELETE"),
10.OPTIONS("OPTIONS"),
11.TRACE("TRACE"),
12.CONNECT("CONNECT");
}
我們可以看到 這11種謂詞是通過一個枚舉變量HttpMethod來進行表示的 說明Xutils支持這么多的請求方式。這里面我們就不一一介紹了,需要了解的朋友自行百度一下。
2.接下來我們就通過一個簡單的GET請求來看看通過Xtuils我們如何發送一個請求來請求服務器的某種資源
- Callback.Cancelable cancelable
= x.http().get(params,
new Callback.CommonCallback<List<BaiduResponse>>() {
4.@Override
5.public void onSuccess(List<BaiduResponse> result) {
6.Toast.makeText(x.app(), result.get(0).toString(), Toast.LENGTH_LONG).show();
7.}
9.@Override
10.public void onError(Throwable ex, boolean isOnCallback) {
11.Toast.makeText(x.app(), ex.getMessage(), Toast.LENGTH_LONG).show();
12.if (ex instanceof HttpException) { // 網絡錯誤
13.HttpException httpEx = (HttpException) ex;
14.int responseCode = httpEx.getCode();
15.String responseMsg = httpEx.getMessage();
16.String errorResult = httpEx.getResult();
17.// ...
18.} else { // 其他錯誤
19.// ...
20.}
21.}
23.@Override
24.public void onCancelled(CancelledException cex) {
25.Toast.makeText(x.app(), "cancelled", Toast.LENGTH_LONG).show();
26.}
28.@Override
29.public void onFinished() {
31.}
32.});
看上面的代碼 這就是一個發起一個GET請求所需要的代碼 是不是一句話就解決了 而且還帶有請求成功或者失敗的回調 是不是很強大呢 ,其實我們在代碼中當發起一個GET請求的時候 我們只需要這么做:
1.封裝一個請求參數Params
2.自定義回調函數的處理就OK了。
使用起來就是這么簡單 ,但是它底層的實現還是很復雜的 接下來我們就從源碼角度去分析作者的設計思路。 其實這里教大家一個分析源碼的方法 ,其實我們就將源碼導進AndroidStdio中 然后我們請求的入口開始 一步一步的去跟并且做下標記,這樣不至于層次太深之后頭腦混亂。
接下來我就帶大家去從x.http.get()這個入口函數中開始整個請求過程的分析:
首先:我們得知道get()方法是在哪個類里面定義的 通過源碼我們看到 x.http()返回的是一個HttpManagerImpl類實現的 是通過單例實現的,這個單例設計模式在Android源碼中也有用到過 這里就不對其做詳細解釋了 其實單例的寫法很多 目前我們經常見到的并且使用頻率很高的是靜態內部類寫法和雙重檢查加鎖機制來實現的加上關鍵字voliate修飾單例變量 由于JDK的不斷升級 目前這兩種寫法都算是比較安全并且穩定的寫法了。這里只貼下源碼即可:
- public static void registerInstance() {
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new HttpManagerImpl();
}
}
}
x.Ext.setHttpManager(instance);
- }
作者就是通過雙重檢查加鎖來實現單例的 ,我們來看下標準的雙重加鎖實現單例的標準寫法
雙重檢查鎖定看起來似乎很完美,但這是一個錯誤的優化!在線程執行到第4行代碼讀取到instance不為null時,instance引用的對象有可能還沒有完成初始化。
問題的根源
前面的雙重檢查鎖定示例代碼的第7行(instance = new Singleton();)創建一個對象。這一行代碼可以分解為如下的三行偽代碼:
memory = allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設置instance指向剛分配的內存地址
上面三行偽代碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的,詳情見參考文獻1的“Out-of-order writes”部分)。2和3之間重排序之后的執行時序如下:
memory = allocate(); //1:分配對象的內存空間
instance = memory; //3:設置instance指向剛分配的內存地址
//注意,此時對象還沒有被初始化!
ctorInstance(memory); //2:初始化對象
如果發生這種情況的話 ,那么就會出現一個線程引用了還沒有初始化的instance 這就是雙重加鎖問題的根源
那么其實上面的問題也很好解決 :我們只需要將instance聲明為voliate類型的就能避免重排序造成的隱患
這里我推薦一種更加優秀的解決方法
基于類初始化的解決方案
JVM在類的初始化階段(即在Class被加載后,且被線程使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化。
基于這個特性,可以實現另一種線程安全的延遲初始化方案(這個方案被稱之為Initialization On Demand Holder idiom):
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public static Instance getInstance() {
return InstanceHolder.instance ; //這里將導致InstanceHolder類被初始化
}
}
假設兩個線程并發執行getInstance(),下面是執行的示意圖:
好了 這里只是作為一個小插曲 給大家賣弄一下最簡單的設計模式 這并不是我們這次分享的主要目的 我們接下來繼續往下分析:
這里我們知道了x.http()返回的對象是HttpManagerImpl 那么我就去看看get方法的實現
@Override
public <T> Callback.Cancelable request(HttpMethod method, RequestParams entity, Callback.CommonCallback<T> callback) {
//設置請求方法為GET
entity.setMethod(method);
Callback.Cancelable cancelable = null;
if (callback instanceof Callback.Cancelable) {
cancelable = (Callback.Cancelable) callback;
}
// 構建一個HttpTask對象 然后后調用x.task().start()方法開啟請求
HttpTask<T> task = new HttpTask<T>(entity, cancelable, callback);
return x.task().start(task);
}
1.其實HttpTask的構造函數里面只是初始化了請求參數以及回調函數的設置 并且對線程池執行器進行初始化。我們看到 對于我們上面的請求 此時我們的執行器是// init executor
2.if (params.getExecutor() != null) {
3.this.executor = params.getExecutor();
4.} else {
5.if (cacheCallback != null) {
6.this.executor = CACHE_EXECUTOR;
7.} else {
8.this.executor = HTTP_EXECUTOR;
9.}
10.}
這個HTTP_EXECUTOR是個什么玩意呢 這里告訴大家 他的類型是PriorityExecutor
是一個支持優先級的一個線程執行器。
這樣將這個任務扔進TaskControllerImpl中進行執行。
我們將整個的請求框架流程圖畫一下:
這個只是整個主要的流程 當然內部還有很多的細節 ,這個我們就通過閱讀源碼去了解就可以了。
接下來我們通過大致的時序圖來帶領大家去熟悉一下整個的請求過程:
主要步驟分為5步:
1.調用x.http().get()發起請求 然后會得到HttpManagerImpl的一個實例 然后調用該類的request方法
2.在request方法中創建一個HttpTask對象并且內部確定了HttpTask的內部線程執行器默認是PriorityExecutor
3.調用TaskController的實現類的start將我們剛才創建的HttpTask傳遞過去 然后構建一個TaskProxy對象
4.調用TaskProxy對象的doBackGround方法
5.然后該方法內部調用HttpTask的doBackGround方法
6 最后將得到的結果更新到UI線程
其實這里面主要的邏輯就在第5步 我們如何調用HTTPTask對象的doBackground方法得到請求的結果 接下來我們就詳細分析每一步:
這里面從發起請求到請求返回結果 一共經歷了10步操作
接下來我們一步步來進行講解:
1.這個方法實現如下
// 解析loadType
private void resolveLoadType() {
Class<?> callBackType = callback.getClass();
if (callback instanceof Callback.TypedCallback) {
loadType = ((Callback.TypedCallback) callback).getLoadType();
} else if (callback instanceof Callback.PrepareCallback) {
loadType = ParameterizedTypeUtil.getParameterizedType(callBackType, Callback.PrepareCallback.class, 0);
} else {
loadType = ParameterizedTypeUtil.getParameterizedType(callBackType, Callback.CommonCallback.class, 0);
}
}
其實這個方法的作用就是得到我們之前傳進來的CommonCallBack泛型中填寫的參數 其實就BaiduResponse 這樣當從服務器得到返回結果之后 我們就知道要將結果解析成什么類型的對象了
2.這一步主要是創建一個HttpRequest請求
1.private UriRequest createNewRequest() throws Throwable {
2.// init request
3.params.init();
4.UriRequest result = UriRequestFactory.getUriRequest(params, loadType);
5.result.setCallingClassLoader(callback.getClass().getClassLoader());
6.result.setProgressHandler(this);
7.this.loadingUpdateMaxTimeSpan = params.getLoadingUpdateMaxTimeSpan();
8.this.update(FLAG_REQUEST_CREATED, result);
9.return result;
10.}
11.這一步其實主要是通過UriRequestFactory.getUriRequest來獲得一個UriRequest對象我們來看下這個對象的實際類型是什么?
1.if (scheme.startsWith("http")) {
2.return new HttpRequest(params, loadType);
這就是這個方法內部最后返回給我們的一個HttpRequest對象 然后返回給調用者
3.主要是檢查下載文件是否沖突的 這個就請讀者們自行閱讀源碼了 這個不是很重要 除非你要下載一個文件時候需要關注這一塊
4.其實就是創建一個重試的對象 然后設置最大的重試次數 這個也不多說
5.這一步主要是檢查緩存中是否包含我們這次的請求 如果包含就將緩存結果取出來然后返回給客戶端 如果沒有 就繼續往下走
6.走到這里就會進行while循環 直到重試次數大于最大重試次數 然后循環體內主要是創建了RequestWorker對象 這是一個線程 創建完成之后會調用他的start方法 然后加入到HttpTask的所在線程中 我們只需關注這個線程的run方法中的一句代碼
1.try {
2.this.result = request.loadResult();
3.} catch (Throwable ex) {
4.this.ex = ex;
5.}
這個request對象我們已經知道 他的類型是HttpRequest 我們來看下這個類里的實現
1.public Object loadResult() throws Throwable {
2.return this.loader.load(this);
3.}
我們發現其實調用了loader對象的load方法 這個loader又是個什么東西呢?
1.public static Loader<?> getLoader(Type type, RequestParams params) {
2.Loader<?> result = converterHashMap.get(type);
3.if (result == null) {
4.result = new ObjectLoader(type);
5.} else {
6.result = result.newInstance();
7.}
8.result.setParams(params);
9.return result;
10.}
我們發現如果我們沒有自定義Loader的話 這里返回給我們的就是ObjectLoader的實體對象
我們來看這個類的load方法
1.@Override
2.public Object load(final UriRequest request) throws Throwable {
3.try {
4.request.sendRequest();
5.} finally {
6.parser.checkResponse(request);
7.}
8.return this.load(request.getInputStream());
9.}
然后此時調用了request的sendRequest其實進去這個方法就知道 這個方法主要的作用就是設置請求參數的 比如添加請求頭 設置請求體(如果是Post請求的話) 設置完成之后 我們將isLoading==true 說明已經處于Loading狀態了
接下來就會調用第9步 然后利用IOUtils將請求的結果封裝成我們想要的類型返回給調用者
最后我們看下返回給調用者之后做了什么?
我們還記得我們之前是怎么一步一步走到現在的嗎? 是在調用HttpTask的setResult的方法中開始的 而這個方法的調用是在TaskProxy類的DoBackGroud方法中調用的
然后接下來返回結果之后呢
- TaskProxy.this.setResult(task.getResult());
// 未在doBackground過程中取消成功
if (TaskProxy.this.isCancelled()) {
throw new Callback.CancelledException("");
}
// 執行成功
TaskProxy.this.onSuccess(task.getResult());
設置結果 并且調用onSucecess方法將結果傳給UI線程 我們來看下這個方法
1.@Override
2.protected void onSuccess(ResultType result) {
3.this.setState(State.SUCCESS);
4.sHandler.obtainMessage(MSG_WHAT_ON_SUCCESS, this).sendToTarget();
5.}
我們看到了熟悉的Handler機制 見到這個Handler 我們首先能夠想到的就是肯定在Activity類里面有一個HanderMessage方法來處理這個消息
那么我們來驗證一下
1.final static class InternalHandler extends Handler {
3.private InternalHandler() {
4.super(Looper.getMainLooper());
5.}
7.@Override
8.@SuppressWarnings("unchecked")
9.public void handleMessage(Message msg) {
10.if (msg.obj == null) {
是吧 我們看到了handleMessage方法
1.switch (msg.what) {
case MSG_WHAT_ON_WAITING: {
taskProxy.task.onWaiting();
break;
}
case MSG_WHAT_ON_START: {
taskProxy.task.onStarted();
break;
}
case MSG_WHAT_ON_SUCCESS: {
taskProxy.task.onSuccess(taskProxy.getResult());
break;
}
case MSG_WHAT_ON_ERROR: {
assert args != null;
Throwable throwable = (Throwable) args[0];
LogUtil.d(throwable.getMessage(), throwable);
taskProxy.task.onError(throwable, false);
break;
}
case MSG_WHAT_ON_UPDATE: {
taskProxy.task.onUpdate(msg.arg1, args);
break;
}
case MSG_WHAT_ON_CANCEL: {
if (taskProxy.callOnCanceled) return;
taskProxy.callOnCanceled = true;
assert args != null;
taskProxy.task.onCancelled((org.xutils.common.Callback.CancelledException) args[0]);
break;
}
case MSG_WHAT_ON_FINISHED: {
if (taskProxy.callOnFinished) return;
taskProxy.callOnFinished = true;
taskProxy.task.onFinished();
break;
}
default: {
break;
}
然后我們就知道這是如何調用到我們之前第一步x.http().get()里的第二個參數CommonCallBack的一系列方法的 這樣 整個請求的過程我們就分析完了 相信大家都有一定的了解了 所以就大膽的嘗試去使用Http請求吧
4.4.4 ImageRequest模塊
在這里我們不得不提一點 Xutils3.0的作者的代碼的精細程度以及對各種場景的準確把握 ,這或許就是為什么Xutils3.0能夠在這么多的框架當中得到這么多用戶的原因 下面我們就列出幾個場景 你在其他框架看不到但是在xutils3.0中卻能看到很精妙的解決方案
并且Xutils3.0中對圖片的請求下載也是支持斷點的 這跟你下載文件是一個邏輯
場景1: 我們打開一個頁面 展示很多圖片 比如一個LIstview然后呢 我們點擊item之后跳到另一個頁面之后 也是一個圖片的列表 此時呢 第一個頁面并沒有被銷毀 那么imageview所持有的圖片也沒有被銷毀 然后第二個頁面加載圖片的時候 我們是往同一個MemearyCache中添加緩存的 如果超過我們設定的緩存的大小呢 就會將第一個頁面中緩存的頁面給清除掉 當我們回到第一個頁面中 可能就會因為緩存中已經被清楚 而從磁盤加載圖片此時效率可能就會受影響 從而導致圖片的閃爍 而這段代碼的目的就是 如果我們第一個頁面的view所持有的圖片資源還沒有被銷毀 那就直接將它添加到緩存中去 然后接下來我們請求就是從內存緩存中讀取而不是磁盤緩存了 這樣就能夠避免這種場景下導致的加載延遲或者頁面閃爍現象了
場景2:
當前屏幕能夠顯示 3個item 那么就會調用三次bind方法
前三次imageview都為null 然后會進行加載將imageview設置AsyncDrawable
此時進入第四個Item 此時復用第一個item的布局 但是imageview的對象沒有變 但是關聯的數據已經變了 那么之前進入屏幕外的第一個item的圖片的加載過程可能還沒完成 也可能已經完成了
假如沒有完成 此時呢 這段代碼就能起到 去取消那個請求 但是如果此時用戶又很快滑動到第一個item此時判斷key相同 那么就什么都不做了 因為之前跟imageview關聯的Imageloade就會繼續之前的操作。
那么作者是如何巧妙的解決上述兩種背景引起的BUG呢 其實就是很簡單的一段代碼以及自定義Drawable就解決了
那我們先睹為快 然后再一步一步帶領你去分析實現的原理
// stop the old loader
MemCacheKey key = new MemCacheKey(url, localOptions);
Drawable oldDrawable = view.getDrawable();
if (oldDrawable instanceof AsyncDrawable) {
ImageLoader loader = ((AsyncDrawable) oldDrawable).getImageLoader();
if (loader != null && !loader.stopped) {
if (key.equals(loader.key)) {
// repetitive url and options binding to the same View.
// not need callback to ui.
// key相同不做處理, url沒有變換.
// key不同, 取消之前的, 開始新的加載
return null;
} else {
loader.cancel();
}
}
} else if (oldDrawable instanceof ReusableDrawable) {
MemCacheKey oldKey = ((ReusableDrawable) oldDrawable).getMemCacheKey();
if (oldKey != null && oldKey.equals(key)) {
MEM_CACHE.put(key, oldDrawable);
}
}
就是上述代碼就解決了我們場景1和2中可能遇到的比如說圖片閃爍或者超出屏幕之外的不必要的請求等問題。
下面我們就通過整個流程來分析一下作者的實現思路。
首先我們來看下Bitmap這個模塊設置圖片的流程圖
其實如果你看了比較流行的UIL以及Volley等框架的網絡圖片的加載的話 你會發現其實他們的流程基本是一致的 也就是說我們加載圖片的整個過程基本是類似的 不同只是一些代碼的實現細節方面 比如緩存機制啊 網絡加載機制啊 等等。
所以我們就根據這個流程圖來看一下這個模塊的設計
Xutils3.0中對于圖片的加載遵循其實也遵循上面的那個流程圖 雖然這個流程圖是Xutils第一個版本的 但是對于Xutils3.0來說照樣適用 我們來從源碼中來分析一下
-
x.image().bind(holder.imgItem,
imgSrcList.get(position),
imageOptions,
new CustomBitmapLoadCallBack(holder));
首先綁定Imageview 并且設置配置參數 以及回調函數
2.static Cancelable doBind(final ImageView view,
final String url,
final ImageOptions options,
final Callback.CommonCallback<Drawable> callback) {
這個是調用ImageLoader類的doBind方法實現ImageView和ImageLoader的綁定 然后我們來看一下這個方法里面的核心代碼邏輯:第一步: // check params
ImageOptions localOptions = options;
{
if (view == null) {
postArgsException(null, localOptions, "view is null", callback);
return null;
}if (TextUtils.isEmpty(url)) { postArgsException(view, localOptions, "url is null", callback); return null; } if (localOptions == null) { localOptions = ImageOptions.DEFAULT; } localOptions.optimizeMaxSize(view); }
這個就是首先對我們配置的圖片的Options進行檢查 這個沒什么好說的
第二步:
// stop the old loader
MemCacheKey key = new MemCacheKey(url, localOptions);
Drawable oldDrawable = view.getDrawable();
//每一個View都會綁定一個Drawable
//如果加載出來的類型都是ReusableDrawable 沒有加載出來之前都是AsyncDrawable
if (oldDrawable instanceof AsyncDrawable) {
ImageLoader loader = ((AsyncDrawable) oldDrawable).getImageLoader();
if (loader != null && !loader.stopped) {
if (key.equals(loader.key)) {
// repetitive url and options binding to the same View.
// not need callback to ui.
// key相同不做處理, url沒有變換.
// key不同, 取消之前的, 開始新的加載
return null;
} else {
loader.cancel();
}
}
} else if (oldDrawable instanceof ReusableDrawable) {
MemCacheKey oldKey = ((ReusableDrawable) oldDrawable).getMemCacheKey();
if (oldKey != null && oldKey.equals(key)) {
MEM_CACHE.put(key, oldDrawable);
}
}
關鍵難點:
這一步就是我們在這個模塊開頭處提到的處理邏輯了 其實實現原理就是 當我們Imageview綁定一個drawable的時候但是并沒有被銷毀的時候 我們是可以獲取imageview綁定的drawable對象 后面當初次加載的時候都會將imageview設置為asyncDrawable表示正在加載也就是正在請求網絡下載圖片 然后當用戶不同滑動ListView或者不同頁面之間的跳轉 重新執行到這里的時候 我們就可以根據Imageview綁定的drawable對象 從而獲取跟這個drawable對象關聯的ImageLoader對象 然后根據加載的key來決定我們是否正在在一個相同的iamgeview加載同一個圖片 還是該Imageview已經被復用但是關聯的圖片資源key卻改變的情況 這種情況 我們就取消之前的加載 因為他已經在屏幕外了 對用戶來說已經沒有加載的必要了就調用loader.cancle方法了
假如 圖片加載很快 用戶往下拉之后 很快又往上滑動listview 此時呢 我們的imageview關聯的drawable已經加載完畢 此時類型就是reusableDrawable了 此時我們就判斷key是否相同 ,如果相同 那么就將該drawable 放進內存緩存中 我們就沒必要進行網絡請求了 這也解決了因為內存不夠 不斷滑動屏幕或者切換頁面 導致內存緩存不足 之前的緩存被清理掉 然后因為要從 磁盤或者網絡重新加載導致的屏幕閃爍問題了。
第三步:
// load from Memory Cache
Drawable memDrawable = null;
if (localOptions.isUseMemCache()) {
memDrawable = MEM_CACHE.get(key);
if (memDrawable instanceof BitmapDrawable) {
Bitmap bitmap = ((BitmapDrawable) memDrawable).getBitmap();
if (bitmap == null || bitmap.isRecycled()) {
memDrawable = null;
}
}
}
if (memDrawable != null) { // has mem cache
boolean trustMemCache = false;
try {
if (callback instanceof ProgressCallback) {
((ProgressCallback) callback).onWaiting();
}
// hit mem cache
view.setScaleType(localOptions.getImageScaleType());
view.setImageDrawable(memDrawable);
trustMemCache = true;
if (callback instanceof CacheCallback) {
trustMemCache = ((CacheCallback<Drawable>) callback).onCache(memDrawable);
if (!trustMemCache) {
// not trust the cache
// load from Network or DiskCache
return new ImageLoader().doLoad(view, url, localOptions, callback);
}
} else if (callback != null) {
callback.onSuccess(memDrawable);
}
} catch (Throwable ex) {
LogUtil.e(ex.getMessage(), ex);
// try load from Network or DiskCache
trustMemCache = false;
return new ImageLoader().doLoad(view, url, localOptions, callback);
} finally {
if (trustMemCache && callback != null) {
try {
callback.onFinished();
} catch (Throwable ignored) {
LogUtil.e(ignored.getMessage(), ignored);
}
}
}
} else {
// load from Network or DiskCache
return new ImageLoader().doLoad(view, url, localOptions, callback);
}
從英文注釋 我們就可以明白 這個其實就判斷是否內存緩存中存在我們想要的結果 如果存在就取出來然后調用回調方法 展示出來
但是這里面要注意的是 如果我們的回調函數的類型是CacheCallBack類型的話 那么是否從緩存中取就取決于 CacheCallBack的oncache方法的返回值了 如果為false 那就從網絡或者DISK中獲取了。
第四步 其實就是 if (memDrawable != null) { // has mem cache
當if判斷走else邏輯的時候 我們會請求網絡加載數據并放進內存緩存和磁盤緩存
return new ImageLoader().doLoad(view, url, localOptions, callback);
也就是會走這里面的邏輯。
大致步驟基本就是這樣 ,但是我在研究源碼的時候曾經阻塞在一個地方,就是我調用請求要求返回結果 我傳進去的泛型參數是Drawable類型的 但是為什么最后變化成了AsyncDrawable 或者ReusableDrawable 這個其實就是第二步當中我們判斷邏輯的實現的重要的一部分,我經過多次閱讀這部分的源碼 終于明白了作者的設計意圖和實現方法
接下來 我就帶大家回顧一下我是如何找到這個答案的。
我們接著上面的第四步繼續往下分析:
在 return new ImageLoader().doLoad(view, url, localOptions, callback);
這個方法的最后調用了 cancelable = x.http().get(params, this); 看到這里大家應該很熟悉了 這其實就是我們之前分析過的Http請求的邏輯了 這個如果不明白的就往回自己看下 這里對于這一塊的 如果一致的我就不重復啰嗦 我只列出來不一樣的地方
經過前面Http模塊的分析 我們都知道 x.http().get(params, this)這個會調用HttpManangerImpl的request方法 這里要注意一點 我們傳進去的this的類型是ImageLoader類型 這個類實現了四個接口
/*此時callback類型是ImageLoader類型這個類實現了
Callback.PrepareCallback<File, Drawable>,
Callback.CacheCallback<Drawable>,
Callback.ProgressCallback<Drawable>,
Callback.TypedCallback<Drawable>,
Callback.Cancelable 這幾個接口 然后構造HttpTask的時候將imageloader作為成員變量傳進去 */
所以這里跟之前分析得不同的地方就是 當我們構造HttpTask對象的時候
//這里如果傳遞過來的是IamgeLoader類型的話 那么cacheCallback prepareCallback progressCallback 都會被賦值
if (callback instanceof Callback.CacheCallback) {
this.cacheCallback = (Callback.CacheCallback<ResultType>) callback;
}
if (callback instanceof Callback.PrepareCallback) {
this.prepareCallback = (Callback.PrepareCallback) callback;
}
if (callback instanceof Callback.ProgressCallback) {
this.progressCallback = (Callback.ProgressCallback<ResultType>) callback;
}
這些回調變量都會被賦值
然后在調用HttpTask的DoBackGroud方法的時候 所解析出來的請求類型是這樣的
// 解析loadType
private void resolveLoadType() {
Class<?> callBackType = callback.getClass();
if (callback instanceof Callback.TypedCallback) {
loadType = ((Callback.TypedCallback) callback).getLoadType();
} else if (callback instanceof Callback.PrepareCallback) {
loadType = ParameterizedTypeUtil.getParameterizedType(callBackType, Callback.PrepareCallback.class, 0);
} else {
loadType = ParameterizedTypeUtil.getParameterizedType(callBackType, Callback.CommonCallback.class, 0);
}
}
這個loadType的返回值就是File類型了 ((Callback.TypedCallback) callback).getLoadType();的實現
其實就是ImageLoader的實現
@Override
public Type getLoadType() {
return loadType;
}
而這個返回值是ImageLoader的成員變量
private static final Type loadType = File.class;
那么我們調用HttpTask的DoBackGroud方法中的try {
this.result = request.loadResult();
} catch (Throwable ex) {
this.ex = ex;
}的loadResult方法
public Object loadResult() throws Throwable {
return this.loader.load(this);
}
這里的loader類型其實就FileLoader了
public File load(final UriRequest request) throws Throwable 最后調用這個函數 去下載圖片資源 然后將下載下來的圖片對象返回給調用者
在這個方法里面我們涉及到了DiskCacheFile這個變量 其實這個就是我們之前所說的緩存機制中的磁盤緩存了 在這個方法里面會 initDiskCacheFile(request); 初始化這個變量 然后當請求數據完成之后 會對這個變量進行賦值
if (diskCacheFile != null) {
DiskCacheEntity entity = diskCacheFile.getCacheEntity();
entity.setLastAccess(System.currentTimeMillis());
entity.setEtag(request.getETag());
entity.setExpires(request.getExpiration());
entity.setLastModify(new Date(request.getLastModified()));
}
這里涉及到了Http協議中關于緩存這一塊的東西 這邊不了解的可以網上自行百度 這里不詳細討論 我們只說這幾個變量的含義是什么意思
Request 請求頭
Cache-Control: max-age=0 以秒為單位
If-Modified-Since: Mon, 19 Nov 2012 08:38:01 GMT 緩存文件的最后修改時間。
If-None-Match: "0693f67a67cc1:0" 緩存文件的Etag值
Cache-Control: no-cache 不使用緩存
Pragma: no-cache 不使用緩存
Response header
Cache-Control: public 響應被緩存,并且在多用戶間共享, (公有緩存和私有緩存的區別,請看另一節)
Cache-Control: private 響應只能作為私有緩存,不能在用戶之間共享
Cache-Control:no-cache 提醒瀏覽器要從服務器提取文檔進行驗證
Cache-Control:no-store 絕對禁止緩存(用于機密,敏感文件)
Cache-Control: max-age=60 60秒之后緩存過期(相對時間)
Date: Mon, 19 Nov 2012 08:39:00 GMT 當前response發送的時間
Expires: Mon, 19 Nov 2012 08:40:01 GMT 緩存過期的時間(絕對時間)
Last-Modified: Mon, 19 Nov 2012 08:38:01 GMT 服務器端文件的最后修改時間
ETag: "20b1add7ec1cd1:0" 服務器端文件的Etag值
如果想詳細了解的朋友 請進入這里 作者很相信的講解了關于Http協議中關于緩存這一塊的知識
這里我就簡單說一下客戶端與服務器端關于緩存機制的配合 我們來看一個圖:
通過最后修改時間, 來判斷緩存新鮮度
- 瀏覽器客戶端想請求一個文檔, 首先檢查本地緩存,發現存在這個文檔的緩存, 獲取緩存中文檔的最后修改時間,通過: If-Modified-Since, 發送Request給Web服務器。
- Web服務器收到Request,將服務器的文檔修改時間(Last-Modified): 跟request header 中的,If-Modified-Since相比較, 如果時間是一樣的, 說明緩存還是最新的, Web服務器將發送304 Not Modified給瀏覽器客戶端, 告訴客戶端直接使用緩存里的版本。如下圖。
- 假如該文檔已經被更新了。Web服務器將發送該文檔的最新版本給瀏覽器客戶端, 如下圖。
ETag是實體標簽(Entity Tag)的縮寫, 根據實體內容生成的一段hash字符串(類似于MD5或者SHA1之后的結果),可以標識資源的狀態。 當資源發送改變時,ETag也隨之發生變化。
ETag是Web服務端產生的,然后發給瀏覽器客戶端。瀏覽器客戶端是不用關心Etag是如何產生的。
為什么使用ETag呢? 主要是為了解決Last-Modified 無法解決的一些問題。
- 某些服務器不能精確得到文件的最后修改時間, 這樣就無法通過最后修改時間來判斷文件是否更新了。
- 某些文件的修改非常頻繁,在秒以下的時間內進行修改. Last-Modified只能精確到秒。
- 一些文件的最后修改時間改變了,但是內容并未改變。 我們不希望客戶端認為這個文件修改了。
http://www.cnblogs.com/TankXiao/archive/2012/11/28/2793365.html
最后將下載下來的圖片保存在本地磁盤緩存中 大家可能還是不明白是如何將Dradble轉換為Async和Resueable類型的 這里我們就要進入HttpTask的DoBackGroud方法中的檢查緩存那一塊了
因為之前我們分析的Httpget 模塊因為沒有使用緩存 直接略過了 但是我們調用ImageLoader加載圖片時候 就要使用緩存了 我們來看一下:
// 檢查緩存
Object cacheResult = null;
if (cacheCallback != null && HttpMethod.permitsCache(params.getMethod())) {
// 嘗試從緩存獲取結果, 并為請求頭加入緩存控制參數.
try {
clearRawResult();
LogUtil.d("load cache: " + this.request.getRequestUri());
rawResult = this.request.loadResultFromCache();
} catch (Throwable ex) {
LogUtil.w("load disk cache error", ex);
}
if (this.isCancelled()) {
clearRawResult();
throw new Callback.CancelledException("cancelled before request");
}
if (rawResult != null) {
if (prepareCallback != null) {
try {
cacheResult = prepareCallback.prepare(rawResult);
} catch (Throwable ex) {
cacheResult = null;
LogUtil.w("prepare disk cache error", ex);
} finally {
clearRawResult();
}
} else {
cacheResult = rawResult;
}
因為這里callback實現了cachecallback 并且我們使用的是get方法 所以if為true進入代碼塊里面執行
因為這里prepareCallback 也不為空 所以就調用了prepare 方法 這里進入了Imageloader的prepare 方法 我們來看一下 :
@Override
public Drawable prepare(File rawData) {
if (!validView4Callback(true)) return null;
try {
Drawable result = null;
if (prepareCallback != null) {
result = prepareCallback.prepare(rawData);
}
if (result == null) {
result = ImageDecoder.decodeFileWithLock(rawData, options, this);
}
if (result != null) {
if (result instanceof ReusableDrawable) {
((ReusableDrawable) result).setMemCacheKey(key);
MEM_CACHE.put(key, result);
}
}
return result;
} catch (IOException ex) {
IOUtil.deleteFileOrDir(rawData);
LogUtil.w(ex.getMessage(), ex);
}
return null;
}
看到這里 我們找到了ImageDecoder.decodeFileWithLock 這里其實就是真正進行轉換的地方 我們進去看下源碼就立馬清楚了
f (bitmap != null) {
result = new ReusableBitmapDrawable(x.app().getResources(), bitmap);
}
這個方法最后對bitmap進行了封裝 然后將封裝后的結果返回給調用者 看到了吧 類型是
ReusableBitmapDrawable
然后在ImageLoder的prepare方法中將ImageDecoder.decodeFileWithLock 返回的結果 if (result != null) {
if (result instanceof ReusableDrawable) {
((ReusableDrawable) result).setMemCacheKey(key);
MEM_CACHE.put(key, result);
}
}放進內存緩存中 這就是整個的緩存的流程
相信大家可能還是有點云里霧里 但是我覺得大致思路知道了之后i 自己再去看一遍源碼 我覺得就應該能夠清楚了吧
這段邏輯比較復雜 所以這里我們使用一個時序圖幫大家屢一下思路
上面這個時序圖就是整個請求網絡圖片的過程 ,其中有一段是關于圖片緩存的這一塊比較復雜 所以我會單獨再畫一個時序圖 來專門講解圖片緩存的時序圖
這里面有一個關鍵的地方就是在HttpTask的doBackgroud方法中 有一段這樣的代碼:
if (cacheResult != null) {
// 同步等待是否信任緩存
this.update(FLAG_CACHE, cacheResult);
while (trustCache == null) {
synchronized (cacheLock) {
try {
cacheLock.wait();
} catch (Throwable ignored) {
}
}
}
// 處理完成
if (trustCache) {
return null;
}
}
這段代碼的作用就是給你一個機會讓你去調用你實現的OncacheCallback中的oncache方法 然后根據返回結果 來決定是否進行下一步的網絡請求 如果你oncache方法返回true說明你信任緩存 此時直接return null 就不再往下進行 直接使用緩存的值
如果不信任緩存的話 那么就進行網絡請求
好了 IamgeFragment這一模塊也基本分析完成了 幾個需要注意的點就是:
1.緩存機制是如何使用的?包括內存緩存和磁盤緩存
2.我們自己設置的CacheCallback在什么時候調用的 起到了什么作用
3.關于Http協議的緩存部分的理解
我相信 只要上面三點弄明白了 我覺得這一塊也就沒什么問題了
5.Xutils3.0總結
整個Xutils3.0的源碼基本分析完成了 ,整個框架還是很不錯的 整體的設計也比較簡潔,我相信讀者看完我的分析之后 對于日常當中的使用應該不會有太大問題,出問題了 就深入去看源碼 你就會找到根源 基本能夠徹底的解決開發過程中出現的BUG了
下面簡單總結一下吧:
1.學會使用Xutils3.0請求服務器數據以及請求參數的設置
2.掌握Xutils3.0中關于緩存機制的應用
3.了解Xutils3.0中對Google官方推薦內存緩存LRU的使用方法
4.能夠在自己的項目當中使用Xutils3.0進行網絡 、數據庫、圖片的處理
我相信 學以致用才是最好的理解一個框架的方法 只有在使用過程當中 發現問題解決問題 才能夠真正理解作者的設計思想以及提高自身代碼質量。
初次分享 ,可能有很多地方表達不是很流暢,希望大家指正 也幫我彌補自己的不足 。