一行程式碼實現Okhttp,Retrofit,Glide下載上傳進度監聽

JessYan發表於2017-06-11

原文地址: http://www.jianshu.com/p/5832c776621f

前言

發表上篇文章 我一行程式碼都不寫實現Toolbar!你卻還在封裝BaseActivity? 已是一個月前的事情,當時有人說我是標題黨,也有人不認可我的內容,但是這也不併不妨礙我,兩天奪得掘金當週周榜第一,並被 鴻洋公眾號 轉載,累計閱讀量超過 3萬

上篇文章的研究成果讓 MVPArms 具備了 監聽整個 App 所有 Activity 以及 Fragment 的生命週期(包括三方庫),並可向其生命週期內插入程式碼 的功能,這次我又拿著最近的另一項研究成果向大家彙報,當然同樣也是 MVPArms 上的新增功能

Github : 你的 Star 是我堅持的動力 ✊

gif

羅列需求

上傳下載是大多數 APP 必備的功能,顯示進度條也是提高使用者體驗的重要一環,當然作為 可配置化整合框架 MVPArms 的作者,我想再次提高開發者的使用體驗以及開發效率,那我就必須提供一套解決方案

於是我開啟 Github 簡單的搜了一圈與 Retrofit , Okhttp , Glide 有關的進度監聽庫,庫到是不少,但是都沒有達到我想要的需求,於是我捲起衣袖,準備擼一個,當然,開擼之前要先簡單梳理下自己的需求

  1. 這個庫一定要支援多個平臺,Okhttp , Retrofit , Glide 這三個必須同時支援
  2. 雖然支援這三個庫,但是庫裡面並不能包含這三個庫,讓使用者自己去引入,減小庫的體積
  3. 使用一定要簡單!!!,最好能一行程式碼搞定
  4. 侵入性低,並不需要改之前寫好的網路請求程式碼,引入與不引入這個庫,對之前的程式碼都不能有任何影響
  5. 低耦合,使用者做網路請求的程式碼,一定不能和進度接收端的程式碼有太多關聯
  6. App 的任何位置都能接受到某個網路請求的 進度資訊
  7. 不僅僅需要滿足,一個資料來源對應一個進度接收端的一對一關係,還需要滿足一個資料來源對應多個進度接收端的,一對多關係,這樣就可以同步更新多個不同位置的進度條
  8. 預設執行在主執行緒,讓使用者少去切換執行緒的煩惱

需求分析及調研

爽一下子,寫出了這麼多需求,當產品經理就是一個字爽!

仔細一看這8個需求,瞬間懵逼了,妹的這不是坑自己嗎?除了最後一項,我知道可以用 Handler 來實現,其他完全沒思路啊,得了,作為一個優質男青年我得知難而進啊,先從第一個需求開始分析吧!

需求 1 (多平臺支援)

寫之前翻了下 Google 發現,Okhttp 實現上傳下載進度監聽,並不困難,只用重寫 RequestBodyResponseBody ,並配合 Interceptor 將每個請求原有的 RequestBodyResponseBody 替換,就可以實現,都是模版程式碼,複製貼上就可以了,而 Retrofit 底層使用的是 Okhttp,那就也可以同樣實現進度監聽

但是 Glide怎麼實現進度監聽呢? 我的第一反應就是既然 Retrofit 使用 Okhttp 請求網路就可以非常容易的實現,那將 Glide 的底層請求框架換成 Okhttp 也可以實現咯,作為一個如此牛逼的庫,肯定有擴充套件的方式,於是馬上去翻 Glide 的原始碼,印證了自己的想法,發現 Glide 底層是使用的 HttpConenction 去請求網路,並且這個類時可以被替換的,趕快 Google 了下

compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
複製程式碼

ok,找到解決方案,可用上面提供的類,將底層請求框架替換為 Okhttp ,這個框架最核心的地方已經找到實現方式,主要是通過 Okhttp 實現,如同吃了定心丸,瞬間舒坦

需求 2 (減小體積)

