原文地址: http://www.jianshu.com/p/5832c776621f
前言
發表上篇文章 我一行程式碼都不寫實現Toolbar!你卻還在封裝BaseActivity? 已是一個月前的事情,當時有人說我是標題黨,也有人不認可我的內容,但是這也不併不妨礙我,兩天奪得掘金當週周榜第一,並被 鴻洋公眾號 轉載,累計閱讀量超過 3萬
上篇文章的研究成果讓 MVPArms 具備了 監聽整個 App 所有 Activity 以及 Fragment 的生命週期(包括三方庫),並可向其生命週期內插入程式碼 的功能,這次我又拿著最近的另一項研究成果向大家彙報,當然同樣也是 MVPArms 上的新增功能
Github : 你的 Star 是我堅持的動力 ✊
羅列需求
上傳下載是大多數 APP 必備的功能,顯示進度條也是提高使用者體驗的重要一環,當然作為 可配置化整合框架 MVPArms 的作者,我想再次提高開發者的使用體驗以及開發效率,那我就必須提供一套解決方案
於是我開啟 Github 簡單的搜了一圈與 Retrofit , Okhttp , Glide 有關的進度監聽庫,庫到是不少,但是都沒有達到我想要的需求,於是我捲起衣袖,準備擼一個,當然,開擼之前要先簡單梳理下自己的需求
- 這個庫一定要支援多個平臺,Okhttp , Retrofit , Glide 這三個必須同時支援
- 雖然支援這三個庫,但是庫裡面並不能包含這三個庫,讓使用者自己去引入,減小庫的體積
- 使用一定要簡單!!!,最好能一行程式碼搞定
- 侵入性低,並不需要改之前寫好的網路請求程式碼,引入與不引入這個庫,對之前的程式碼都不能有任何影響
- 低耦合,使用者做網路請求的程式碼,一定不能和進度接收端的程式碼有太多關聯
- 在 App 的任何位置都能接受到某個網路請求的 進度資訊
- 不僅僅需要滿足,一個資料來源對應一個進度接收端的一對一關係,還需要滿足一個資料來源對應多個進度接收端的,一對多關係,這樣就可以同步更新多個不同位置的進度條
- 預設執行在主執行緒,讓使用者少去切換執行緒的煩惱
需求分析及調研
爽一下子,寫出了這麼多需求,當產品經理就是一個字爽!
仔細一看這8個需求,瞬間懵逼了,妹的這不是坑自己嗎?除了最後一項,我知道可以用 Handler 來實現,其他完全沒思路啊,得了,作為一個優質男青年我得知難而進啊,先從第一個需求開始分析吧!
需求 1 (多平臺支援)
寫之前翻了下 Google 發現,Okhttp 實現上傳下載進度監聽,並不困難,只用重寫 RequestBody 和 ResponseBody ,並配合 Interceptor 將每個請求原有的 RequestBody 和 ResponseBody 替換,就可以實現,都是模版程式碼,複製貼上就可以了,而 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 中將每個請求原有的 RequestBody 和 ResponseBody 替換成重寫後的
如何識別需要監聽進度的請求?
替換是簡單,但是不是每個請求都需要監聽上傳和下載進度,不可能每個請求都替換啊,開始我想到的是給需要監聽進度的請求生成個標記,然後在 Interceptor 中解析到這個標記,就說明這個請求需要監聽上傳或下載進度,然後就開始替換之
於是我想到最簡單的方式就是在請求的時候加一個自定義的 Header ,這樣就不用再定義其他的類, Interceptor 遍歷所有 Header 發現有這個自定義 Header ,就可以替換
但是這樣並沒有解決需求 4,因為這樣讓使用者比平時請求時多了個操作,如果想讓之前的程式碼具有進度監聽功能,就要一個個挨著改,增加了勞動量,而且這個操作是針對於我這個庫而產生的,當使用者並不想使用這個庫的時候,會牽扯到修改之前的程式碼,這樣還增加了侵入性
Url 作為標記
一個念頭一閃而過,還要什麼標記, Url 是唯一的, 不就可以作為標記嗎!!!
需求 5 (低耦合) ,需求 6 (任何位置都可接收),以及 需求 7 (一對多)
借用 EventBus 思想
為什麼把這三個需求放在一起呢,因為這三個需求讓我想到了 EventBus ,多個觀察者使用同一個標記將自己註冊進一個容器,被觀察者使用這個標記 Post 一個事件,然後從這個容器中拿出所有使用這個標記註冊過的觀察者,挨個通知,這樣既解耦,並且只要知道這個標記,在 App 任何位置都可以監聽,也支援一對多
加上需求 4,中提到的使用 Url 作為標記,那我就可以做到之前請求的程式碼一個也不用改,只用寫接收端的程式碼即可實現以上的需求
構思 Api
既然談到 EventBus ,那我就用 EventBus 的 Api 來設計,使用者只用一行程式碼,傳入一個 標記 和一個 事件 即可實現上傳和下載進度監聽,沒錯 標記 就是 Url , 事件 就是用於獲取進度資訊的 監聽器,這樣也就滿足了 需求 3 的一行程式碼實現的需求
Like this
ProgressManager.post(標記,事件);
複製程式碼
使用者呼叫這一行程式碼後,我會將 Url 作為 Key,監聽器 作為 value 放入一個全域性唯一的 Map 中
等等?說好一對多的呢?所以這個 value 必須是 List< 監聽器 > ,這樣就滿足了一對多的條件了
內部如何通知監聽器?
我們把所有需要監聽的 Url 的 監聽器 都註冊進了這個容器,那我們什麼時候該去通知 監聽器 進度資訊呢,當然是在 RequestBody 和 ResponseBody 中開始寫入或讀取二進位制流的時候,因為只有他們第一時間知道,讀取和寫入的時間,現在只需要把對應 Url 的所有 監聽器 放入他的 Body 中就可以了
因為 需求 4 中提到,我們並不知道哪些請求是需要監聽上傳或下載進度,哪些是不需要的,但是現在我們就可以通過 Url 來辨別,因為我們可以在 Interceptor 中拿到 Request 的 Url
之前我們已經將 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,一起學習進步,如果框架有更新,我也會在公眾號上第一時間通知大家
Hello 我叫 JessYan,如果您喜歡我的文章,可以在以下平臺關注我
- 個人主頁: jessyan.me
- GitHub: github.com/JessYanCodi…
- 掘金: juejin.im/user/57a9db…
- 簡書: www.jianshu.com/u/1d0c0bc63…
- 微博: weibo.com/u/178626251…
-- The end