美文网首页NetRx系列android开源APP
一行代码实现Okhttp,Retrofit,Glide下载上传进

一行代码实现Okhttp,Retrofit,Glide下载上传进

作者: JessYan | 来源:发表于2017-06-12 01:59 被阅读4452次

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

    前言

    发表上篇文章 我一行代码都不写实现Toolbar!你却还在封装BaseActivity? 已是一个月前的事情,当时有人说我是标题党,也有人不认可我的内容,但是这也不并不妨碍我,两天夺得掘金当周周榜第一,并被 鸿洋公众号 转载,累计阅读量超过 5万

    上篇文章的研究成果让 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:x.y.z@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 ✊ 感谢!


    Hello 我叫Jessyan,如果您喜欢我的文章,可以在以下平台关注我

    -- The end

    相关文章

      网友评论

      • 相互交流:楼主思想很不错,但是oKHttp必须引入,虽然你没有引入进去,只是减少了你的库的大小,开发者还不是要自己引用,这个其实我感觉意义不大,而且网络访问框架也绑定了,如果能像那个RxJava2一样可以适应不同的网络框架请求做适配器那就很完美了..
        JessYan:@相互交流 减少包体积,只是 compileOnly 达到上面两个主要特性的产物,一个次要原因,上面两个特性才是 Google 创造这个功能的意义,根本的因数并不是你所说的减少包体积,所以我觉你根本没理解 compileOnly 的意义
        JessYan:@相互交流 我使用 compileOnly 依赖 okhttp ,并不是为了让使用者觉得我没依赖其他库,而是为了避免重复依赖和版本冲突
        JessYan:@相互交流 本来就是为 okhttp 而设计的,如果要写一个所有支持所有网络库的抽象层,我才觉得意义不大,工作量大不多说,现在除了那种很老的项目,基本都是使用的 okhttp 和 retrofit
      • 6cd941e3d78b:大神啊,我用你的框架,但是遇到Glide加载图片只显示图片,进度没有显示,这是什么问题呢。我用的Glide3.7,okhttp3.7
      • 木子而东:okhttp在多文件上传的时候使用的同一个RequestBody 上传的吗,进度只需要RequestBody 里main叠加吗
      • 68db9c2eff1a:思路值得学习:+1:
      • AWeiLoveAndroid:博主就是厉害。。
      • Small_Cake:对于大型项目,思想确实不错!但对于迭代多,更新换代快的小项目却不合适!特别是要求:开发者必须有使用 Dagger2 , Rxjava , Retrofit 的经验,如果我只使用其中一种或两种,并不愿也不想全都使用,这个时候框架就有点过于庞大了,更喜欢插件式的!感谢作者思想的共享,但我看了看,还是觉得不想引入这个。
        JessYan:@Small_Cake 嗯,这很正常,每个人有每个人不同的环境和思考问题的角度,以及需求也不尽相同,所以也不强求,不用,思想也是可以互相学习的,大部分使用者都是用作新项目,以学习新知识作为主要目的,还是很欣慰,现在框架有上千个公司或个人在使用,能让它们能互相鼓励和成长,面试时有更佳大的优势,自己的每天坚持维护也是值得的
      • Small_Cake:就喜欢你一本正经的吹牛逼~!
      • 万象归真:楼主是真大神啊。你是怎么学习的?需要学习哪些知识点?能否分享下?
        JessYan:我的精华都在 MVPArms 里面
      • UP7CR:牛逼,喜欢看思路
      • 不催流弊:前几天还在github的项目上扒怎么弄。
        github维护的那哥们还非常不理解为什么有人需要百分比进度。
        基本思路也是类似,都是重写下接收数据的部分。
        https://gist.github.com/TWiStErRob/08d5807e396740e52c90
        o动感超人o:@不催流弊 外国网速快吧。。。MB级别的东西很快就下来了不需要看进度。。。
        不催流弊: @jessyan 你发的这个,下面不是还圈出来了原作者么。看样子原作者应该就是glide项目组的维护人。不过他也没说明白为啥他们项目组不原生提供进度支持。老外认为有了开始和结束状态不就行了?要啥进度啊。互相表示歪果仁的思想搞不懂。。。
        JessYan:@不催流弊 这个我之前看过,从这个库看到的 https://github.com/shangmingchao/ProgressGlide ,没想到原作者是你发的这个人,这个人基本什么都没改就把别人的东西封装成了一个库
      • db83ecb6a69e:GlideConfiguration 这个类是在哪里调用的?
        JessYan:@战国吃熊 glide 自己会调用
      • Android_Romance:赶紧 跑来点个赞:smiley:
      • 搬砖的乐趣:弘洋那里来的,博主真厉害
      • loveExpert: ProgressManager.getInstance().addResponseListener(DOWNLOAD_URL, getDownloadListener());
        新手求教,DOWNLOAD_URL怎么获取呀,当前retrofit的url;
        JessYan:@loveExpert 那我想知道不用拼就可以得到你这个url的方法,找到了麻烦告诉我
        loveExpert:@jessyan 我的意思是
        Retrofit retrofit = BaseApiServiceModule.provideRetrofit();
        DownUploadService downUploadService = retrofit.create(DownUploadService.class);

        @Multipart
        @post("DownUploadServlet")
        Observable<UploadResult> uploadFile(@Part MultipartBody.Part file,

        url 已经在 service 里面了,怎么从service里面拿url.不要自己再拼接哟
        JessYan:@loveExpert 这个url,是你自己app里面的,你自己想办法拿到啊,你问我,我也很懵逼
      • b09dbea7de6a:想和作者搞基
        JessYan:拒绝:smirk:
      • liunewshine:很多思想值得借鉴,我多文件上传就是循环调用同一个url,也可以完美解决
        JessYan:@liunewshine :grin:
      • woodnaonly:加油,你是最胖的
        JessYan:@woodnaonly :scream:
      • 路人葵:赞一个~一直觉得一个好的过程 远高于其结果的价值
        JessYan:是的,复制代码毫无意义,传承思想才是一篇文章的核心价值
      • 会理发的店小二:真心不错,从有需求,到分析问题,最后解决问题,完成需求,提供很多思路。
        JessYan:@会理发的店小二 嗯,这篇文章没有讲具体细节,主要分享,确定需求,需求分析,以及最后优化的思路

      本文标题:一行代码实现Okhttp,Retrofit,Glide下载上传进

      本文链接:https://www.haomeiwen.com/subject/htmkqxtx.html