這個需求 Google 了下,也非常簡單,用 provided 引入依賴框架,打包時引入的框架就不會包含進去

需求 3 (一行程式碼實現)

對於這種對外 Api 設計上的需求,我們應該把主體功能實現了,再慢慢優化到想達到的目標所以先分析下面的需求

需求 4 (侵入性低)

因為需求 1 已經提到,實現上傳和下載進度監聽的關鍵就是,在 Interceptor 中將每個請求原有的 RequestBodyResponseBody 替換成重寫後的

如何識別需要監聽進度的請求?

替換是簡單,但是不是每個請求都需要監聽上傳和下載進度,不可能每個請求都替換啊,開始我想到的是給需要監聽進度的請求生成個標記,然後在 Interceptor 中解析到這個標記,就說明這個請求需要監聽上傳或下載進度,然後就開始替換之

於是我想到最簡單的方式就是在請求的時候加一個自定義的 Header ,這樣就不用再定義其他的類, Interceptor 遍歷所有 Header 發現有這個自定義 Header ,就可以替換

但是這樣並沒有解決需求 4,因為這樣讓使用者比平時請求時多了個操作,如果想讓之前的程式碼具有進度監聽功能,就要一個個挨著改,增加了勞動量,而且這個操作是針對於我這個庫而產生的,當使用者並不想使用這個庫的時候,會牽扯到修改之前的程式碼,這樣還增加了侵入性

Url 作為標記

一個念頭一閃而過,還要什麼標記, Url 是唯一的, 不就可以作為標記嗎!!!

需求 5 (低耦合) ,需求 6 (任何位置都可接收),以及 需求 7 (一對多)

借用 EventBus 思想

為什麼把這三個需求放在一起呢,因為這三個需求讓我想到了 EventBus ,多個觀察者使用同一個標記將自己註冊進一個容器,被觀察者使用這個標記 Post 一個事件,然後從這個容器中拿出所有使用這個標記註冊過的觀察者,挨個通知,這樣既解耦,並且只要知道這個標記,在 App 任何位置都可以監聽,也支援一對多

加上需求 4,中提到的使用 Url 作為標記,那我就可以做到之前請求的程式碼一個也不用改,只用寫接收端的程式碼即可實現以上的需求

構思 Api

既然談到 EventBus ,那我就用 EventBusApi 來設計,使用者只用一行程式碼,傳入一個 標記 和一個 事件 即可實現上傳和下載進度監聽,沒錯 標記 就是 Url , 事件 就是用於獲取進度資訊的 監聽器,這樣也就滿足了 需求 3 的一行程式碼實現的需求

Like this

ProgressManager.post(標記,事件);
複製程式碼

使用者呼叫這一行程式碼後,我會將 Url 作為 Key,監聽器 作為 value 放入一個全域性唯一的 Map

等等?說好一對多的呢?所以這個 value 必須是 List< 監聽器 > ,這樣就滿足了一對多的條件了

內部如何通知監聽器?

我們把所有需要監聽的 Url監聽器 都註冊進了這個容器,那我們什麼時候該去通知 監聽器 進度資訊呢,當然是在 RequestBodyResponseBody 中開始寫入或讀取二進位制流的時候,因為只有他們第一時間知道,讀取和寫入的時間,現在只需要把對應 Url 的所有 監聽器 放入他的 Body 中就可以了

因為 需求 4 中提到,我們並不知道哪些請求是需要監聽上傳或下載進度,哪些是不需要的,但是現在我們就可以通過 Url 來辨別,因為我們可以在 Interceptor 中拿到 RequestUrl

之前我們已經將 Url 作為 Key 註冊進了容器,如果容器裡面 Contain 這個 Url 那就是說明這個請求,是需要監聽上傳或下載進度的,那我們就給他替換成重寫後的 Body 並將監聽器傳入,重寫後的 Body 在發生二進位制流的 讀取 或 寫入 時不斷的遍歷這個 Url 的所有 監聽器,呼叫 監聽器 的監聽方法,並傳入進度資訊,就可以執行使用者的更新邏輯,這就大功告成了

