Android 启动优化(六) - 深入理解布局优化
Android 启动优化(五) - AnchorTask 1.0.0 版本更新了
本文字数: 7.4k 阅读时长 ≈ 7 分钟
Android 启动优化(五) - AnchorTask 1.0.0 版本更新了
Android 启动优化(四) - 手把手教你实现 AnchorTask
本文字数: 8.7k 阅读时长 ≈ 8 分钟
Android 启动优化(四) - 手把手教你实现 AnchorTask
Android 启动优化(三) - AnchorTask 使用说明
本文字数: 4.8k 阅读时长 ≈ 4 分钟
Android 启动优化(三) - AnchorTask 使用说明
Android 启动优化(二) - 拓扑排序的原理以及解题思路
本文字数: 4.4k 阅读时长 ≈ 4 分钟
Android 启动优化(二) - 拓扑排序的原理以及解题思路
Android 启动优化(一) - 有向无环图
本文字数: 2.2k 阅读时长 ≈ 2 分钟
Android 启动优化(一) - 有向无环图
面试官, 怎样实现 Router 框架?
本文字数: 16k 阅读时长 ≈ 15 分钟
Android 开发中,组件化,模块化是一个老生常谈的问题。随着项目复杂性的增长,模块化是一个必然的趋势。除非你能忍受改一下代码,就需要六七分钟的漫长时间。
模块化,组件化随之带来的另外一个问题是页面的跳转问题,由于代码的隔离,代码之间有时候会无法互相访问。于是,路由(Router)框架诞生了。
目前用得比较多的有阿里的 ARouter,美团的 WMRouter,ActivityRouter 等。
今天,就让我们一起来看一下怎样实现一个路由框架。
实现的功能有。
- 基于编译时注解,使用方便
- 结果回调,每次跳转 Activity 都会回调跳转结果
- 除了可以使用注解自定义路由,还支持手动分配路由
- 支持多模块使用,支持组件化使用
使用说明
基本使用
第一步,在要跳转的 activity 上面注明 path,
1 | @Route(path = "activity/main") |
在要跳转的地方
1 | Router.getInstance().build("activity/main").navigation(this); |
如果想在多 moule 中使用
第一步,使用 @Modules({"app", "sdk"})
注明总共有多少个 moudle,并分别在 moudle 中注明当前 moudle 的 名字,使用 @Module("")
注解。注意 @Modules({“app”, “sdk”}) 要与 @Module(“”) 一一对应。
在主 moudle 中,
1 | @Modules({"app", "moudle1"}) |
在 moudle1 中,
1 | @Route(path = "my/activity/main") |
这样就可以支持多模块使用了。
自定义注入 router
1 | Router.getInstance().add("activity/three", ThreeActivity.class); |
跳转的时候调用
1 | Router.getInstance().build("activity/three").navigation(this); |
结果回调
路由跳转结果回调。
1 | Router.getInstance().build("my/activity/main", new RouterCallback() { |
startActivityForResult 跳转结果回调
1 | Router.getInstance().build("activity/two").navigation(this, new Callback() { |
原理说明
实现一个 Router 框架,涉及到的主要的知识点如下:
- 注解的处理
- 怎样解决多个 module 之间的依赖问题,以及如何支持多 module 使用
- router 跳转及 activty startActivityForResult 的处理
我们带着这三个问题,一起来探索一下。
总共分为四个部分,router-annotion, router-compiler,router-api,stub
router-annotion 主要是定义注解的,用来存放注解文件
router-compiler 主要是用来处理注解的,自动帮我们生成代码
router-api 是对外的 api,用来处理跳转的。
stub 这个是存放一些空的 java 文件,提前占坑。不会打包进 jar。
router-annotion
主要定义了三个注解
1 | @Target({ElementType.TYPE}) |
1 | @Retention(RetentionPolicy.CLASS) |
1 | @Retention(RetentionPolicy.CLASS) |
Route 注解主要是用来注明跳转的 path 的。
Modules 注解,注明总共有多少个 moudle。
Module 注解,注明当前 moudle 的名字。
Modules,Module 注解主要是为了解决支持多 module 使用的。
router-compiler
router-compiler 只有一个类 RouterProcessor,他的原理其实也是比较简单的,扫描那些类用到注解,并将这些信息存起来,做相应的处理。这里是会生成相应的 java 文件。
主要包括以下两个步骤
- 根据是否有
@Modules
@Module
注解,然后生成相应的RouterInit
文件 - 扫描
@Route
注解,并根据moudleName
生成相应的 java 文件
注解基本介绍
在讲解 RouterProcessor 之前,我们先来了解一下注解的基本知识。
如果对于自定义注解还不熟悉的话,可以先看我之前写的这两篇文章。Android 自定义编译时注解1 - 简单的例子,Android 编译时注解 —— 语法详解
1 | public class RouterProcessor extends AbstractProcessor { |
首先我们先来看一下 getSupportedAnnotationTypes
方法,这个方法返回的是我们支持扫描的注解。
注解的处理
接下来我们再一起来看一下 process
方法
1 | @Override |
,首先判断是否有注解需要处理,没有的话直接返回 annotations == null || annotations.size() == 0
。
接着我们会判断是否有 @Modules
注解(这种情况是多个 moudle 使用),有的话会调用 generateModulesRouterInit(String[] moduleNames)
方法生成 RouterInit java 文件,当没有 @Modules
注解,并且没有 @Module
(这种情况是单个 moudle 使用),会生成默认的 RouterInit 文件。
1 | private void generateModulesRouterInit(String[] moduleNames) { |
假设说我们有”app”,”moudle1” 两个 moudle,那么我们最终生成的代码是这样的。
1 | public final class RouterInit { |
如果我们都没有使用 @Moudles 和 @Module 注解,那么生成的 RouterInit 文件大概是这样的。
1 | public final class RouterInit { |
这也就是为什么有 stub module 的原因。因为默认情况下,我们需要借助 RouterInit 去初始化 map。如果没有这两个文件,ide 编辑器 在 compile 的时候就会报错。
1 | compileOnly project(path: ':stub') |
我们引入的方式是使用 compileOnly,这样的话再生成 jar 的时候,不会包括这两个文件,但是可以在 ide 编辑器中运行。这也是一个小技巧。
Route 注解的处理
我们回过来看 process 方法连对 Route 注解的处理。
1 | // 扫描 Route 自己注解 |
首先会扫描所有的 Route 注解,并添加到 targetInfos list 当中,接着调用 generateCode
方法生成相应的文件。
1 | private void generateCode(List<TargetInfo> targetInfos, String moduleName) { |
这个方法主要是使用 javapoet 生成 java 文件,关于 javaposet 的使用可以见官网文档,生成的 java 文件是这样的。
1 | package com.xj.router.impl; |
可以看到我们定义的注解信息,最终都会调用 Router.getInstance().add()
方法存放起来。
router-api
这个 module 主要是多外暴露的 api,最主要的一个文件是 Router。
1 | public class Router { |
当我们想要初始化 Router 的时候,代用 init 方法即可。 init 方法会先判断是否初始化过,没有初始化过,会调用 RouterInit#init 方法区初始化。
而在 RouterInit#init 中,会调用 RouterMap_{@moduleName}#map 方法初始化,改方法又调用 Router.getInstance().add()
方法,从而完成初始化
router 跳转回调
1 | public interface RouterCallback { |
1 | public void navigation(Activity context, int requestCode, Callback callback) { |
主要看 navigation 方法,在跳转 activity 的时候,首先会会调用
beforeOpen 方法回调 RouterCallback#beforeOpen。接着 catch exception 的时候,如果发生错误,会调用 errorOpen 方法回调 RouterCallback#errorOpen 方法。同时调用 tryToCallNotFind 方法判断是否是 ClassNotFoundException,是的话回调 RouterCallback#notFind。
如果没有发生 eception,会回调 RouterCallback#afterOpen。
Activity 的 startActivityForResult 回调
可以看到我们的 Router 也是支持 startActivityForResult 的
1 | Router.getInstance().build("activity/two").navigation(this, new Callback() { |
它的实现原理其实很简单,是借助一个空白 fragment 实现的,原理的可以看我之前的这一篇文章。
Android Fragment 的妙用 - 优雅地申请权限和处理 onActivityResult
小结
如果觉得效果不错的话,请到 github 上面 star, 谢谢。 Router
我们的 Router 框架,流程大概是这样的。
题外话
看了上面的文章,文章一开头提到的三个问题,你懂了吗,欢迎在评论区留言评论。
- 注解的处理
- 怎样解决多个 module 之间的依赖问题,以及如何支持多 module 使用
- router 跳转及 activty startActivityForResult 的处理
其实,现在很多 router 框架都借助 gradle 插件来实现。这样有一个好处,就是在多 moudle 使用的时候,我们只需要 apply plugin
就 ok,对外屏蔽了一些细节。但其实,他的原理跟我们上面的原理都是差不多的。
接下来,我也会写 gradle plugin 相关的文章,并借助 gradle 实现 Router 框架。有兴趣的话可以关注我的微信公众号,徐公码字,谢谢。
相关文章
Android Fragment 的妙用 - 优雅地申请权限和处理 onActivityResult
扫一扫,欢迎关注我的微信公众号 stormjun94(徐公码字), 目前是一名程序员,不仅分享 Android开发相关知识,同时还分享技术人成长历程,包括个人总结,职场经验,面试经验等,希望能让你少走一点弯路。
面试官,https 真的安全吗,可以抓包吗,如何防止抓包
本文字数: 5.2k 阅读时长 ≈ 5 分钟
@[toc]
往期文章
Android 面试必备 - http 与 https 协议
Android 面试必备 - 计算机网络基本知识(TCP,UDP,Http,https)
Android 面试必备 - 系统、App、Activity 启动过程
面试官问, https 真的安全吗,可以抓包吗,如何防止抓包吗
我的 CSDN 博客:https://blog.csdn.net/gdutxiaoxu
我的掘金:https://juejin.im/user/58aa8508570c35006bbd9e03
github: https://github.com/gdutxiaoxu/
微信公众号:徐公码字(stormjun94)
知乎:https://www.zhihu.com/people/xujun94
有兴趣的话可以关注我的公众号 徐公码字(stormjun94),第一时间会在上面更新
前言
转眼间,2020 年已过去一大半了,2020 年很难,各企业裁员的消息蛮多的,降职,不发年终奖等等。2020 年确实是艰难的一年。然而生活总是要继续,时间不给你丧的机会!如果我们能坚持下来,不断提高自己,说不定会有新的机会。
面试中,网络(http, https, tcp, udp), jvm, 类加载机制等这些基础的知识点是高频出现的,每个程序员都能说上好多。但不一定说到重点,以及理解背后的原理。
我在面试的过程中也经常被问到,于是总结记录了下来。千万不要小瞧这些基础,有时候,你算法,项目经验都过了,但是基础答得不太好。结果可能会通过,但这肯定会影响你的评级,这是特别吃亏的。所以,不如花点时间背一下,理解一下背后的原理。
举一个简单的例子, https 连接过程是怎样的,使用了了哪种加密方式,可以抓包吗,怎样防止抓包,你是否能够对答如下。
废话不多说,开始进入正文。
背景
我们知道,http 通信存在以下问题:
- 通信使用明文可能会被窃听
- 不验证通信方的身份可能遭遇伪装
- 无法证明报文的完整型,可能已遭篡改
使用 https 可以解决数据安全问题,但是你真的理解 https 吗?
当面试官连续对你发出灵魂追问的时候,你能对答如流吗
- 什么是 https,为什么需要 https
- https 的连接过程
- https 的加密方式是怎样的,对称加密和非对称加密,为什么要这样设计?内容传输为什么要使用对称机密
- https 是绝对安全的吗
- https 可以抓包吗
如果你能对答自如,恭喜你,https 你已经掌握得差不多了,足够应付面试了。
什么是 https
简单来说, https 是 http + ssl,对 http 通信内容进行加密,是HTTP的安全版,是使用TLS/SSL加密的HTTP协议
Https的作用:
- 内容加密 建立一个信息安全通道,来保证数据传输的安全;
- 身份认证 确认网站的真实性
- 数据完整性 防止内容被第三方冒充或者篡改
什么是SSL
SSL 由 Netscape 公司于1994年创建,它旨在通过Web创建安全的Internet通信。它是一种标准协议,用于加密浏览器和服务器之间的通信。它允许通过Internet安全轻松地传输账号密码、银行卡、手机号等私密信息。
SSL证书就是遵守SSL协议,由受信任的CA机构颁发的数字证书。
SSL/TLS的工作原理:
需要理解SSL/TLS的工作原理,我们需要掌握加密算法。加密算法有两种:对称加密和非对称加密:
对称加密:通信双方使用相同的密钥进行加密。特点是加密速度快,但是缺点是需要保护好密钥,如果密钥泄露的话,那么加密就会被别人破解。常见的对称加密有AES,DES算法。
非对称加密:它需要生成两个密钥:公钥(Public Key)和私钥(Private Key)。
公钥顾名思义是公开的,任何人都可以获得,而私钥是私人保管的。相信大多程序员已经对这种算法很熟悉了:我们提交代码到github的时候,就可以使用SSH key:在本地生成私钥和公钥,私钥放在本地.ssh目录中,公钥放在github网站上,这样每次提交代码,不用麻烦的输入用户名和密码了,github会根据网站上存储的公钥来识别我们的身份。
公钥负责加密,私钥负责解密;或者,私钥负责加密,公钥负责解密。这种加密算法安全性更高,但是计算量相比对称加密大很多,加密和解密都很慢。常见的非对称算法有RSA。
https 的连接过程
https 的连接过程大概分为两个阶段,证书验证阶段和数据传输阶段
证书验证阶段
大概分为三个步骤
- 浏览器发起请求
- 服务器接收到请求之后,会返回证书,包括公钥
- 浏览器接收到证书之后,会检验证书是否合法,不合法的话,会弹出告警提示(怎样验证合法,下文会详细解析,这里先忽略)
数据传输阶段
证书验证合法之后
- 浏览器会生成一个随机数,
- 使用公钥进行加密,发送给服务端
- 服务器收到浏览器发来的值,使用私钥进行解密
- 解析成功之后,使用对称加密算法进行加密,传输给客户端
之后双方通信就使用第一步生成的随机数进行加密通信。
https 的加密方式是怎样的,对称加密和非对称加密,为什么要这样设计
从上面我们可以知道,https 加密是采用对称加密和非对称机密一起结合的。
在证书验证阶段,使用非对称加密。
在数据传输阶段,使用对称机密。
这样设计有一个好处,能最大程度得兼顾安全效率。
在证书验证阶段,使用非对称加密,需要公钥和私钥,假如浏览器的公钥泄漏了,我们还是能够确保随机数的安全,因为加密的数据只有用私钥才能解密。这样能最大程度确保随机数的安全。
在内容传输阶段,使用对称机密,可以大大提高加解密的效率。
内容传输为什么要使用对称机密
- 对称加密效率比较高
- 一对公私钥只能实现单向的加解密。只有服务端保存了私钥。如果使用非对称机密,相当于客户端必须有自己的私钥,这样设计的话,每个客户端都有自己的私钥,这很明显是不合理的,因为私钥是需要申请的。
https 是绝对安全的吗
不是绝对安全的,可以通过中间人攻击。
什么是中间人攻击
中间人攻击是指攻击者与通讯的两端分别创建独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制。
HTTPS 使用了 SSL 加密协议,是一种非常安全的机制,目前并没有方法直接对这个协议进行攻击,一般都是在建立 SSL 连接时,拦截客户端的请求,利用中间人获取到 CA证书、非对称加密的公钥、对称加密的密钥;有了这些条件,就可以对请求和响应进行拦截和篡改。
过程原理:
- 本地请求被劫持(如DNS劫持等),所有请求均发送到中间人的服务器
- 中间人服务器返回中间人自己的证书
- 客户端创建随机数,通过中间人证书的公钥对随机数加密后传送给中间人,然后凭随机数构造对称加密对传输内容进行加密传输
- 中间人因为拥有客户端的随机数,可以通过对称加密算法进行内容解密
- 中间人以客户端的请求内容再向正规网站发起请求
- 因为中间人与服务器的通信过程是合法的,正规网站通过建立的安全通道返回加密后的数据
- 中间人凭借与正规网站建立的对称加密算法对内容进行解密
- 中间人通过与客户端建立的对称加密算法对正规内容返回的数据进行加密传输
- 客户端通过与中间人建立的对称加密算法对返回结果数据进行解密
由于缺少对证书的验证,所以客户端虽然发起的是 HTTPS 请求,但客户端完全不知道自己的网络已被拦截,传输内容被中间人全部窃取。
https 是如何防止中间人攻击的
在https中需要证书,证书的作用是为了防止”中间人攻击”的。 如果有个中间人M拦截客户端请求,然后M向客户端提供自己的公钥,M再向服务端请求公钥,作为”中介者” 这样客户端和服务端都不知道,信息已经被拦截获取了。这时候就需要证明服务端的公钥是正确的.
怎么证明呢?
就需要权威第三方机构来公正了.这个第三方机构就是CA. 也就是说CA是专门对公钥进行认证,进行担保的,也就是专门给公钥做担保的担保公司。 全球知名的CA也就100多个,这些CA都是全球都认可的,比如VeriSign、GlobalSign等,国内知名的CA有WoSign。
浏览器是如何确保CA证书的合法性?
一、证书包含什么信息?
颁发机构信息、公钥、公司信息、域名、有效期、指纹……
二、证书的合法性依据是什么?
首先,权威机构是要有认证的,不是随便一个机构都有资格颁发证书,不然也不叫做权威机构。另外,证书的可信性基于信任制,权威机构需要对其颁发的证书进行信用背书,只要是权威机构生成的证书,我们就认为是合法的。所以权威机构会对申请者的信息进行审核,不同等级的权威机构对审核的要求也不一样,于是证书也分为免费的、便宜的和贵的。
三、浏览器如何验证证书的合法性?
浏览器发起HTTPS请求时,服务器会返回网站的SSL证书,浏览器需要对证书做以下验证:
- 验证域名、有效期等信息是否正确。证书上都有包含这些信息,比较容易完成验证;
- 判断证书来源是否合法。每份签发证书都可以根据验证链查找到对应的根证书,操作系统、浏览器会在本地存储权威机构的根证书,利用本地根证书可以对对应机构签发证书完成来源验证;
- 判断证书是否被篡改。需要与CA服务器进行校验;
- 判断证书是否已吊销。通过CRL(Certificate Revocation List 证书注销列表)和 OCSP(Online Certificate Status Protocol 在线证书状态协议)实现,其中 OCSP 可用于第3步中以减少与CA服务器的交互,提高验证效率。
以上任意一步都满足的情况下浏览器才认为证书是合法的。
https 可以抓包吗
HTTPS 的数据是加密的,常规下抓包工具代理请求后抓到的包内容是加密状态,无法直接查看。
但是,我们可以通过抓包工具来抓包。它的原理其实是模拟一个中间人。
通常 HTTPS 抓包工具的使用方法是会生成一个证书,用户需要手动把证书安装到客户端中,然后终端发起的所有请求通过该证书完成与抓包工具的交互,然后抓包工具再转发请求到服务器,最后把服务器返回的结果在控制台输出后再返回给终端,从而完成整个请求的闭环。
关于 httpps 抓包的原理可以看这一篇文章。
有人可能会问了,既然 HTTPS 不能防抓包,那 HTTPS 有什么意义?
HTTPS 可以防止用户在不知情的情况下通信链路被监听,对于主动授信的抓包操作是不提供防护的,因为这个场景用户是已经对风险知情。要防止被抓包,需要采用应用级的安全防护,例如采用私有的对称加密,同时做好移动端的防反编译加固,防止本地算法被破解。
扩展
如何防止抓包?
对于HTTPS API接口,如何防止抓包呢?既然问题出在证书信任问题上,那么解决方法就是在我们的APP中预置证书。在TLS/SSL握手时,用预置在本地的证书中的公钥校验服务器的数字签名,只有签名通过才能成功握手。由于数字签名是使用私钥生成的,而私钥只掌握在我们手上,中间人无法伪造一个有效的签名,因此攻击失败,无法抓包。
同时,为了防止预置证书被替换,在证书存储上,可以将证书进行加密后进行「嵌入存储」,如嵌入在图片中或一段语音中。这涉及到信息隐写的领域,这个话题我们有空了详细说。
关于 Android 中Https 请求如何防止中间人攻击和Charles抓包,可以看一下这一篇文章。
Android中Https请求如何防止中间人攻击和Charles抓包原理
预置证书/公钥更新问题
这样做虽然解决了抓包问题,但是也带来了另外一个问题:我们购买的证书都是有有效期的,到期前需要对证书进行更新。主要有两种方式:
提供预置证书更新接口。在当前证书快过期时,APP请求获取新的预置证书,这过渡时期,两个证书同时有效,直到安全完成证书切换。这种方式有一定的维护成本,且不易测试。
在APP中只预埋公钥,这样只要私钥不变,即使证书更新也不用更新该公钥。但是,这样不太符合周期性更新私钥的安全审计需求。一个折中的方法是,一次性预置多个公钥,只要任意一个公钥验证通过即可。考虑到我们的证书一般购买周期是3-5年,那么3个公钥,可以使用9-15年,同时,我们在此期间还可以发布新版本废弃老公钥,添加新公钥,这样可以使公钥一直更新下去。
小结
开头说到的几个问题,你能对答如流了吗
- 什么是 https,为什么需要 https
- https 的连接过程
- https 的加密方式是怎样的,对称加密和非对称加密,为什么要这样设计?内容传输为什么要使用对称机密
- https 是绝对安全的吗
- https 可以抓包吗
面试官,你真的了解 http 吗
本文字数: 5.6k 阅读时长 ≈ 5 分钟
@[toc]
我的 CSDN 博客:https://blog.csdn.net/gdutxiaoxu
我的掘金:https://juejin.im/user/58aa8508570c35006bbd9e03
github: https://github.com/gdutxiaoxu/
微信公众号:徐公码字(stormjun94)
知乎:https://www.zhihu.com/people/xujun94
往期文章
Android 面试必备 - http 与 https 协议
Android 面试必备 - 计算机网络基本知识(TCP,UDP,Http,https)
Android 面试必备 - 系统、App、Activity 启动过程
有兴趣的话可以关注我的公众号 徐公码字(stormjun94),第一时间会在上面更新
面试常见
一道经典的面试题
还记得这道经典的面试题目吗?从 URL 在浏览器被被输入到页面展现的过程中发生了什么?
总体来说分为以下几个过程:
- DNS 解析:将域名解析成 IP 地址
- TCP 连接:TCP 三次握手
- 发送 HTTP 请求
- 服务器处理请求并返回 HTTP 报文
- 浏览器解析渲染页面
- 断开连接:TCP 四次挥手
完整的可以看以下下面的图片
http 必备基础知识
HTTP 是一种 超文本传输协议(Hypertext Transfer Protocol),HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范
HTTP 主要内容分为三部分,超文本(Hypertext)、传输(Transfer)、协议(Protocol)。
- 超文本就是不单单只是本文,它还可以传输图片、音频、视频,甚至点击文字或图片能够进行超链接的跳转。
- 上面这些概念可以统称为数据,传输就是数据需要经过一系列的物理介质从一个端系统传送到另外一个端系统的过程。通常我们把传输数据包的一方称为请求方,把接到二进制数据包的一方称为应答方。
- 而协议指的就是是网络中(包括互联网)传递、管理信息的一些规范。如同人与人之间相互交流是需要遵循一定的规矩一样,计算机之间的相互通信需要共同遵守一定的规则,这些规则就称为协议,只不过是网络协议。
什么是无状态协议,HTTP 是无状态协议吗,怎么解决
无状态协议(Stateless Protocol) 就是指浏览器对于事务的处理没有记忆能力。举个例子来说就是比如客户请求获得网页之后关闭浏览器,然后再次启动浏览器,登录该网站,但是服务器并不知道客户关闭了一次浏览器。
HTTP 就是一种无状态的协议,他对用户的操作没有记忆能力。可能大多数用户不相信,他可能觉得每次输入用户名和密码登陆一个网站后,下次登陆就不再重新输入用户名和密码了。这其实不是 HTTP 做的事情,起作用的是一个叫做 小甜饼(Cookie) 的机制。它能够让浏览器具有记忆能力。
如果你的浏览器允许 cookie 的话,查看方式 chrome://settings/content/cookies
几种方法
HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法
HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT
- GET: 通常用于请求服务器发送某些资源
- HEAD: 请求资源的头部信息, 并且这些头部与 HTTP GET 方法请求时返回的一致. 该请求方法的一个使用场景是在下载一个大文件前先获取其大小再决定是否要下载, 以此可以节约带宽资源
- OPTIONS: 用于获取目的资源所支持的通信选项
- POST: 发送数据给服务器,是非幂等的
- PUT: 跟POST方法很像,也是想服务器提交数据。但是,它们之间有不同。PUT指定了资源在服务器上的位置,而POST不需要置顶资源在服务器的位置,是幂等的
- DELETE: 用于删除指定的资源
- PATCH: 用于对资源进行部分修改
- CONNECT: HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器
- TRACE: 回显服务器收到的请求,主要用于测试或诊断
http get 和 post 区别
Post一般用于更新或者添加资源信息 | Get一般用于查询操作,而且应该是安全和幂等的 |
---|---|
Post更加安全 | Get会把请求的信息放到URL的后面 |
Post传输量一般无大小限制 | Get不能大于2KB |
Post执行效率低 | Get执行效率略高 |
http put 和 post 区别
举一个简单的例子
POST:用于提交请求,可以更新或者创建资源,是非幂等的
举个例子,在我们的支付系统中,一个api的功能是创建收款金额二维码,它和金额相关,每个用户可以有多个二维码,如果连续调用则会创建新的二维码,这个时候就用POST
PUT: 用于向指定的URI传送更新资源,是幂等的
还是那个例子,用户的账户二维码只和用户关联,而且是一一对应的关系,此时这个api就可以用PUT,因为每次调用它,都将刷新用户账户二维码
如果从 RESTful API 的角度来理解,PUT 方法是这么工作的:
把一个对象 V 绑定到地址 K 上;今后请求地址 K 时,就会返回对象 V。
如果地址 K 之前曾绑定过另一个对象,比如 V0,那么 V0 会被 V 替换。
举一个简单的例子,假设我的博客后台支持 RESTful API,我可以通过下面的请求发布这篇文章:
1 | PUT https://gdutxiao.github.io/2018/04/16/http-put-vs-post HTTP/1.1 |
可以看出,使用 PUT 方法时,客户端需要在 HTTP 请求中明确指定地址 K。
正如 Java 的例子一样,PUT 方法应当支持幂等性。如果是同一个对象 V,PUT 多次与 PUT 一次返回的结果应该是相同的。客户端可以利用 PUT 的幂等性安全地重试请求,保证客户端的请求至少被服务端处理一次。
如果把上面发布文章的例子用 HTTP POST 方法重写,它可能会是下面这样:
1 | POST https://gdutxiao.github.io/post-article HTTP/1.1 |
也就是说,地址 K 不是由客户端指定的,而是由服务端生成的。比如,服务端可能会根据日期和文章标题,为本文分配一个地址。
另外,与 PUT 方法不同,POST 方法是不支持幂等性的。同一个请求被处理两次,应当生成两份对象。换句话说,客户端应该只发送一次 POST 请求,而客户端的请求至多会被服务端处理一次。
现在问题来了,如果真的遇到了网络故障,客户端应该如何重试 POST 请求呢?解决方法其实很简单,我们可以在 POST 请求中隐藏一个唯一的 token,服务端在处理请求后把 token 存入数据库,如果这个 token 之前遇到过,服务端就知道这是重复的 POST 请求,可以不再处理了。
http 版本
1.0 与 1.1
- http1.0一次只能处理一个请求,不能同时收发数据
- http1.1可以处理多个请求,能同时收发数据
- http1.1增加可更多字段,如cache-control,keep-alive.
2.0
- http 2.0采用二进制的格式传送数据,不再使用文本格式传送数据
- http2.0对消息头采用hpack压缩算法,http1.x的版本消息头带有大量的冗余消息
- http2.0 采用多路复用,即用一个tcp连接处理所有的请求,真正意义上做到了并发请求,流还支持优先级和流量控制(HTTP/1.x 虽然通过 pipeline也能并发请求,但是多个请求之间的响应会被阻塞的,所以 pipeline 至今也没有被普及应用,而 HTTP/2 做到了真正的并发请求。同时,流还支持优先级和流量控制。)
- http2.0支持server push,服务端可以主动把css,jsp文件主动推送到客户端,不需要客户端解析HTML,再发送请求,当客户端需要的时候,它已经在客户端了。
缺点
虽然 HTTP/2 解决了很多之前旧版本的问题,但是它还是存在一个巨大的问题, 主要是底层支撑的 TCP 协议造成的
。HTTP/2的缺点主要有以下几点:
- TCP 以及 TCP+TLS建立连接的延时
HTTP/2使用TCP协议来传输的,而如果使用HTTPS的话,还需要使用TLS协议进行安全传输,而使用TLS也需要一个握手过程,
这样就需要有两个握手延迟过程 :
①在建立TCP连接的时候,需要和服务器进行三次握手来确认连接成功,也就是说需要在消耗完1.5个RTT之后才能进行数据传输。
②进行TLS连接,TLS有两个版本——TLS1.2和TLS1.3,每个版本建立连接所花的时间不同,大致是需要1~2个RTT。
总之,在传输数据之前,我们需要花掉 3~4 个 RTT。
- TCP的队头阻塞并没有彻底解决
上文我们提到在HTTP/2中,多个请求是跑在一个TCP管道中的。但当出现了丢包时,HTTP/2 的表现反倒不如 HTTP/1
了。因为TCP为了保证可靠传输,有个特别的“丢包重传”机制,丢失的包必须要等待重新传输确认,HTTP/2出现丢包时,整个 TCP
都要开始等待重传,那么就会阻塞该TCP连接中的所有请求(如下图)。而对于 HTTP/1.1 来说,可以开启多个 TCP
连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。
Http 3.0
Google 在推SPDY的时候就已经意识到了这些问题,于是就另起炉灶搞了一个基于 UDP 协议的“QUIC”协议,让HTTP跑在QUIC上而不是TCP上。
而这个“HTTP over QUIC”就是HTTP协议的下一个大版本,HTTP/3。它在HTTP/2的基础上又实现了质的飞跃,真正“完美”地解决了“队头阻塞”问题。
QUIC 虽然基于 UDP,但是在原本的基础上新增了很多功能,接下来我们重点介绍几个QUIC新功能。不过HTTP/3目前还处于草案阶段,正式发布前可能会有变动,所以本文尽量不涉及那些不稳定的细节。
QUIC新功能
上面我们提到QUIC基于UDP,而UDP是“无连接”的,根本就不需要“握手”和“挥手”,所以就比TCP来得快。此外QUIC也实现了可靠传输,保证数据一定能够抵达目的地。它还引入了类似HTTP/2的“流”和“多路复用”,单个“流”是有序的,可能会因为丢包而阻塞,但其他“流”不会受到影响。具体来说QUIC协议有以下特点:
- 实现了类似TCP的流量控制、传输可靠性的功能。
虽然UDP不提供可靠性的传输,但QUIC在UDP的基础之上增加了一层来保证数据可靠性传输。它提供了数据包重传、拥塞控制以及其他一些TCP中存在的特性。
- 实现了快速握手功能。
由于QUIC是基于UDP的,所以QUIC可以实现使用0-RTT或者1-RTT来建立连接,这意味着QUIC可以用最快的速度来发送和接收数据,这样可以大大提升首次打开页面的速度。
0RTT 建连可以说是 QUIC 相比 HTTP2 最大的性能优势 。
- 集成了TLS加密功能。
目前QUIC使用的是TLS1.3,相较于早期版本TLS1.3有更多的优点,其中最重要的一点是减少了握手所花费的RTT个数。
- 多路复用,彻底解决TCP中队头阻塞的问题
和TCP不同,QUIC实现了在同一物理连接上可以有多个独立的逻辑数据流(如下图)。实现了数据流的单独传输,就解决了TCP中队头阻塞的问题。
关于 http 3.0 的,如果想了解更多,可以查看这一篇文章。解密HTTP/2与HTTP/3 的新特性
总结
- HTTP/1.1有两个主要的缺点:安全不足和性能不高。
- HTTP/2完全兼容HTTP/1,是“更安全的HTTP、更快的HTTPS”,头部压缩、多路复用等技术可以充分利用带宽,降低延迟,从而大幅度提高上网体验;
- QUIC 基于 UDP 实现,是 HTTP/3 中的底层支撑协议,该协议基于 UDP,又取了 TCP 中的精华,实现了即快又可靠的协议
http 状态码
Http 状态码 | 含义 |
---|---|
200 | 请求成功 |
206 | 支持断点下载(range = byte = 0 -1024) |
301 | 永远移动 |
302 | 临时移动 |
303 | See Other 查看其它地址。与301类似。使用GET和POST请求查看 |
304 | 无更新 |
400 | Bad request,服务器无法识别 |
403 | 禁止访问 |
404 | not found |
405 | Method Not Allowed 客户端请求中的方法被禁止 |
500 | Internal Server Error 服务器内部错误,无法完成请求 |
关于更详细的可以查看
题外话
下一篇预告,将会推出 面试官系列 - https 真的安全吗,可以抓包吗,如何防止抓包。
有兴趣的话可以关注我的公众号 徐公码字(stormjun94)
目前从事于 Android 开发,除了分享 Android开发相关知识,还有职场心得,面试经验,学习心得,人生感悟等等。希望通过该公众号,让你看到程序猿不一样的一面,我们不只会敲代码,我们还会。。。。。。,期待你的参与
Android LiveData 源码分析
本文字数: 11k 阅读时长 ≈ 10 分钟
说在前面
本次推出 Android Architecture Components 系列文章,目前写好了四篇,主要是关于 lifecycle,livedata 的使用和源码分析,其余的 Navigation, Paging library,Room,WorkMannager 等春节结束之后会更新,欢迎关注我的公众号,有更新的话会第一时间会在公众号上面通知。
github sample 地址: ArchiteComponentsSample
Android 技术人,一位不羁的码农。
前言
在前面三篇博客中,我们已经介绍了 lifecycle 的使用及原理,livedata ,ViewModel 的常用用法,今天,让我们一起来学习 livedata 的原理。
我们先来回顾一下 LiveData 的特点:
LiveData 是一个可以被观察的数据持有类,它可以感知 Activity、Fragment或Service 等组件的生命周期。
- 它可以做到在组件处于激活状态的时候才会回调相应的方法,从而刷新相应的 UI。
- 不用担心发生内存泄漏
- 当 config 导致 activity 重新创建的时候,不需要手动取处理数据的储存和恢复。内部已经帮我们封装好了。
- 当 Actiivty 不是处于激活状态的时候,如果你想 livedata setValue 之后立即回调 obsever 的 onChange 方法,而不是等到 Activity 处于激活状态的时候才回调 obsever 的 onChange 方法,你可以使用 observeForever 方法,但是你必须在 onDestroy 的时候 removeObserver
下面,让我们一步步解剖它
原理分析
我们知道 livedata 的使用很简单,它是采用观察者模式实现的
- 添加观察者
- 在数据改变的时候设置 value,这样会回调 Observer 的 onChanged 方法
1 | MutableLiveData<String> nameEvent = mTestViewModel.getNameEvent(); |
observe 方法
1 | @MainThread |
首先,我们先来看一下它的 observe 方法,首先通过 owner.getLifecycle().getCurrentState() 获取状态,判断是否已经被销毁,如果已经被销毁,直接返回。接着用 LifecycleBoundObserver 包装起来。然后从缓存的 mObservers 中读取 observer,如果有,证明已经添加过了。
observe 方法,小结起来就是
- 判断是否已经销毁,如果销毁,直接移除
- 用 LifecycleBoundObserver 包装传递进来的 observer
- 是否已经添加过,添加过,直接返回
- 将包装后的 LifecycleBoundObserver 添加进去
因此,当 owner 你(Activity 或者 fragment) 生命周期变化的时候,会回调 LifecycleBoundObserver 的 onStateChanged 方法,onStateChanged 方法又会回调 observer 的 onChange 方法
LifecycleBoundObserver
1 | class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver { |
我们来看一下 LifecycleBoundObserver,继承 ObserverWrapper,实现了 GenericLifecycleObserver 接口。而 GenericLifecycleObserver 接口又实现了 LifecycleObserver 接口。 它包装了我们外部的 observer,有点类似于代理模式。
GenericLifecycleObserver#onStateChanged
Activity 回调周期变化的时候,会回调 onStateChanged ,会先判断 mOwner.getLifecycle().getCurrentState() 是否已经 destroy 了,如果。已经 destroy,直接移除观察者。这也就是为什么我们不需要手动 remove observer 的原因。
如果不是销毁状态,会调用 activeStateChanged 方法 ,携带的参数为 shouldBeActive() 返回的值。
而当 lifecycle 的 state 为 started 或者 resume 的时候,shouldBeActive 方法的返回值为 true,即表示激活。
1 | void activeStateChanged(boolean newActive) { |
}
activeStateChanged 方法中,,当 newActive 为 true,并且不等于上一次的值,会增加 LiveData 的 mActiveCount 计数。接着可以看到,onActive 会在 mActiveCount 为 1 时触发,onInactive 方法则只会在 mActiveCount 为 0 时触发。即回调 onActive 方法的时候活跃的 observer 恰好为 1,回调 onInactive 方法的时候,没有一个 Observer 处于激活状态。
当 mActive 为 true 时,会促发 dispatchingValue 方法。
dispatchingValue
1 | private void dispatchingValue(@Nullable ObserverWrapper initiator) { |
其中 mDispatchingValue, mDispatchInvalidated 只在 dispatchingValue 方法中使用,显然这两个变量是为了防止重复分发相同的内容。当 initiator 不为 null,只处理当前 observer,为 null 的时候,遍历所有的 obsever,进行分发
considerNotify 方法
1 | private void considerNotify(ObserverWrapper observer) { |
- 如果状态不是在活跃中,直接返回,这也就是为什么当我们的 Activity 处于 onPause, onStop, onDestroy 的时候,不会回调 observer 的 onChange 方法的原因。
- 判断数据是否是最新,如果是最新,返回,不处理
- 数据不是最新,回调 mObserver.onChanged 方法。并将 mData 传递过去
setValue
1 | @MainThread |
setValue 方法中,首先,断言是主线程,接着 mVersion + 1; 并将 value 赋值给 mData,接着调用 dispatchingValue 方法。dispatchingValue 传递 null,代表处理所有 的 observer。
这个时候如果我们依附的 activity 处于 onPause 或者 onStop 的时候,虽然在 dispatchingValue 方法中直接返回,不会调用 observer 的 onChange 方法。但是当所依附的 activity 重新回到前台的时候,会促发 LifecycleBoundObserver onStateChange 方法,onStateChange 又会调用 dispatchingValue 方法,在该方法中,因为 mLastVersion < mVersion。所以会回调 obsever 的 onChange 方法,这也就是 LiveData 设计得比较巧妙的一个地方
同理,当 activity 处于后台的时候,您多次调用 livedata 的 setValue 方法,最终只会回调 livedata observer 的 onChange 方法一次。
postValue
1 | protected void postValue(T value) { |
- 首先,采用同步机制,通过 postTask = mPendingData == NOT_SET 有没有人在处理任务。 true,没人在处理任务, false ,有人在处理任务,有人在处理任务的话,直接返回
- 调用 AppToolkitTaskExecutor.getInstance().postToMainThread 到主线程执行 mPostValueRunnable 任务。
1 | @MainThread |
因为 AlwaysActiveObserver 没有实现 GenericLifecycleObserver 方法接口,所以在 Activity o生命周期变化的时候,不会回调 onStateChange 方法。从而也不会主动 remove 掉 observer。因为我们的 obsever 被 remove 掉是依赖于 Activity 生命周期变化的时候,回调 GenericLifecycleObserver 的 onStateChange 方法。
总结
- liveData 当我们 addObserver 的时候,会用 LifecycleBoundObserver 包装 observer,而 LifecycleBoundObserver 可以感应生命周期,当 activity 生命周期变化的时候,如果不是处于激活状态,判断是否需要 remove 生命周期,需要 remove,不需要,直接返回
- 当处于激活状态的时候,会判断是不是 mVersion最新版本,不是的话需要将上一次缓存的数据通知相应的 observer,并将 mLastVsersion 置为最新
- 当我们调用 setValue 的时候,mVersion +1,如果处于激活状态,直接处理,如果不是处理激活状态,返回,等到下次处于激活状态的时候,在进行相应的处理
- 如果你想 livedata setValue 之后立即回调数据,而不是等到生命周期变化的时候才回调数据,你可以使用 observeForever 方法,但是你必须在 onDestroy 的时候 removeObserver。因为 AlwaysActiveObserver 没有实现 GenericLifecycleObserver 接口,不能感应生命周期。
题外话
Android Architecture Components 已经写了四篇文章了,其余的 Navigation, Paging library,Room,WorkMannager 等春节结束之后再更新了,欢迎关注我的公众号,有更新的话会第一时间在公众好上面更新。