2020 上半年百度 Android 岗(初级到高级)面试真题全收录
1.7 布局为什么会导致卡顿,你又是如何优化的?
分析完布局的加载流程之后,我们发现有如下四点可能会导致布局卡顿:1、首先,系统会将我们的 Xml 文件通过 IO 的方式映射的方式加载到我们的内存当中,而 IO 的过程可能会导致卡顿。2、其次,布局加载的过程是一个反射的过程,而反射的过程也会可能会导致卡顿。3、同时,这个布局的层级如果比较深,那么进行布局遍历的过程就会比较耗时。4、最后,不合理的嵌套 RelativeLayout 布局也会导致重绘的次数过多。
对此,我们的优化方式有如下几种:
1、针对布局加载 Xml 文件的优化,我们使用了异步 Inflate 的方式,即 AsyncLayoutInflater。它的核心原理是在子线程中对我们的 Layout 进行加载,而加载完成之后会将 View 通过 Handler 发送到主线程来使用。所以不会阻塞我们的主线程,加载的时间全部是在异步线程中进行消耗的。而这仅仅是一个从侧面缓解的思路。2、后面,我们发现了一个从根源解决上述痛点的方式,即使用 X2C 框架。它的一个核心原理就是在开发过程我们还是使用的 XML 进行编写布局,但是在编译的时候它会使用 APT 的方式将 XML 布局转换为 Java 的方式进行布局,通过这样的方式去写布局,它有以下优点:1、它省去了使用 IO 的方式去加载 XML 布局的耗时过程。2、它是采用 Java 代码直接 new 的方式去创建控件对象,所以它也没有反射带来的性能损耗。这样就从根本上解决了布局加载过程中带来的问题。3、然后,我们可以使用 ConstraintLayout 去减少我们界面布局的嵌套层级,如果原始布局层级越深,它能减少的层级就越多。而使用它也能避免嵌套 RelativeLayout 布局导致的重绘次数过多。4、最后,我们可以使用 AspectJ 框架(即 AOP)和 LayoutInflaterCompat.setFactory2 的方式分别去建立线下全局的布局加载速度和控件加载速度的监控体系。
1.8 你是怎么做卡顿优化的?
从项目的初期到壮大期,最后再到成熟期,每一个阶段都针对卡顿优化做了不同的处理。各个阶段所做的事情如下所示:1、系统工具定位、解决 2、自动化卡顿方案及优化 3、线上监控及线下监测工具的建设
我做卡顿优化也是经历了一些阶段,最初我们的项目当中的一些模块出现了卡顿之后,我是通过系统工具进行了定位,我使用了 Systrace,然后看了卡顿周期内的 CPU 状况,同时结合代码,对这个模块进行了重构,将部分代码进行了异步和延迟,在项目初期就是这样解决了问题。但是呢,随着我们项目的扩大,线下卡顿的问题也越来越多,同时,在线上,也有卡顿的反馈,但是线上的反馈卡顿,我们在线下难以复现,于是我们开始寻找自动化的卡顿监测方案,其思路是来自于 Android 的消息处理机制,主线程执行任何代码都会回到 Looper.loop 方法当中,而这个方法中有一个 mLogging 对象,它会在每个 message 的执行前后都会被调用,我们就是利用这个前后处理的时机来做到的自动化监测方案的。同时,在这个阶段,我们也完善了线上 ANR 的上报,我们采取的方式就是监控 ANR 的信息,同时结合了 ANR-WatchDog,作为高版本没有文件权限的一个补充方案。在做完这个卡顿检测方案之后呢,我们还做了线上监控及线下检测工具的建设,最终实现了一整套完善,多维度的解决方案。
1.9 你是怎么样自动化的获取卡顿信息?
我们的思路是来自于 Android 的消息处理机制,主线程执行任何代码它都会走到 Looper.loop 方法当中,而这个函数当中有一个 mLogging 对象,它会在每个 message 处理前后都会被调用,而主线程发生了卡顿,那就一定会在 dispatchMessage 方法中执行了耗时的代码,那我们在这个 message 执行之前呢,我们可以在子线程当中去 postDelayed 一个任务,这个 Delayed 的时间就是我们设定的阈值,如果主线程的 messaege 在这个阈值之内完成了,那就取消掉这个子线程当中的任务,如果主线程的 message 在阈值之内没有被完成,那子线程当中的任务就会被执行,它会获取到当前主线程执行的一个堆栈,那我们就可以知道哪里发生了卡顿。
经过实践,我们发现这种方案获取的堆栈信息它不一定是准确的,因为获取到的堆栈信息它很可能是主线程最终执行的一个位置,而真正耗时的地方其实已经执行完成了,于是呢,我们就对这个方案做了一些优化,我们采取了高频采集的方案,也就是在一个周期内我们会多次采集主线程的堆栈信息,如果发生了卡顿,那我们就将这些卡顿信息压缩之后上报给 APM 后台,然后找出重复的堆栈信息,这些重复发生的堆栈大概率就是卡顿发生的一个位置,这样就提高了获取卡顿信息的一个准确性。
1.10TextView setText 耗时的原因,对 TextView 绘制层源码的理解?
1.11 移动端获取网络数据优化的几个点
1、连接复用:节省连接建立时间,如开启 keep-alive。于 Android 来说默认情况下 HttpURLConnection 和 HttpClient 都开启了 keep-alive。只是 2.2 之前 HttpURLConnection 存在影响连接池的 Bug。2、请求合并:即将多个请求合并为一个进行请求,比较常见的就是网页中的 CSS Image Sprites。如果某个页面内请求过多,也可以考虑做一定的请求合并。3、减少请求数据的大小:对于 post 请求,body 可以做 gzip 压缩的,header 也可以做数据压缩(不过只支持 http 2.0)。返回数据的 body 也可以做 gzip 压缩,body 数据体积可以缩小到原来的 30%左右(也可以考虑压缩返回的 json 数据的 key 数据的体积,尤其是针对返回数据格式变化不大的情况,支付宝聊天返回的数据用到了)。4、根据用户的当前的网络质量来判断下载什么质量的图片(电商用的比较多)。5、使用 HttpDNS 优化 DNS:DNS 存在解析慢和 DNS 劫持等问题,DNS 不仅支持 UDP,它还支持 TCP,但是大部分标准的 DNS 都是基于 UDP 与 DNS 服务器的 53 端口进行交互。HTTPDNS 则不同,顾名思义它是利用 HTTP 协议与 DNS 服务器的 80 端口进行交互。不走传统的 DNS 解析,从而绕过运营商的 LocalDNS 服务器,有效的防止了域名劫持,提高域名解析的效率。
1.12 关于网络优化可参照百度 APP 网络优化
[网络优化最佳实践!百度 App 网络深度优化系列《一》DNS 优化](
)[网络优化最佳实践!百度 App 网络深度优化系列《二》连接优化](
)[网络优化最佳实践!百度 App 网络深度优化系列《三》弱网优化](
)
1.13APP 安全优化
1、[提高 app 安全性的方法?](
)2、[安卓的 app 加固如何做?](
)3、[安卓的混淆原理是什么?](
)4、[谈谈你对安卓签名的理解。](
)
1.14 为什么 WebView 加载会慢呢?
这是因为在客户端中,加载 H5 页面之前,需要先初始化 WebView,在 WebView 完全初始化完成之前,后续的界面加载过程都是被阻塞的。
优化手段围绕着以下两个点进行:预加载 WebView。加载 WebView 的同时,请求 H5 页面数据。
因此常见的方法是:全局 WebView。客户端代理页面请求。WebView 初始化完成后向客户端请求数据。asset 存放离线包。
除此之外还有一些其他的优化手段:脚本执行慢,可以让脚本最后运行,不阻塞页面解析。DNS 链接慢,可以让客户端复用使用的域名与链接。React 框架代码执行慢,可以将这部分代码拆分出来,提前进行解析。
1.15 如何优化自定义 View
为了加速你的 view,对于频繁调用的方法,需要尽量减少不必要的代码。先从 onDraw 开始,需要特别注意不应该在这里做内存分配的事情,因为它会导致 GC,从而导致卡顿。在初始化或者动画间隙期间做分配内存的动作。不要在动画正在执行的时候做内存分配的事情。
你还需要尽可能的减少 onDraw 被调用的次数,大多数时候导致 onDraw 都是因为调用了 invalidate().因此请尽量减少调用 invaildate()的次数。如果可能的话,尽量调用含有 4 个参数的 invalidate()方法而不是没有参数的 invalidate()。没有参数的 invalidate 会强制重绘整个 view。
另外一个非常耗时的操作是请求 layout。任何时候执行 requestLayout(),会使得 Android UI 系统去遍历整个 View 的层级来计算出每一个 view 的大小。如果找到有冲突的值,它会需要重新计算好几次。另外需要尽量保持 View 的层级是扁平化的,这样对提高效率很有帮助。
如果你有一个复杂的 UI,你应该考虑写一个自定义的 ViewGroup 来执行他的 layout 操作。与内置的 view 不同,自定义的 view 可以使得程序仅仅测量这一部分,这避免了遍历整个 view 的层级结构来计算大小。
1.16TraceView 的实现原理,分析数据误差来源。
[Andorid 性能优化之 traceview](
)
2.Android Framework 相关
2.1View 的事件分发机制?滑动冲突怎么解决?
触摸事件对应的是 MotionEvent 类,事件的类型主要有如下三种:ACTION_DOWNACTION_MOVE(移动的距离超过一定的阈值会被判定为 ACTION_MOVE 操作)ACTION_UPView 事件分发本质就是对 MotionEvent 事件分发的过程。即当一个 MotionEvent 发生后,系统将这个点击事件传递到一个具体的 View 上。
事件分发流程事件分发过程由三个方法共同完成:dispatchTouchEvent:方法返回值为 true 表示事件被当前视图消费掉;返回为 super.dispatchTouchEvent 表示继续分发该事件,返回为 false 表示交给父类的 onTouchEvent 处理。onInterceptTouchEvent:方法返回值为 true 表示拦截这个事件并交由自身的 onTouchEvent 方法进行消费;返回 false 表示不拦截,需要继续传递给子视图。如果 return super.onInterceptTouchEvent(ev), 事件拦截分两种情况: ?1.如果该 View 存在子 View 且点击到了该子 View, 则不拦截, 继续分发给子 View 处理, 此时相当于 return false。2.如果该 View 没有子 View 或者有子 View 但是没有点击中子 View(此时 ViewGroup 相当于普通 View), 则交由该 View 的 onTouchEvent 响应,此时相当于 return true。
注意:一般的 LinearLayout、 RelativeLayout、FrameLayout 等 ViewGroup 默认不拦截, 而 ScrollView、ListView 等 ViewGroup 则可能拦截,得看具体情况。onTouchEvent:方法返回值为 true 表示当前视图可以处理对应的事件;返回值为 false 表示当前视图不处理这个事件,它会被传递给父视图的 onTouchEvent 方法进行处理。如果 return super.onTouchEvent(ev),事件处理分为两种情况:1.如果该 View 是 clickable 或者 longclickable 的,则会返回 true, 表示消费了该事件, 与返回 true 一样;2.如果该 View 不是 clickable 或者 longclickable 的,则会返回 false, 表示不消费该事件,将会向上传递,与返回 false 一样。
注意:在 Android 系统中,拥有事件传递处理能力的类有以下三种:Activity:拥有分发和消费两个方法。ViewGroup:拥有分发、拦截和消费三个方法。View:拥有分发、消费两个方法。
2.2 如何解决 View 的事件冲突?
常见开发中事件冲突的有 ScrollView 与 RecyclerView 的滑动冲突、RecyclerView 内嵌同时滑动同一方向。
滑动冲突的处理规则:对于由于外部滑动和内部滑动方向不一致导致的滑动冲突,可以根据滑动的方向判断谁来拦截事件。对于由于外部滑动方向和内部滑动方向一致导致的滑动冲突,可以根据业务需求,规定何时让外部 View 拦截事件,何时由内部 View 拦截事件。对于上面两种情况的嵌套,相对复杂,可同样根据需求在业务上找到突破点。
滑动冲突的实现方法:外部拦截法:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。具体方法:需要重写父容器的 onInterceptTouchEvent 方法,在内部做出相应的拦截。内部拦截法:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。具体方法:需要配合 requestDisallowInterceptTouchEvent 方法。
2.3View 的绘制流程?
DecorView 被加载到 Window 中
从 Activity 的 startActivity 开始,最终调用到 ActivityThread 的 handleLaunchActivity 方法来创建 Activity,首先,会调用 performLaunchActivity 方法,内部会执行 Activity 的 onCreate 方法,从而完成 DecorView 和 Activity 的创建。然后,会调用 handleResumeActivity,里面首先会调用 performResumeActivity 去执行 Activity 的 onResume()方法,执行完后会得到一个 ActivityClientRecord 对象,然后通过 r.window.getDecorView()的方式得到 DecorView,然后会通过 a.getWindowManager()得到 WindowManager,最终调用其 addView()方法将 DecorView 加进去。
WindowManager 的实现类是 WindowManagerImpl,它内部会将 addView 的逻辑委托给 WindowManagerGlobal,可见这里使用了接口隔离和委托模式将实现和抽象充分解耦。在 WindowManagerGlobal 的 addView()方法中不仅会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 通过 root.setView()把 DecorView 加载到 Window 中。这里的 ViewRootImpl 是 ViewRoot 的实现类,是连接 WindowManager 和 DecorView 的纽带。View 的三大流程均是通过 ViewRoot 来完成的。
2.4Android 中进程和线程的关系?区别?
线程是 CPU 调度的最小单元,同时线程是一种有限的系统资源;而进程一般指一个执行单元,在 PC 和移动设备上指一个程序或者一个应用。
一般来说,一个 App 程序至少有一个进程,一个进程至少有一个线程(包含与被包含的关系),通俗来讲就是,在 App 这个工厂里面有一个进程,线程就是里面的生产线,但主线程(即主生产线)只有一条,而子线程(即副生产线)可以有多个。
进程有自己独立的地址空间,而进程中的线程共享此地址空间,都可以并发执行。
2.5 为何需要 IPC?多进程通信可能会出现的问题?
所有运行在不同进程的四大组件(Activity、Service、Receiver、ContentProvider)共享数据都会失败,这是由于 Android 为每个应用分配了独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这会导致在不同的虚拟机中访问同一个类的对象会产生多份副本。比如常用例子(通过开启多进程获取更大内存空间、两个或者多个应用之间共享数据、微信全家桶)。
一般来说,使用多进程通信会造成如下几方面的问题:
静态成员和单例模式完全失效:独立的虚拟机造成。
线程同步机制完全失效:独立的虚拟机造成。
SharedPreferences 的可靠性下降:这是因为 Sp 不支持两个进程并发进行读写,有一定几率导致数据丢失。
Application 会多次创建:Android 系统在创建新的进程时会分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程,自然也会创建新的 Application。
2.6 为什么选择 Binder?为什么选用 Binder,在讨论这个问题之前,我们知道 Android 也是基于 Linux 内核,Linux 现有的进程通信手段有以下几种:
管道:在创建时分配一个 page 大小的内存,缓存区大小比较有限;
消息队列:信息复制两次,额外的 CPU 消耗;不合适频繁或信息量大的通信;
共享内存:无须复制,共享缓冲区直接附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决;
套接字:作为更通用的接口,传输效率低,主要用于不同机器或跨网络的通信;
信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等;
既然有现有的 IPC 方式,为什么重新设计一套 Binder 机制呢。主要是出于以上三个方面的考量:
1、效率:传输效率主要影响因素是内存拷贝的次数,拷贝次数越少,传输速率越高。从 Android 进程架构角度分析:对于消息队列、Socket 和管道来说,数据先从发送方的缓存区拷贝到内核开辟的缓存区中,再从内核缓存区拷贝到接收方的缓存区,一共两次拷贝,如图:
而对于 Binder 来说,数据从发送方的缓存区拷贝到内核的缓存区,而接收方的缓存区与内核的缓存区是映射到同一块物理地址的,节省了一次数据拷贝的过程,如图:
![](https://upload-images.jianshu.io/up
load_images/22976303-98c7b7e26d7a87b1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
共享内存不需要拷贝,Binder 的性能仅次于共享内存。
2、稳定性:上面说到共享内存的性能优于 Binder,那为什么不采用共享内存呢,因为共享内存需要处理并发同步问题,容易出现死锁和资源竞争,稳定性较差。Socket 虽然是基于 C/S 架构的,但是它主要是用于网络间的通信且传输效率较低。Binder 基于 C/S 架构 ,Server 端与 Client 端相对独立,稳定性较好。
3、安全性:传统 Linux IPC 的接收方无法获得对方进程可靠的 UID/PID,从而无法鉴别对方身份;而 Binder 机制为每个进程分配了 UID/PID,且在 Binder 通信时会根据 UID/PID 进行有效性检测。
Binder 机制的作用和原理?
Linux 系统将一个进程分为用户空间和内核空间。对于进程之间来说,用户空间的数据不可共享,内核空间的数据可共享,为了保证安全性和独立性,一个进程不能直接操作或者访问另一个进程,即 Android 的进程是相互独立、隔离的,这就需要跨进程之间的数据通信方式。普通的跨进程通信方式一般需要 2 次内存拷贝,如下图所示:
一次完整的 Binder IPC 通信过程通常是这样:
首先 Binder 驱动在内核空间创建一个数据接收缓存区。
接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系。
发送方进程通过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
Binder 框架中 ServiceManager 的作用?
Binder 框架 是基于 C/S 架构的。由一系列的组件组成,包括 Client、Server、ServiceManager、Binder 驱动,其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。如下图所示:
Server&Client:服务器 &客户端。在 Binder 驱动和 Service Manager 提供的基础设施上,进行 Client-Server 之间的通信。
ServiceManager(如同 DNS 域名服务器)服务的管理者,将 Binder 名字转换为 Client 中对该 Binder 的引用,使得 Client 可以通过 Binder 名字获得 Server 中 Binder 实体的引用。
Binder 驱动(如同路由器):负责进程之间 binder 通信的建立,计数管理以及数据的传递交互等底层支持。
最后,结合[Android 跨进程通信:图文详解 Binder 机制](
) 的总结图来综合理解一下:
2.7AMS 家族重要术语解释。
1.ActivityManagerServices,简称 AMS,服务端对象,负责系统中所有 Activity 的生命周期。2.ActivityThread,App 的真正入口。当开启 App 之后,调用 main()开始运行,开启消息循环队列,这就是传说的 UI 线程或者叫主线程。与 ActivityManagerService 一起完成 Activity 的管理工作。3.ApplicationThread,用来实现 ActivityManagerServie 与 ActivityThread 之间的交互。在 ActivityManagerSevice 需要管理相关 Application 中的 Activity 的生命周期时,通过 ApplicationThread 的代理对象与 ActivityThread 通信。4.ApplicationThreadProxy,是 ApplicationThread 在服务器端的代理,负责和客户端的 ApplicationThread 通信。AMS 就是通过该代理与 ActivityThread 进行通信的。5.Instrumentation,每一个应用程序只有一个 Instrumetation 对象,每个 Activity 内都有一个对该对象的引用,Instrumentation 可以理解为应用进程的管家,ActivityThread 要创建或暂停某个 Activity 时,都需要通过 Instrumentation 来进行具体的操作。6.ActivityStack,Activity 在 AMS 的栈管理,用来记录经启动的 Activity 的先后关系,状态信息等。通过 ActivtyStack 决定是否需要启动新的进程。7.ActivityRecord,ActivityStack 的管理对象,每个 Acivity 在 AMS 对应一个 ActivityRecord,来记录 Activity 状态以及其他的管理信息。其实就是服务器端的 Activit 对象的映像。8.TaskRecord,AMS 抽象出来的一个“任务”的概念,是记录 ActivityRecord 的栈,一个“Task”包含若干个 ActivityRecord。AMS 用 TaskRecord 确保 Activity 启动和退出的顺序。如果你清楚 Activity 的 4 种 launchMode,那么对这概念应该不陌生。
2.8ActivityThread 工作原理。
2.9 广播发送和接收的原理了解吗?
继承 BroadcastReceiver,重写 onReceive()方法。
通过 Binder 机制向 ActivityManagerService 注册广播。
通过 Binder 机制向 ActivityMangerService 发送广播。
ActivityManagerService 查找符合相应条件的广播(IntentFilter/Permission)的 BroadcastReceiver,将广播发送到 BroadcastReceiver 所在的消息队列中。
BroadcastReceiver 所在消息队列拿到此广播后,回调它的 onReceive()方法。
2.10apk 打包流程
1.通过 AAPT 工具进行资源文件(包括 AndroidManifest.xml、布局文件、各种 xml 资源等)的打包,生成 R.java 文件。2.通过 AIDL 工具处理 AIDL 文件,生成相应的 Java 文件。3.通过 Java Compiler 编译 R.java、Java 接口文件、Java 源文件,生成.class 文件。4.通过 dex 命令,将.class 文件和第三方库中的.class 文件处理生成 classes.dex,该过程主要完成 Java 字节码转换成 Dalvik 字节码,压缩常量池以及清除冗余信息等工作。5.通过 ApkBuilder 工具将资源文件、DEX 文件打包生成 APK 文件。6.通过 Jarsigner 工具,利用 KeyStore 对生成的 APK 文件进行签名。7.如果是正式版的 APK,还会利用 ZipAlign 工具进行对齐处理,对齐的过程就是将 APK 文件中所有的资源文件距离文件的起始距位置都偏移 4 字节的整数倍,这样通过内存映射访问 APK 文件的速度会更快,并且会减少其在设备上运行时的内存占用。
2.11 说下安卓虚拟机和 java 虚拟机的原理和不同点?
[JVM、Davilk、ART 三者的原理和区别](
)
JVM 和 Dalvik 虚拟机的区别 JVM:.java -> javac -> .class -> jar -> .jar 架构: 堆和栈的架构.DVM:.java -> javac -> .class -> dx.bat -> .dex 架构: 寄存器(cpu 上的一块高速缓存)
3.Android 优秀三方库源码及架构设计
3.1 原理性问题
[网络底层框架:OkHttp 实现原理](
)[网络封装框架:Retrofit 实现原理](
)[响应式编程框架:RxJava 实现原理](
)[图片加载框架:Glide 实现原理](
)[事件总线框架:EventBus 实现原理](
)[内存泄漏检测框架:LeakCanary 实现原理](
)[依赖注入框架:ButterKnife 实现原理](
)[依赖全局管理框架:Dagger2 实现原理](
)[数据库框架:GreenDao 实现原理](
)[MVC MVP MVVM 原理和区别?](
)
3.2 为什么要在项目中使用 OKHTTP 这个库?
1.OkHttp 提供了对最新的 HTTP 协议版本 HTTP/2 和 SPDY 的支持,这使得对同一个主机发出的所有请求都可以共享相同的套接字连接。2.如果 HTTP/2 和 SPDY 不可用,OkHttp 会使用连接池来复用连接以提高效率。3.OkHttp 提供了对 GZIP 的默认支持来降低传输内容的大小。4.OkHttp 也提供了对 HTTP 响应的缓存机制,可以避免不必要的网络请求。5.当网络出现问题时,OkHttp 会自动重试一个主机的多个 IP 地址。
3.3OKhttp 针对网络层有哪些优化?
3.4 网络请求缓存处理,okhttp 如何处理网络缓存的?
3.5 从网络加载一个 10M 的图片,说下注意事项?
3.6WebSocket 与 socket 的区别?
3.7Retrofit 优势
1、功能强大:支持同步、异步支持多种数据的解析 & 序列化格式支持 RxJava2、简洁易用:通过注解配置网络请求参数采用大量设计模式简化使用
评论