需求 8 (主執行緒執行)

這個很簡單,使用 Handler.post(Runnable)Runnable 中呼叫 監聽器 的方法就可以了

框架細節優化

無需手動登出

大家都知道 EventBus 註冊觀察者後,在不需要接受事件時,需要手動登出,但是應用到我這個庫中,事件的接收可能不需要這麼嚴謹,所以為了免去使用者多餘的步驟,我就是使用 WeakHashMap 代替之前的 Map 容器,這個 WeakHashMap 會在 Java虛擬機器 回收記憶體時,找到沒被使用的 Key,將此條目整個移除,所以不需要手動 remove()

加鎖

在上面提到使用者只需要一行程式碼,將 Url監聽器 加入容器,但是這行程式碼,可能是在不同執行緒中被呼叫的,而且這行程式碼內的一些邏輯在多執行緒中是不安全的,所有這時我需要加入執行緒鎖,這個對於三方庫很重要,因為你無法預知一些使用者的操作

向使用者丟擲清晰的錯誤

因為我在 需求 2 中已經提到,此庫只會用 provided 引入 Okhttp ,所以 Okhttp 是不會被打進 aar 包裡的,所以如果使用者在自己的專案中沒有引入 Okhttp 是會報 NoClassDefFoundError 這個錯誤的,但是這個錯誤會讓使用者不知道真實的出錯原因,讓使用者誤以為是這個庫的導致的,所以我會在庫初始化的時候, Class.forName("okhttp3.OkHttpClient"); 如果找不到 Okhttp 的這個類,說明使用者沒有引入 Okhttp ,然後我會丟擲一個解釋非常清晰的錯誤

提高效能

因為上面提到過我會在 Body ,開始讀取或寫入二進位制流時,不斷的遍歷所有監聽器並呼叫它的監聽方法,來達到一對多的同步更新

但是這樣 監聽器 達到一定數量就會出現效能問題,並且在遍歷時,搞不好使用者也會,不斷的新增新的監聽器,在遍歷時改變容器的長度是容易發生錯誤的

所以我在將 List 傳入 Body 時,將這個 List.toArray() ,陣列分配的是連續的記憶體區域並且長度是固定的,所以索引效率佔有優勢,則使用陣列來遍歷,由於陣列長度是固定的,所以也不會出現遍歷時長度變化的問題

區分同一個Url的多個進度

因為 App 使用者可能在前一個進度還沒上傳或下載完的情況下,繼續使用同一個 Url 開始新的請求,如果框架使用者在上層不去做去除重複點選的操作,那同一個 Url 就會同時存在多個正在執行的進度更新,這時就需要有識別符號來區分到底是哪個進度資訊(這個 Url 的所有正在執行的進度更新都會呼叫之前以這個 Url 註冊過的監聽器),所以我在 Body ,建立時會將 System.currentTimeMillis() 作為唯一 ID ,儲存起來,每次將進度資訊和 Id 一起傳給使用者

總結

其實這個庫本來就比較簡單,實現的核心方式在很多地方都是能複製貼上到的,但經過我這麼一封裝還是要比之前的方式,簡單優雅不少,而寫這篇文章的目也是想分享下,如何分析需求,以及如何封裝優化一個小型的庫,當然平時也要多閱讀原始碼,不斷積累和借鑑優秀的思想在創作時靈感才會源源不斷,比如我這個庫就是借鑑的 EventBus 的思想,在寫程式碼時要敢於想敢於嘗試較於之前不同的新思想,才會不斷進步

Github : 具體實現還得看原始碼不是? 記得給 Star ✊ 感謝!

公眾號

掃碼關注我的公眾號 JessYan,一起學習進步,如果框架有更新,我也會在公眾號上第一時間通知大家

一行程式碼實現Okhttp,Retrofit,Glide下載上傳進度監聽


Hello 我叫 JessYan,如果您喜歡我的文章,可以在以下平臺關注我

-- The end

相關文章