Giter Club home page Giter Club logo

blog's People

Contributors

gieczhang avatar xxm-sz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blog's Issues

Android Jetpack Lifecycle-Aware Components

为什么需要我们管理Activity和Fragment的生命周期?这些不是Framework自动帮我们搞定的么?(手动黑人问号)刚看到这样的标题我也是很懵逼,不就是onCreate->onSart()->onResume()->onPause()->onStop()->onDestory()么?难道还有什么高深的地方么?

Abount Lifecycle-Aware Components

该组件能在像Activity、Fragment等等具有生命周期的组件发生状态改变时,以轻量级的和易维护的代码作出响应动作。

吃个栗子就懂意思了:

//为了考虑大多数同学学习过Java,就贴Java的代码
//定义个监听类,用来在Activity生命周期发生变化时,对定位服务资源作相关处理
class MyLocationListener {
    public MyLocationListener(Context context, Callback callback) {
        // ...
    }

    void start() {
        // connect to system location service
    }

    void stop() {
        // disconnect from system location service
    }
}
//在Activity中回调监听类相关方法
class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    @Override
    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, (location) -> {
            // update UI
        });
    }
    
    @Override
    public void onStart() {
        super.onStart();
        myLocationListener.start();
        // manage other components that need to respond
        // to the activity lifecycle
    }

    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
        // manage other components that need to respond
        // to the activity lifecycle
    }
}

毕竟拿人手短,吃人嘴软,吃了人家的栗子就得给人家分析。

这个栗子还是挺简单的,只是简单调用了MyLocationListener对象的两个回调方法,但是在实际开发中,大多数情况下会在Activity的onCreate()、onStart()等等生命周期中做大量的业务逻辑处理和UI更新,而且如果不止一个监听类需要被回调的话,就意味着要在Activity中管理多个其他组件的生命周期或者回调。那代码的维护性和可读性就非常差。(另外,在发生某种竞态条件的情况下,可能会导致Activity的onStop()方法在onStart()方法前就发生了)

为了解决这种痛点,在包android.arch.lifecycle 中提供类和接口来独立的管理Activity和Fragment组件的生命周期。

Lifecycle

生命周期是一个类包含组件的生命周期状态的信息(例如Activity和Fragment),并允许其他对象观察到这个状态。
Lifecycle对象以两种主要的枚举类型跟踪它关联组件的生命周期状态:

  • Event
    Framework和Lifecycle类分发生命周期事件(Lifecycle Event),并映射到Activity或者Fragment的回调事件。
  • State Lifecycle对象跟踪组件的当前状态。

生命周期状态
方框中的INITALZIEDDESTROYED等等表示组件的状态,而箭头上的ON_CREATEON_START等等则表示事件。假如Framework或者Lifecycle类分发ON_CREATE事件,表示关联的组件的状态从初始化到onCreate状态,对应调用组件的onCreate()方法。

通过在类的方法上添加注解来监听组件的生命周期。然后将该类以观察者形式添加到具有生命周期的类中。

//MyObserver作为一个观察者来监听有生命周期的类
public class MyObserver implements LifecycleObserver {
    @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
    public void connectListener() {
        ...
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
    public void disconnectListener() {
        ...
    }
}
//myLifecycleOwner是一个实现了LifecycleOwner 接口的类,继续看下文
myLifecycleOwner.getLifecycle().addObserver(new MyObserver());

LifecycleOwner

LifecycleOwner,是只有一个getLifecycle()方法的接口,实现该接口的类表示具有生命周期。如果想管理整个应用的生命周期,用 ProcessLifecycleOwner代替LifecycleOwner。LifecycleOwner接口将所有具有生命周期的类的生命周期所有权抽象出来,以方便可以在其生命周期进行读写。实现了LifecycleObserver接口的类可以注册到实现了LifecycleOwner的类,以观察对应组件的生命周期。例如上文的:

myLifecycleOwner.getLifecycle().addObserver(new MyObserver());

这样做带来的好处是什么?
通过这种观察者模式,可以将平常根据Activity或Fragment生命周期状态的逻辑分离到单独类中进行处理,以便更好的逻辑开发、功能迭代和后期维护。

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
            // update UI
        });
        Util.checkUserStatus(result -> {
            if (result) {
                myLocationListener.enable();
            }
        });
  }
}

讲到这里基本就已经知道如何使用Lifecycle-Aware Components管理Activity或Fragment的生命周期了。如果LifecycleObserver需要监听其他Activity或Fragment的生命周期,只需要重新初始化并注册到新的Activity或Fragment即可。资源的设置和清除回收不需要我们担心。

Custom LifecycleOwner

在Support Library 26.1.0和更高版本,Activity和Fragment已经默认实现了LifecycleOwner。如果要实现定制的LifecycleOwner,需要新建LifecycleRegistry对象,并将相关事件传递给它。

public class MyActivity extends Activity implements LifecycleOwner {
    private LifecycleRegistry lifecycleRegistry;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        lifecycleRegistry = new LifecycleRegistry(this);
        lifecycleRegistry.markState(Lifecycle.State.CREATED);
    }

    @Override
    public void onStart() {
        super.onStart();
        lifecycleRegistry.markState(Lifecycle.State.STARTED);
    }

    @NonNull
    @Override
    public Lifecycle getLifecycle() {
        return lifecycleRegistry;
    }
}

最好实践

  • 保持UI(Activity或Fragment)精简。不要在Activity或Fragment中取获取应用数据,采用ViewModel或者Livedata。也就是为什么有MVP、MVVM模式。
  • 尝试写数据驱动类型UI。在数据发生变化的时候通过UI控制器更新UI或者通知用户进行操作。
  • 把数据的处理逻辑放到ViewModel类中。ViewModel作为UI界面与数据的桥梁,处理数据与UI界面交互的逻辑。注意不是获取数据的逻辑,例如获取网络数据或者数据库数据。
  • 使用数据绑定库(Data Binding Library)在UI界面和UI控制器之间进行维护。这样有利于减少在Activity或Fragment中更新UI的代码。在Java中可以使用 Butter Knife库。
  • 如果UI界面很复杂,可以建立一个主持(Presenter)类去处理视图。MVP模式?
  • 使用Kotlin协同机制管理耗时任务。

使用lifecycle-aware components的场景

lifecycle-aware组件在不同情况能让我们更方便的管理Acitivity和Fragment的生命周期。那在什么情景下适合使用该组件呢?

  • 粗细粒度的切换。例如在定位中,如果当前界面可见,那么定位精度应该更细,定位请求更频繁,以提高响应性。当切换到后台时,请求定位的频率就要放缓,以降低功耗
  • 开始和停止视频缓冲。这个是高手,在应用加载的时候尽早缓冲视频,减少用户等待时间在应用退出是结束缓冲。
  • 开始和停止网络连接。根据App状态自动切换连接的状态。
  • 开始和停止绘制图片。应用在后台时不会绘制,返回前台继续绘制。

存在的一些问题

当Fragment或AppCompatActivity在调用onSaveInstanceState()方法保存状态后,它们的视图在ON_START事件被调用之前是不会改变的,在这期间UI被更改会引起不一致的问题,这就是为什么FragmentManager在状态保存后运行FragmentTransaction会抛出异常。

因此如果AppCompatActivity的onStop()方法在调用onSaveInstanceState()方法之后,就会造成一个缺口:UI状态改变是不允许,但生命周期还没有改为CREATED的状态。为此,Lifecycle类通过将状态标记为CREATED,但直到调用onStop()方法前,不分发该事件,此时去检测当前的状态也可获得真实的值。但是还存在以下两个问题:

  • 在Android 6.0和更低版本,系统调用onSaveInstanceState()方法,但它不一定调用onStop()方法。这样就会造成事件无法分发,潜在的导致观察者以为lifecycle处于活动状态,尽管此时它处于停止状态。
  • 任何想要在LiveData暴露类似行为的类必须实现Lifecycle 版本 beta 2 和更低版本提供的方法。

总结

本文是对官网知识的理解或翻译,建议再进一步阅读原文,毕竟原文才是原汁原味,知识点也多。

Google Android Developer 官网

从第一次看官网懵懵懂懂,到开始了解,又掌握一个知识点。不仅光讲Lifecycle-Aware Components的知识点,还讲到平常开发应用的最佳实践,这些对实际开发都有很强指导作用。

坚持初心,写优质好文章

开文有益,Star支持好文

Android 网络编程基础

学习网络编程先要学习相关网络基础协议,而网络协议是一套规定了两个终端之间如何进行连接和数据交互的集合。

1.常见网络分层

不管是OSI七层模型还是TCP/IP的四层、五层模型,每一层中都要自己的专属协议,完成自己相应的工作以及与上下层级之间进行沟通。

1.1 应用层

也包括:表示层和会话层
为操作系统或网络应用程序提供访问网络服务的接口。常用的HTTP协议就是在此层,另外还有FTP、Telnet、DNS、SMTP,POP3等协议。

1.2 传输层

第一个端到端,即主机到主机的层次。传输层负责将上层数据分段并提供端到端的、可靠的或不可靠的传输。此外,传输层还要处理端到端的差错控制和流量控制问题。著名的TCP、UDP协议就位于此。

1.3 网络层

第一个端到端,即主机到主机的层次。传输层负责将上层数据分段并提供端到端的、可靠的或不可靠的传输。此外,传输层还要处理端到端的差错控制和流量控制问题。主要协议有IP协议,ICMP协议,ARP协议,RARP协议。

1.4 网络接口层

数据链路层在物理层提供的服务的基础上向网络层提供服务,其最基本的服务是将源自网络层来的数据可靠地传输到相邻节点的目标机网络层。
物理层具有激活、维持、关闭通信端点之间的机械特性、电气特性、功能特性以及过程特性。主要协议有以太网协议。

2. HTTP与HTTPS协议

先提个醒,Android 9.0默认是不能进行Http网络请求的,所以在适配Android 9.0的时候记得添加配置文件,让其可以进行HTTP网络请求。

2.1 HTTP

HTTP协议是目前互联网上应用最为广泛的网络协议。所有WWW文件都必须遵守这个标准。但HTTP协议是以明文方式发送内容,不提供任何方式的数据加密,因此不适合传输较为敏感的数据。

2.1.1 请求报文

HTTP请求报文分为三部分:请求行,请求头部,请求体。

  • 请求行 是由请求方法、URL、HTTP协议版本组成。
  • 请求头部是有key:value格式的键值对组成。用于告知服务端,客户端的一些信息。
  • 请求数据,当请求行的请求方法是post时,才会有请求体,如果是get数据将携带URL后面。

请求方法

常用的请求方法有POST,GET。GET方法携带数据拼接在URL后面,?连接数据和URL,而数据key和value则用=连接,多对key value 则用&连接。在URL携带数据,有时候会泄漏一些敏感信息。

https://www.google.com/search?name=gitcode8&password=123456

POST方法一般用来提交表单和上传文件数据,数据存放在报文的请求主体中。理论上不存在数据限制。

URL 即要请求的网址

HTTP协议版本 现在基本是 HTTP/1.1

请求头

常见的请求头如下

名称 作用
Content-Type 请求体的类型,如:text/plain,application/json
Accept 说明接收的类型,可以多个值,用","隔开
Content-Length 请求体长度
Content-Encoding 请求体的编码格式,如 gzip、deflate
Accept-Encoding 告知对方我方接受的 Content-Encoding
Catche-Control 取值一般为 no-catche、max-age=xx(xx 为整数,表示自愿缓存 xx 秒)
HTTP请求报文长这样子的:
GET /search?name=gitcode8 HTTP/1.1
Host: www.baidu.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13.6; rv:47.0) 
Accept: text/javascript, application/javascript
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
X-Requested-With: XMLHttpRequest
Connection: keep-alive
Referer: https://www.google.com/
Cookie: FFDB24244D22236694CF00C4877ADA56562322 

2.1.2 响应报文

HTTP响应报文分为三部分:状态行、响应首部、响应数据。


状态码

状态码 描述
1xx 信息提示
2xx 成功,常见200
3xx 重定向,常见302
4xx 客户端错误,常见404
5xx 服务端错误,常见500
响应头

常见响应头

名词 作用
Date 服务器日期
Last-Modified 最后被修改时间
Transfer-Encoding 取值一般为 chunked,一般出现在响应体长度不能确定的情况下
Set-cookie 设置 Cookie
Location 重定向到另一个 URL
Server 服务器信息

响应报文长这样子的:

HTTP/1.1 200 OK
Server: linux/10.18.14
Date: Sun, 31 Jul 2016 03:41:53 GMT
Content-Type: baiduApp/json; v6.27.2.14; charset=UTF-8
Content-Length: 100
Connection: keep-alive
Cache-Control: private
Expires: Sun, 28 Jul 2019 04:41:53 GMT
Set-Cookie: FFDB24244D22236694CF00C4877ADA56562322 

2.2 HTTPS

由于HTTP不是不适合传输敏感数据,因为在应用层和传输层之间增加了TSL/SSL层来加密数据,从而生成加了安全版本的HTTP,即HTTPS。
TSL是SSL的升级版本,TLS/SSL中使用了非对称加密,对称加密以及HASH算法,使用TSL/SSL带来的好处是:

  • 数据加密传输,第三方无法窃取
  • 校验机制,数据被篡改,会立即通知双方
  • 配备身份证书,防止身份被冒充

HTTPS工作原理

  1. 客户端向服务器发起443端口请求,携带客户端支持的加密算法和哈希算法。
  2. 服务器收到请求后,选择客户端的加密算法和哈希算法。
  3. 服务器将自己数字证书发给客户端。证书来源机构或自己制作。
  4. 客户端认证证书。客户端会先从证书列表查找服务器发过来的证书机构,找不到就要求用户手动授权,然后取出公钥。用公钥解密证书的内容和签名,证书内容包括网站的网址、网站的公钥、证书的有效期等,并验证相关信息。最后用公钥加密一个随机数R。
  5. 客户端将加密随机数R发给服务器。
  6. 服务器用私钥解密得到随机数R。
  7. 服务器以R为秘钥使用对称算法加密内容传给客户端。
  8. 客户端以 R 为密钥使用之前约定好的解密算法获取内容。


前 5 步其实就是 HTTPS 的握手过程,这个过程主要是认证服务端证书(内置的公钥)的合法性。因为非对称加密计算量较大,整个通信过程只会用到一次非对称加密算法(主要是用来保护传输客户端生成的用于对称加密的随机数私钥)。后续内容的加解密都是通过一开始约定好的对称加密算法进行的。

HTTPS工作原理

2.3 HTTP与HTTPS区别

大家都这么写得.....

  • HTTPS需要到CA付费申请证书;HTTP不需要。
  • HTTPS使用443端口;HTTP使用80端口。
  • HTTPS具有安全性的ssl加密传输协议;HTTP是超文本传输协议,信息是明文传输。
  • HTTPS使用SSL+HTTP协议构建的可进行加密传输、身份认证的;而HTTP连接简单与无状态。

3. TCP协议

传输控制协议,先了解一下TCP报文首部格式,避免待会下文将三次握手和四次挥手时一头雾水:


自己画的图,再丑也要写完。第四行ACK表示确认标识符,SYN表示同步标识符,FIN表示结束标识符。

3.1 TCP三次握手建立连接

三次握手图例如下,与文字解释配合使用效果更佳。


第一次:客户端发送连接请求报文给服务端,其中SYN=1,seq=x。发送完毕后进入YSN_END状态。

第二次:服务端接收到报文后,发回确认报文,其中ACK=1,ack=x+1,因为需要客户端确认,所以报文中也有SYN=1,seq=y的信息。发送完后进入SYN_RCVD状态。

第三次:客户端接收到报文后,发送确认报文,其中ACK=1,ack=y+1。发送完客户端进入ESTABLISHED状态,服务端接收到报文后,进入ESTABLISHED状态。到此,连接建立完成。

三次握手原因

避免资源被浪费掉。如果在第二步握手时,由于网络延迟导致确认包不能及时到达客户端,那么客户端会认为第一次握手失败,再次发送连接请求,服务端收到后再次发送确认包。在这种情况下,服务端已经创建了两次连接,等待两个客户端发送数据,而实际却只有一个客户端发送数据。

3.2 四次挥手断开连接

四次挥手指客户端和服务端各发送一次请求终止连接的报文,同时双方响应彼此的请求。
四次挥手图例如下,请配置文字解释使用哦。
四次挥手图例
第一次挥手:客户端发送FIN=1,seq=x的包给服务端,表示自己没有数据要进行传输,单面连接传输要关闭。发送完后,客户端进入FIN_WAIT_1状态。

第二次挥手:服务端收到请求包后,发回ACK=1,ack=x+1的确认包,表示确认断开连接。服务端进入CLOSE_WAIT状态。客户端收到该包后,进入FIN_WAIT_2状态。此时客户端到服务端的数据连接已断开。

第三次挥手:服务端发送FIN=1,seq=y的包给客户端,表示自己没有数据要给客户端了。发送完后进入LAST_ACK状态,等待客户端的确认包。

第四次挥手:客户端收到请求包后,发送ACK=1,ack=y+1的确认包给服务端,并进入TIME_WAIT状态,有可能要重传确认包。服务端收到确认包后,进入CLOSED状态,服务端到客户端的连接已断开。客户端等到一段时间后也会进入CLOSED状态。

四次挥手原因
由于TCP的连接是全双工,双方都可以主动传输数据,一方的断开需要告知对方,让对方可以相关操作,负责任的表现。

使用TCP协议有:FTP(文件传输协议)、Telnet(远程登录协议)、SMTP(简单邮件传输协议)、POP3(和SMTP相对,用于接收邮件)、HTTP协议等

4. UDP协议

用户数据报协议,UDP面向无连接的传输,对数据的传输结果不提供保证,支持多播和广播。每个UDP报文分UDP报头和UDP数据区两部分。报头由四个16位长(2字节)字段组成,分别说明该报文的源端口、目的端口、报文长度以及校验值。

使用UDP协议包括:TFTP(简单文件传输协议)、SNMP(简单网络管理协议)、DNS(域名解析协议)、NFS、BOOTP。

TCP与UDP的区别

  • TCP面向连接,UDP面向无连接
  • TCP提供可靠数据服务,UDP不提供可靠数据服务

4. IP协议

4.1 IP地址

在数据链路层中,每台电脑都有唯一网卡MAC,用于标识电脑的唯一性。在网络层则通过IP地址来定位每一台需要在互联网联网的设备。
IP地址共分A、B、C、D四类。

  • A类地址是首位以“0”开头的地址,从第1位到第8位是它的网络标识符,0.0.0.0~127.0.0.0。
  • B 类 IP 地址是前两位 “10” 的地址,从第 1 位到第 16 位是它的网络标识,十进制标识B类的网络地址的范围128.0.0.0~191.255.0.0
  • C 类 IP 地址是前三位为 “110” 的地址。从第 1 位到第 24 位是它的网络标识。十进制标识C类的网络地址的范围是192.0.0.0~223.255.255.0。我国大多数IP地址在此类别。
  • D 类 IP 地址是前四位为 “1110” 的地址。从第 1 位到第 32 位是它的网络标识。十进制标识D类的网络地址的范围是224.0.0.0~239.255.255.255。因此D类地址没有主机标识符,常用于多播。

网络标识必须保证相互连接的每个段的地址不相重复。而相同段内相连的主机必须有相同的网络标识。IP 地址的“主机标识”则不允许在同一个网段内重复出现。由此,可以通过设置网络地址和主机地址,在相互连接的整个网络中保证每台主机的 IP 地址都不会相互重叠。即 IP 地址具有了唯一性。

计算机网络层相关协议

在TCP/IP协议族中,两大重要协议就是传输层的TCP协议和网络层的IP协议。本文主要讲网络层的IP协议以及其他相关协议。

IP协议

IP协议作为TCP/IP协议中最重要协议之一,理解和掌握IP协议相关知识点,在实际项目工作中,能起到指导性作用。

分类IP地址

分类地址即将32位IP地址划分成两个固定长度的块,每个块代表不同的含义。其中一个称为网络地址,用来标明主机所接入的网络,该网络地址在全球唯一的;另一个称为主机号,标识接入前面网络地址的主机。从这种两级的IP地址结构可以看出,IP地址在互联网上都是唯一的。常见的分类地址就是A类、B类、C类。


上图中,A类、B类、C类都是单播地址,网络号前面0、10、110值固定的,永久不会改变。其对应的网络号位数也分别是8、16、24位。网络号所占位数越多,能接入的主机就越少,毕竟IP地址固定为32位。
32位二进制的记法并不适合我们人类阅读,从引进方便人类阅读的点分十进制,也就是我们熟悉的样子:128.11.3.31。


在A类地址中,网络号占8位,第一位固定为0,那么还剩下7位。由于网络号全为0代表本地(“this”)和网络号全为1(0111 1111)代表本地软件的环回地址。所以可分配的网络号数为27-2。与网络号类似,主机号全零的IP地址表示“本机”所连接到的单个网络地址;而全1则表示该网络号上的所有主机。因此A类的主机数为224-2。

在B类地址中,网络号占16位,即两个字节,前两位固定,那么可分配的网络号数为214(因为网络号后面14位无论如何取值,都不会出现全零和全1的状况,所以不需要减2。但实际上128.0.0.0网络地址是不指派的,所以网络地址总数应该是214-1。那么主机号数共216-2。

C类地址与B类地址类似,所以网络地址总数为224-1,主机数为28-2。

D类地址常用于多播,即一对多通信。

IP数据报格式

通过IP数据报的格式可以知道IP数据报具有什么功能。

IP数据报由首部和数据两部分组成,首部固定20个字节,也就是说IP数据报最小长度20个字节。

  • 版本:长度共4位,指定IP协议的版本,例如IPv4(版本号为4)和IPv6(版本号为6),通信双方使用的IP协议版本必须一致。

  • 首部长度:长度共4位,因为首部存在可变部分,所以首部长度=固定部分(20字节)+可变部分长度。因为固定部分长度20个字节,所有首部长度最小值为5(因为首部长度字段 所表示数的单位是32位字长,即4个字节。20个字节等于数5,即0101)。当首部长度4位为最大值全为1时,即十进制的15,那么可以表示首都最大长度为60字节。当首部长度不是4个字节的整数倍时,需要用填充字段进行填充。

  • 区分服务:用来获得更好的服务,一般情况下不使用。

  • 总长度:长度为16位,单位为字节,表示整个数据报的长度,即总长度=首部长度+数据部分长度。总长度最大值为216-1=65535个字节。

  • 标识:长度共16位,IP软件在存储器维持一个计时器,每发送一个数据,计时器就增1,并将该值赋值给 标志 字段。标志字段主要为了IP数据报在需要分片时,会复制到每个数据片的标志字段,各个数据片在接收后能够正确组装成原来的数据报。

  • 标志:共3位,目前只有两位有意义。最低位记为“MF”,即MF=1表示后面还有分片,MF=0,后面没有分片。中间位记为“DF”,即DF=1不允许分片。DF=0允许分片。

  • 片偏移:共13位,单位为8字节。较长的IP数据报进行分片后,片偏移表示该分片距离原数据报起始位置的偏移量。为分片重新组装成数据报提供有力保证。

  • 生存时间:共8位,该数据报在网络的寿命,常用TTL表示。表示该数据报在网络能够转发多少次,一个路由器转发一次,该值就减1,直到为0就丢失该数据报。所以,最多可以转发255次。

  • 协议:共8位,指出该数据报携带的数据携带何种数据,以让目的主机IP层知道如何将数据部分上交给什么协议处理。例如TCP、UDP协议。常用的协议和对应协议字段的值:

  • 首部检验和:共16位,只校对首部,不校对数据。在发送端,将首部划分为许多16位字的序列,并将校验和置0,然后对所有序列进行反码算术运算求和,求和结果取反码写入到检验和中。接收端,操作与发送端类似,但不把检验和置0,如果取放码后结果为0,则保留该数据报,不为0则表示出错,丢弃该数据报。

    二进制反码算术求和:从低位到高位逐列进行运算:0+0=0;0+1=1;1+1=0,进1,如果最高进1,则在最后的结果加1即可。

  • 源地址和目的地址:各32位本地IP地址和目的主机的IP地址。

路由器分组转发算法

知道了IP数据报的格式,那么数据报是如何从发送端通过路由器到达接收端的呢?那就要了解路由表转发算法了。

  1. 路由器从数据报的的首部提取目的IP地址D,并计算出目的网络地址N.
  2. 如果N就是与此路由器直接相连的某个网络地址,则直接进行交付,不需要经过其他路由器。所谓直接交付就是将目的地址D直接转换为具体的硬件地址,把数据报封装成MAC帧,在数据链路层发送此帧。否侧就是间接交付,执行第3步。
  3. 路由器的路由表如果含有该目的地址D所指定的特定主机路由,则将数据报传输给下一跳路由。否则执行第4步。
  4. 路由表有能到达网络N的路由,则将数据报传输给下一跳路由。否则执行第5步。
  5. 若路由表有默认路由,则将数据报传输给下一跳默认路由。否则执行第6步。
  6. 报告转发分组出错。

那么在第二步中,如何将目的IP地址转换成硬件地址呢?那就涉及到ARP协议,具体看下一节。

ARP(地址解析协议)

在实际网络数据链路中传递数据帧,使用的还是硬件地址,因此需要通过ARP将IP地址解析出在数据链路层使用的硬件地址。

每台主机都设有ARP高速缓存,存有本局域网上的各主机和路由器的IP地址到硬件地址的映射表,该映射表是动态更新的。因为IP地址32位,而硬件地址是48位,经常的转换计算操作是非常的耗性能,所以通过高速缓存来存储IP地址和硬件地址的映射关系。同时由于主机经常添加和移除,所以需要动态更新映射表。映射地址项在映射表中都有生存时间,超过了生存时间就会被剔除。

那么ARP是如何通过IP地址找到对应的硬件地址呢?

如果主机H1要向主机H2发送IP数据报,那么会先在ARP高速缓存查找是否有H2主机的IP地址,如果有,就找到H2主机的硬件地址,然后把硬件地址写入到MAC帧即可,发送出去。如果在高速缓存中找不到H2主机的IP地址,那么H1主机就需要向局域网广播一个ARP请求分组,请求分组会携带有H2主机的IP地址。局域网内的所有主机都会收到该广播,并将请求分组的IP地址和自己的IP地址比较,如果一致则回复ARP响应分组,告知H1z主机,自己的硬件地址。H1主机收到H2的答复后,就会写入到高速缓存中,后续需要向H2主机发送数据就直接从映射表查找即可,有效降低通信量。

那,要是H2主机不在H1主机的局域网中怎么办?

ARP是解决同一个局域网内所有主机和路由器的IP地址与硬件地址解析问题。如果局域网内不存在H2主机,即ARP无法在本地局域网解析出H2的硬件地址,那么就查找本地局域网的一个路由器R1。路由器R1会在另一个相连局域网A通过ARP查找H2主机,如果找不到,则会在A局域网查找另外一个路由器R3,进行ARP查询。

那如果主机H2已经没有接入互联网了,或者在路由表转发分组出错时,怎么办?那么就需要ICMP(网际控制报文协议)。

ICMP(网际控制报文协议)

ICMP允许在主机或路由器报告差错情况和提供有关异常的情况报告。ICMP报文是装在IP数据报中,作为IP数据报的数据部分。如图所示:

ICMP报文的种类

ICMP报文有ICMP差错报告报文ICMP询问报文两种。

下面表格是常见几种ICMP报文类型:

差错报告报文

  • 终点不可达:当主机或路由不能交付数据报时就向源主机发送终点不可达报文。例如上文路由表转发分组第6步就是发送此报文。
  • 时间超过:在讲解IP数据报格式,讲到生存时间TTL,当数据报在路由器转发的次数(跳数)超过了源数据TTL的值,则会丢失该数据报,向源主机发送时间超过报文。或者终点在预定的时间内收不到所有的数据片的情况。
  • 参数问题:在接收端校验首部检验数据和,如果不为0,会丢失该数据报,并向源主机发送参数问题报文。
  • 改变路由:路由器把改变路由报文发给主机,让主机下次把报文发给另外的路由器(可通过更好的路由器)。

至于ICMP的数据部分,在差错报告报文,具有同一个格式,取原有的数据报数据字段的前8个字节(对TCPh和UDP协议来说,可以获取到端口号)。


下面几种情况是不再发送ICMP差错报文:

  • 对ICMP差错报文,不再发送ICMP差错报文。
  • 第一个数据片后面的所有数据片。
  • 具有多播地址的数据报
  • 具有特殊地址的数据报,如回环地址等等。

询问报文

  • 回送请求和回答:由主机或路由表向特定主机发送出的询问。收到此询问报文的主机向主机或路由表回复的回答报文。一般用来测试目的主机的是否可达和了解相关状态。
  • 时间戳请求和回答:请求主机或路由表回答当前的日期和时间。用于时间同步或时间测量。

例如我们常用的PING就是ICMP的应用实例,用来测试两台主机的连通性。

划分子网

两级IP地址到三级IP地址

在IP地址小结的介绍中,IP地址分为网络地址和主机号两级结构。两级划分IP地址会存在以下问题:

IP地址空间利用率有时很低:一个A类地址网络可以连接主机数超过1000万台,B类超过6万台。对主机数需求不大的单位又不愿申请C类,造成大量IP地址浪费。

路由表变得太大:给每一个物理网络分配一个网络地址,会导致路由器的路由表的项目数越来越多,从而导致网络性能下降。

两级IP地址不够灵活:一个单位在新的地点新建一个网络,是无法立即接入互联网的,需要向管理机构申请新的网络地址。

为此,通过将两级IP地址划分为三级地址来解决上述问题。这种做法叫做划分子网或者子网寻址或者子网路由选择

所谓划分子网就是从主机号中借若干位作为子网号,这样就行三级结构:

IP地址::={网络号:子网号:主机号}

同个子网内的所有主机,网络号也是相同,子网内的主机对外通讯需要将数据报发给路由器,再由路由器统一发送到其他网络。其他主机发送到本子网的主机也需要经过路由器。也就是说,划分子网是单位内部的事,对外还是表现为一个网络。

子网掩码

那么,当外部数据达到路由器,路由器如何转发给子网呢?从IP数据报首部是无法判断出网络是否划分子网的,所以需要子网掩码来协助。子网掩码是用来计算子网的网络地址。假如子网掩码为255.255.255.0,IP地址为145.13.3.10,将子网掩码和IP地址逐位与(AND),得到145.130.3.0就是子网网络地址。

使用子网掩码的好处是无论有没有划分子网,通过和IP地址按位与,可以快速得出网络地址,进行分组转发。如果划分子网,那么使用子网掩码就可以计算子网络地址,没有划分,方便路由器在路由表中查找下一跳。所以现在网络规定必须使用子网掩码。

构造超网

构造超网也就是无分类编址(CIDR),划分子网能在一定程度缓解IP地址不够用的问题,但是还不够,因此出现了无分类域间路由选择(CIDR)。CIDR具有以下两大特点:

消除传统分类地址和子网划分,成为无分类的两级编址,记法为:

IP地址::={网络前缀:主机}

更熟悉的记法是斜线记法

128.14.35.7/20 20表示前20位为网络地址(网络前缀),而剩下的12位为主机号。通过斜线记法也可以得出最小地址为128.14.32.0;最大地址为128.14.47.255。

IPv6

随着互联网的发展,IPv4分配的IP地址基本耗尽。新版IPv6是采用具有更大的地址空间来解决IP地址不够用的问题。
在IPv4中将数据协议单元PDU称为数据报,而在IPv6称为分组。IPv6相对IPv4具有以下特点:

  • 更大地址空间,位数为128位
  • 扩展地址层次结构
  • 灵活首部格式
  • 改进的选项
  • 允许协议继续扩充
  • 支持即插即用
  • 支持资源预分配
  • 首部改为8字节对齐,IPv4是4字节对齐

IPv6更多信息参阅GitHub文档资源吧。

点赞支持支持吧

手机如何控制BLE设备

前言

最近一直在思考一个问题,如何写文章?即内容高质量又通俗易懂,让新手既明白其中蕴含的真理又能轻松跑起第一个程序,同时也能让高手温故知新,如获新欢。经过长时间的思索,最终定位为,内容高质量,描述简洁,思路清晰,对读者负责任的文章。初出茅庐,不会高手的底层功力,也不会段子手的套路人心,但,坚持做自己,尽自己所能,为人民服务。

BLE的一些关键概念

在Android应用层开发BLE,不懂一些理论和协议也没关系,照样可以上手开发。本着知其然知其所以然,下面知识点的理解,能够有力支撑使用Android API。

蓝牙类别

低功耗蓝牙是不能兼容经典蓝牙的,需要兼容,只能选择双模蓝牙。

  • 低功耗蓝牙:字如其名,第一特点就是低功耗,一个纽扣电池可以支持其运行数月至数年,至于怎么实现低功耗,看下文。小体积,低成本,在某宝上的价格有提供邮票体积大小,价格三四块前的蓝牙模块,可以想象,厂商批发价格会更低。应用场景广,可以想想,现在的智能家居,智能音箱,智能手表等等物联网设备,大多数通过BLE进行配网和数据交互。
  • 经典蓝牙:经典蓝牙,泛指蓝牙4.0以下的都是经典蓝牙,蓝牙4.0以上的,你还怀念通过蓝牙让音箱播放手机的音乐么?经典蓝牙常用在语音、音乐等较高数据量传输的应用场景上。
  • 双模蓝牙:即在蓝牙模块中兼容BLE和BT.

Android 4.3及更高版本,Android 蓝牙堆栈可提供实现蓝牙低功耗 (BLE) 的功能,在 Android 8.0 中,原生蓝牙堆栈完全符合蓝牙 5 的要求。也就是说在Android 4.3以上,我们可以通过Android 原生API和蓝牙设备交互。

GAP(Generic Access Profile)

GAP用来控制蓝牙设备的广播和连接。GAP可以使蓝牙设备被其他蓝牙设备发现,并决定是否可以被连接。GAP协议将蓝牙设备分为中心设备和外围设备。

  • 中心设备功能比强大,用来连接外围设备,处理数据等。例如手机。
  • 外围设备一般指非常小和低功耗的设备,用来提供数据,连接功能相对较强大的中心设备。例如体温计,小米手环等。

外围设备通过广播数据扫描回复两种方式之一让中心设备发现,然后进行连接,从而达到进行数据交互的前提条件。为了达到低功耗,外围设备并不是一直广播,会设定一个广播间隔,每个广播间隔中,它会重新发送自己的广播数据。广播间隔越长,越省电,同时也不太容易扫描到。

在Android开发中,常通过蓝牙MAC进行连接,连接成功后就可以进行交互嘹。

GATT(Generic Attribute Profile)

简单理解为普通属性描述,BLE连接成功后,BLE设备基于该描述进行发送和接收类似“属性”的较短数据。目前大多数BLE属性描述是基于GATT。一般一个Profile代表了一个特殊的功能应用,例如心率或者电量应用。

ATT(Attribute Protocol)
GATT是基于ATT上实现的,ATT是运行在BLE设备中,它们之间以尽可能小的属性在进行交互,而属性则是以Service和Characteristic的形式在ATT上传输。下图是GATT的结构。
GATT结构

  • Characteristic 一个特性(Characteristic)包含一个值(value)和0至n个描述符(descriptors),而每个描述符又可以代表特性的值。
  • Descriptor 描述符是用来定义代表Characteristic的值的属性。例如用来描述心率的取值范围和单位。
  • Service 一个Profile代表着一个应用,而Service代表该应用可以提供多少种服务。例如心率监视器提供心率值检测服务,Service内包含着多个Characteristic。

Service和Characteristic都通过16位或128位的UUID进行识别,16位的UUID需要向官方购买,全球唯一,而120位可以自己定义。一般UUID由硬件部门或者厂商提供。数据的交互都是客户端发起请求,服务端响应,客户端进行读写从而达到全双工。

在BLE连接中,定义者两个角色,GATT客户端和Gatt服务端,一般认为,主动发起数据请求的是Client,而响应数据结果的是Server。例如手机和手环。在数据交互的过程中,永远是Client单方面发起请求,然后读写Server相关属性达到全双工效果。

理论知识就讲到这里了哇,下面进行Android应用层的开发哦。

实战

实战部分的内容,大多数和蓝牙实现聊天功能是一致的。但为了没有看过这边文章的同学,我就Ctrl+cCtrl-v一下,顺便修改一下代码。

声明权限

在AndroidManifest.xml配置下面代码,让APP具有蓝牙访问权限和发现周边蓝牙权限。

//使用蓝牙需要该权限
<uses-permission android:name="android.permission.BLUETOOTH"/>
//使用扫描和设置需要权限
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
//Android 6.0以上声明一下两个权限之一即可。声明位置权限,不然扫描或者发现蓝牙功能用不了哦
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

为了适配Android 6.0,在主Activity中添加动态申请定位权限代码,不添加扫描不到蓝牙代码哦。

    /**
     * Android 6.0 动态申请授权定位信息权限,否则扫描蓝牙列表为空
     */
    private void requestPermissions() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            if (ContextCompat.checkSelfPermission(this,
                    Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {

                if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                        Manifest.permission.ACCESS_COARSE_LOCATION)) {
                    Toast.makeText(this, "使用蓝牙需要授权定位信息", Toast.LENGTH_LONG).show();
                }
                //请求权限
                ActivityCompat.requestPermissions(this,
                        new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
                        REQUEST_ACCESS_COARSE_LOCATION_PERMISSION);
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == REQUEST_ACCESS_COARSE_LOCATION_PERMISSION) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                //用户授权
            } else {
                finish();
            }

        }

        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

检测设备是否支持BLE功能

避免部分同学在不支持蓝牙的手机或者设备安装了Demo,或者安装在模拟器了。

    /**
     * 是否支持BLE
     */
    private boolean isSupportBLE() {
        mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

        BluetoothManager manager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);

        mBluetoothAdapter = manager.getAdapter();
            //设备是否支持蓝牙
        if (mBluetoothAdapter == null
                    //系统是否支持BLE
                && !getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
            Log.e(TAG, "not support bluetooth");
            return true;
        } else {
            Log.e(TAG, " support bluetooth");
            return false;
        }

    }

    /**
     * 弹出不支持低功耗蓝牙对话框
     */
    private void showNotSupportBluetoothDialog() {
        AlertDialog dialog = new AlertDialog.Builder(this).setTitle("当前设备不支持BLE").create();
        dialog.show();
        dialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialog) {
                finish();
            }
        });

    }

开启蓝牙

有了支持BLE的手机,那么要检测手机蓝牙是否打开。如果没有打开则打开蓝牙和监听蓝牙的状态变化的广播。蓝牙打开后,扫描周边蓝牙设备。

    //开启蓝牙
    private void enableBLE() {
        if (mBluetoothAdapter.isEnabled()) {
            startScan();
        } else {
            mBluetoothAdapter.enable();
        }
    }
    //注册监听蓝牙状态变化广播
    private void registerBluetoothReceiver() {
        IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
        registerReceiver(bluetoothReceiver, filter);
    }

    BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();

            if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
                int state = mBluetoothAdapter.getState();
                if (state == BluetoothAdapter.STATE_ON) {
                    startScan();
                }
            }
        }
    };

扫描

Android 5.0以上的扫描API和Android 5.0以下的API已经不一样了。蓝牙扫描是非常耗电的,Android 默认在手机息屏停止扫描,在手机亮屏后开始扫描。为了更好的降低耗电,正式APP应该主动关闭扫描,不应该循环扫描。BLE扫描速度非常快,我们根据扫描到的蓝牙设备MAC保存Set集合中,过滤掉重复的设备。

   private void startScan() {

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            //android 5.0之前的扫描方式
            mBluetoothAdapter.startLeScan(new BluetoothAdapter.LeScanCallback() {
                @Override
                public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {

                }
            });
        } else {
            //android 5.0之后的扫描方式
             scanner = mBluetoothAdapter.getBluetoothLeScanner();

             scanCallback=new ScanCallback() {
                 @Override
                 public void onScanResult(int callbackType, ScanResult result) {

                     //停止扫描
                     if (firstScan){
                         handler.postDelayed(new Runnable() {
                             @Override
                             public void run() {
                                 scanner.stopScan(scanCallback);

                             }
                         },SCAN_TIME);

                         firstScan=false;
                     }

                     String mac=result.getDevice().getAddress();

                     Log.i(TAG,"mac:"+mac);
                     //过滤重复的mac
                     if (!macSet.contains(mac)){
                         macSet.add(result.getDevice().getAddress());
                         deviceList.add(result.getDevice());
                         deviceAdapter.notifyDataSetChanged();
                     }
                 }

                 @Override
                 public void onBatchScanResults(List<ScanResult> results) {
                     super.onBatchScanResults(results);
                     //需要蓝牙芯片支持,支持批量扫描结果。此方法和onScanResult是互斥的,只会回调其中之一
                 }

                 @Override
                 public void onScanFailed(int errorCode) {
                     super.onScanFailed(errorCode);
                     Log.e(TAG,"扫描失败:"+errorCode);
                 }
             };

            scanner.startScan(scanCallback);
        }

    }

这里主要实现的Android 5.0后的扫描,通过将扫描到的设备添加到list,并显示到界面上。由于可能扫描到重复的蓝牙设备,通过Set过滤掉重复的设备。

抽象类ScanCallback作为BLE扫描的回调,重写其中三个抽象方法。

  • onScanResult 一般情况,我们重写该方法,每扫描到设备则回调一次。
  • onBatchScanResults 接口文档注释是回调之前已经扫描的的蓝牙列表,但实际在测试没有结果,网上搜了一下,结果在代码中备注了。
  • onScanFailed 扫描失败

ScanResult扫描结果内包含扫描到的周边BLE设备BluetoothDevice。通过BluetoothDevice,我们可以获取周边BLE的相关信息,例如MAC,连接状态等。

连接BLE

在上一步获得我们的BLE列表后,选择我们要连接的BLE设备,进行连接。处理listview 的点击效果,进行连接BLE设备。

    lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            BluetoothDevice device = deviceList.get(position);
            bluetoothGatt = device.connectGatt(MainActivity.this, true, gattCallback);
        }
    });

通过BluetoothDevice的connectGatt()方法连接周边BLE设备。现在明白为何要先了解GATT了吧。connectGatt()方法有三个参数,第二个参数表示当设备可用时,是否自动连接,第三个参数是BluetoothGattCallback类型,通过该回调,我们可以知道BLE的连接状态和对Service、Charateristic进行操作,从而进行数据交互。connectGatt()方法会返回类型BluetoothGatt的实例,通过该实例,我们可以发送请求服务端

BluetoothGattCallback

抽象类BluetoothGattCallback有很多方法需要我们重写,我们这里说几个比较重要的,其他可以看Demo。我们通过定义 GattCallback继承BluetoothGattCallback,并在类中重写其方法。这里假设我们通过手机去连接小米手环,那么手机就是Gatt客户端,小米手环就是Gatt服务端。

  • onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
    该方法手机连接或者断开连接到小米手环会回调该方法。参数一代表当前Gatt客户端,也就是我们的手机。参数二表示连接或者断开连接的操作是否成功,只有参数二status值为GATT_SUCCESS,参数三才有效。参数三会返回STATE_CONNECTEDSTATE_DISCONNECTED表示当前客户端和服务端的连接状态。连接成功后,我们通过bluetoothGatt对象的 discoverServices()
  • onServicesDiscovered(BluetoothGatt gatt, int status) 当发现Service就会回调该方法,参数二值为GATT_SUCCESS表示服务端的所有服务已经被搜索完毕,此时可以调用bluetoothGatt.getServices()获得Service列表,进而获得所有Characteristic。

也可以通过指定的UUID获得Service和Characteristic。

private void updateValue() {
    BluetoothGattService service = bluetoothGatt.getService(UUID.fromString(serviceUuid));
    if (service == null) return;
    BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID.fromString(charUuid));
    enableNotification(characteristic, charUuid);
    characteristic.setValue("on");
}

设置GATT通知

这样当我们修改characteristic成功后,会回调告知我们。

private void enableNotification(BluetoothGattCharacteristic characteristic,String uuid){
    bluetoothGatt.setCharacteristicNotification(characteristic,true);
    BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
            UUID.fromString(uuid));
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    bluetoothGatt.writeDescriptor(descriptor);
}

上面代码设置成功后,会回调BluetoothGattCallback的onCharacteristicChanged()方法。

如果Characteristic的值被修改,会回调BluetoothGattCallback的onCharacteristicChanged()方法,在这里我们可以进一步提高用户体验。需要注意一下,类BluetoothGattCallback有很多方法需要我们实现,因为Gatt的响应结果都是回调该对象的方法。

小结一下

Gatt客户端通过BluetoothDevice的connectGatt()方法与服务端连接成功后,利用返回的BluetoothGatt对象,请求Gatt服务端相关数据。Gatt服务端根据请求,将自身的状态通过回调客户端传入的BluetoothGattCallback对象的相关方法,从而告知客户端。

关闭BLE

当我们使用完BLE之后,应该及时关闭,以释放相关资源和降低功耗。

public void close() {
    if (bluetoothGatt == null) {
        return;
    }
    bluetoothGatt.close();
    bluetoothGatt = null;
}

总结

在应用层操作BLE难度不大,因为Android屏蔽了很多蓝牙栈协议的细节。但应用层开发会苦于没有硬件设备支持。通过本文,我们知道BLE的AP和GATT等等一些概念,了解Android BLE开发的整体流程,对BLE有一个感性的认知。

在恋爱的世界里谈观察者模式

不知看这篇文章的你,是否一只孤独的单身汪~

如果是的话,让我告诉你恋爱的那件小事~

从本文,你可能会感悟到恋爱真经和学习到观察者模式。

爱情故事

心动的感觉

忙忙人海中,靠着左右手生活了二十来年的你,在刚从洗手间出来的刹那间,遇到那个想要过一生的人,怦然的心动....

从此变得不可收拾,你总是情不自禁,偷偷的观察她的一举一动,总是想知道她干嘛。也好想引起她的注意,可以你好怂。因为,喜欢一个人,就是小心翼翼的....

你终于忍不住了,不想错过对人了,鼓起了勇气,小碎步的走到她旁边,结结巴巴的,"Hi,我是...是....技术部的XXX,可以...可以加你微信么?"

然后,你的通讯录里有了她的名字。可是,你变了,总是傻傻的盯着她名字,打开了聊天框又关了,一直这样重复重复着...

今天,直男癌晚期的你,喝了口江小白,紧张的给她发了人生第一句招呼:“Hello,my world!”

“在,有什么事么?”

“没事,有点无聊”

“哦哦哦”

“上个月碰到你,被你深深的吸引了,能不能和你进一步的发展,做个朋友呢”

“可以呀,多个朋友多条路”

“谢谢”

此时,你开心到起飞,你的心已经在天空飘来飘去~

小姐姐默默把你加入到帅哥列表中...


在你喜欢小姐姐那一刻起,你就变成了观察者,小姐姐就变成了被观察者。因为关心小姐姐的生活,所以被成功加入姐姐的帅哥列表中,表示你和小姐姐产生了订阅关系。

幸福的感觉

就这样,每天在微信偶尔聊聊天,打打招呼。但妹纸只有在自己有困难的时候才会主动找你,例如帮忙修电脑。

今天,妹纸的电脑又坏了,妹纸问了帅锅列表的所有人,“Hi,我电脑坏了,有空帮我修一下么?”每个帅锅都收到了,但他们顾着撩其他妹纸,没空。

只有你有空,于是你兴高采烈的帮忙修电脑,妹纸给你端来的温水,给你擦擦,好幸福哦。。。


小姐姐在自己的生活,也就是你关心的事发生变化时,例如电脑坏了,就会告知列表里的孩子们,让他们知道,以让他们决定是否做点事。例如这里你会去修电脑。

心累的感觉

慢慢的相处来,你发现自己心累了,感觉没啥意思,因为小姐姐平常没事都不搭理你,有困难才找你。你很想把小姐姐占为己有,很想天天和她待在一起,于是你对小姐姐说:

“做我女朋友吧”

“额,你是好人”

“让我照顾你吧”

“你是个很好的朋友”

“那我们分手吧”

“都没恋爱过,哪来的分手”

你伤心了好久,觉得是自己不够爱,或许真爱就是放手吧。你沮丧的对小姐姐说不要再联系了,小姐姐二话没说就从帅锅列表把你移除了。于是你们两就断了关系了,除非你重新向小姐姐表明爱意。


这时候的你们就是解注册,小姐姐发生什么事再也不会通知你。


听完故事,咱们还是面对现实,好好敲代码,看文章,来聊聊观察者模式吧。

观察者模式

如上文所示,观察者模式涉及到观察者和被观察者,观察者需要向被观察者注册,产生订阅关系。被观察者可以持有多个观察者,在自身相关事件发生变化的时候通知所有订阅该事件的观察者。观察者对事件不再感兴趣,需要向被观察者解注册。

一般为了程序的灵活和扩展,都会将观察者和实现者抽象为一个接口,再由具体的实现类去实现接口。


下面举个简单的栗子:

被观察者

定义一个被观察者接口,具有注册和解注册函数。

public interface Observable<T> {

    void register(T obj);

    void unregister(T obj);
}

在定义一个具体的被观察者,例如上文的女孩,持有一个集合,用来保存对其感兴趣的观察者。有一个repairComputer()函数,用来在事件发生变化时,通知帅锅们来帮忙修电脑。

public class GirlObservable implements Observable<Observer> {

    private List<Observer> observers = new ArrayList<>();

    @Override
    public void register(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void unregister(Observer observer) {
        observers.remove(observer);
    }

    public void repairComputer(){
        for (Observer observer:observers){
            observer.call("电脑坏了,来帮我修吧");
        }
    }
}

观察者

抽象一个观察者接口,例如拥有一个call()函数,这样当我们关心被观察者的事改变时,才能得到通知。

public interface Observer {
    void call(String msg);
}

再定义两个具体的观察者,实现call()函数,在被 被观察者通知的时候,根据自身逻辑做相关处理。

public class ZhangsanObserver implements Observer {
    @Override
    public void call(String msg) {
        System.out.println("我是张三,现在没空帮你");
    }
}

public class LisiObaserver implements Observer {
    @Override
    public void call(String msg) {
        System.out.println("好呀,我马上就来帮你修电脑");
    }
}

订阅和发展

有了观察者和被观察者,那么就要发生订阅关系,不然就失去了意义。

    //张三对象
    Observer zhangsan=new ZhangsanObserver();
    //李四对象 
    Observer lisi=new LisiObaserver();
    //女孩对象
    GirlObservable girl=new GirlObservable();
    //向女孩注册张三    
    girl.register(zhangsan);
    //向女孩注册李四       
    girl.register(lisi);
    //女孩的电脑坏了,请求帮忙
    girl.repairComputer();
    //对女孩不感兴趣了,张三解注册   
    girl.unregister(zhangsan);
    //对女孩不感兴趣了,李四解注册      
   girl.unregister(lisi);

从上文,观察者模式很容易理解,从栗子来看也很简单,却能有效的降低耦合,让具体的对象依赖于抽象,但其中一方逻辑变更不会影响另一方。,观察者模式在实际开发中得到很大的推广,如EventBus、Android的广播、setOnClikLisenter事件等。

点个Star啦,支持好文,早日脱单

欢迎到GitHub Star

Android IntentService

Android的四大组件中,Service排行老二,在Android中的主要作用是后台服务,进行与界面无关的操作。由于Service运行在主线程,所以进行异步操作需要在子线进行。为此Android为我们提供了IntentService
IntentService是一个抽象类,继承至Service,主要方便我们新建工作线程进行异步操作。提交任务到IntentService时,异步任务以串行方式进行处理,意味着工作线程一次只处理一个任务。而且当所有任务都完成之后,会自动停止Service,不需要我们手动停止。

IntentService 的使用

  1. 我们定义DownloadService类并继承至IntentService。来模拟网络下载的过程。
public class DownloadService extends IntentService {

    private static int count = 0;

    /**
     * 主要用于调用服务类构造器
     *
     * @param name 用于区分不同任务
     */
    public DownloadService(String name) {
        super(name);
    }

    /**
     * AndroidManifest.xml配置清单需要配置
     *
     * @param
     */
    public DownloadService() {
        super("action");
    }

    /**
    *主要重写该方法,在该方法内进行异步操作。
    **/
    @Override
    protected void onHandleIntent(Intent intent) {
        Log.i("Download", "onHandleIntent" + count);
        count++;

        String name = intent.getStringExtra("action");

        if (name.equals("download")) {
            for (int i = 0; i < 5; i++) {

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return;
                }

                Log.i("Download", "download:" + count);
            }
        }
    }
    //以下方法的重写,仅仅为了打印日志
    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i("Download", "onDestroy");

    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i("Download", "onCreate");
    }

    @Override
    public void onStart(Intent intent, int startId) {
        super.onStart(intent, startId);

        Log.i("Download", "onStart");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i("Download", "onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }
}

  1. 在AndroidManifest.xml配置DownloadService
<service android:name=".DownloadService"/>
  1. 在MainActivity类中循环调用Service,启动多循环任务。
 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent intent=new Intent(this,DownloadService.class);
        for (int i =0;i<3;i++){

            intent.putExtra("action","download");
            intent.putExtra("count",""+i);
            startService(intent);

        }
    }
  1. 运行结果

分析:
从运行结果知道,IntentService在运行多个任务情况下,只调用一次onCreate,调用多次onStartCommand,跟Service的生命周期一致。但,只有在运行完download:1之后才会去运行download:2,接着是download:3。最后所有任务结束后会自动调用onDestroy,停止服务。在这里需要注意的是,和Service并不同,Service需要我们手动停止服务。对于结果的回调,可以采用接口回调,广播,EventBus

那么,IntentService是如何在Service中实现异步操作和串行处理任务的呢?

IntentService内部实现

  1. 查看IntentService的onCreate方法
 @Override
    public void onCreate() {
        super.onCreate();
        //分析一
        HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
        thread.start();

        mServiceLooper = thread.getLooper();
        分析二
        mServiceHandler = new ServiceHandler(mServiceLooper);
    }

分析一

HandThread继承Thread,通过start方法创建工作线程,内部建立Looper来达到消息循环,通过Hanlder消息机制来达到串行的效果和处理多任务。HandThreadHandler消息机制,可以另外查看文章。

分析二

ServiceHandler继承Handler,与普通的Handler并没有区别,在其内容处理handleMessage。即调用IntentServiceonHandleIntent

  private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            onHandleIntent((Intent)msg.obj);
            stopSelf(msg.arg1);
        }
    }
  1. 那么,当我们在Activity中重复调用startService方法时,只会多次调用onStartCommand方法,并不会重复调用onCreate方法。我们看看onStartComamnd方法的实现。
    @Override
    public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
        onStart(intent, startId);
        return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
    }

可以看得出,调用了onStart方法了。而onStart方法只是将我们的Intent包装成Message,通过Handler发送出去,并在Handler中进行处理,调用我们的onHandleIntent。进而调用我们实现onHandleIntent的代码。

    @Override
    public void onStart(@Nullable Intent intent, int startId) {
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent;
        mServiceHandler.sendMessage(msg);
    }

总结

IntentService并没有什么的新的技术含量,在了解HandlerTread和Handler的消息机制下,对Handler+Thread+Service作一个封装,更适合我们后台进行异步耗时操作的场景。有效避免通过new多个Thread

RxJava 3.0.0-RC0来袭

RxJava一路走来,已经是第三个版本了~现在百分之99.99还在使用RxJava 2.X

主要变化

主要特点

  • 单一依赖:Reactive-Streams
  • 继续支持Java 6+和Android 2.3+
  • 修复了API错误和RxJava 2的许多限制
  • 旨在替代RxJava 2,具有相对较少的二进制不兼容更改
  • 提供Java 8 lambda友好的API
  • 关于并发源(线程,池,事件循环,光纤,演员等)的不同意见
  • 异步或同步执行
  • 参数化并发的虚拟时间和调度程序
  • 为测试schedulers,consumers和plugin hooks提供测试和诊断支持

RxJava2到2020年12月31号不再提供支持,错误时同时在2.x和3.x修复,但新功能只会在3.x上添加。

与RxJava的主要区别是:

  • 将eagerTruncate添加到replay运算符,以便head节点将在截断时丢失它保留的项引用
  • 新增 X.fromSupplier()
  • 使用 Scheduler 添加 concatMap,保证 mapper 函数的运行位置
  • 新增 startWithItem 和 startWithIterable
  • ConnectableFlowable/ConnetableFlowable 重新设计
  • 将 as() 并入 to()
  • 更改 Maybe.defaultIfEmpty() 以返回 Single
  • 用 Supplier 代替 Callable
  • 将一些实验操作符推广到标准
  • 从某些主题/处理器中删除 getValues()
  • 删除 replay(Scheduler) 及其重载
  • 删除 dematerialize()
  • 删除 startWith(T|Iterable)
  • 删除 as()
  • 删除 Maybe.toSingle(T)
  • 删除 Flowable.subscribe(4 args)
  • 删除 Observable.subscribe(4 args)
  • 删除 Single.toCompletable()
  • 删除 Completable.blockingGet()

入门

1、添加依赖

implementation "io.reactivex.rxjava3:rxjava:3.0.0-RC0"

不好意思哦,还没看到RxAndroid出3.0,这就很尴尬了...

2、一些概念

2.1、上流、下流

在RxJava,数据以流的方式组织。也就是说,Rxjava包括一个源的数据流,数据流后跟着消费者的零个到多个消费数据流步骤。

source
  .operator1()
  .operator2()
  .operator3()
  .subscribe(consumer)

在上文代码中,对于operator2来说,在它前面叫做上流,在它后面的叫做下流。憋住,别笑,真的是下流来的。

2.2、流的对象

在RxJava的文档中,emission, emits, item, event, signal, data and message都被认为在数据流中被传递的数据对象。

2.3、背压(Backpressure)

当数据流通过异步的步骤执行时,这些步骤的执行速度可能不一致。也就是说上流数据发送太快,下流没有足够的能力去处理。为了避免这种情况,一般要么缓存上流的数据,要么抛弃数据。但这种处理方式,有时会带来很大的问题。为此,RxJava带来了backpressure的概念。背压是一种流量的控制步骤,在不知道上流还有多少数据的情形下控制内存的使用,表示它们还能处理多少数据。

支持背压的有Flowable类,不支持背压的有Observable,Single, Maybe and Completable类。

2.4 线程调度器(Schedulers)

对于我们Android开发来说,最喜欢的就是它简洁切换线程的操作。RxJava通过调度器来方便线程的切换。

  • Schedulers.computation(): 适合运行在密集计算的操作,大多数异步操作符使用该调度器。
  • Schedulers.io():适合运行I/0和阻塞操作.
  • Schedulers.single():适合需要单一线程的操作
  • Schedulers.trampoline(): 适合需要顺序运行的操作

在不同平台还有不同的调度器,例如Android的主线程:AndroidSchedulers.mainThread()

Flowable.range(1, 10)
  .observeOn(Schedulers.computation())
  .map(v -> v * v)
  .blockingSubscribe(System.out::println);

2.5 基类

在 RxJava 3 可以发现有以下几个基类(跟RxJava 2是一致的吧):

  • io.reactivex.Flowable:发送0个N个的数据,支持Reactive-Streams和背压
  • io.reactivex.Observable:发送0个N个的数据,不支持背压,
  • io.reactivex.Single:只能发送单个数据或者一个错误
  • io.reactivex.Completable:没有发送任何数据,但只处理 onComplete 和 onError 事件。
  • io.reactivex.Maybe:能够发射0或者1个数据,要么成功,要么失败。

操作符

实用操作符

1、ObserveOn

指定观察者的线程,例如在Android访问网络后,数据需要主线程消费,那么将观察者的线程切换到主线就需要ObserveOn操作符。每次指定一次都会生效。

2、subscribeOn

指定被观察者的线程,即数据源发生的线程。例如在Android访问网络时,需要将线程切换到子线程。多次指定只有第一次有效。

3、doOnEach

数据源(Observable)每发送一次数据,就调用一次。

4、doOnNext

数据源每次调用onNext() 之前都会先回调该方法。

5、doOnError

数据源每次调用onError() 之前会回调该方法。

6、doOnComplete

数据源每次调用onComplete() 之前会回调该方法

7、doOnSubscribe

数据源每次调用onSubscribe() 之后会回调该方法

8、doOnDispose

数据源每次调用dispose() 之后会回调该方法

其他的见官网吧,不难

实用操作符

对数据源过滤操作符

主要讲对数据源进行选择和过滤的常用操作符

1、skip(跳过)

可以作用于Flowable,Observable,表示源发射数据前,跳过多少个。例如下面跳过前四个:

Observable<Integer> source = Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

source.skip(4)
    .subscribe(System.out::print);

打印结果:5678910

Observable<Integer> source = Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

source.skipLast(4)
    .subscribe(System.out::print);
    
打印结果:1 2 3 4 5 6

skipLast(n)操作表示从流的尾部跳过n个元素。

2、debounce(去抖动)

可作用于Flowable,Observable。在Android开发,通常为了防止用户重复点击而设置标记位,而通过RxJava的debounce操作符可以有效达到该效果。在规定时间内,用户重复点击只有最后一次有效,

Observable<String> source = Observable.create(emitter -> {
    emitter.onNext("A");

    Thread.sleep(1_500);
    emitter.onNext("B");

    Thread.sleep(500);
    emitter.onNext("C");

    Thread.sleep(250);
    emitter.onNext("D");

    Thread.sleep(2_000);
    emitter.onNext("E");
    emitter.onComplete();
});

source.subscribeOn(Schedulers.io())
        .debounce(1, TimeUnit.SECONDS)
        .blockingSubscribe(
                item -> System.out.print(item+" "),
                Throwable::printStackTrace,
                () -> System.out.println("onComplete"));

打印:A D E onComplete

上文代码中,数据源以一定的时间间隔发送A,B,C,D,E。操作符debounce的时间设为1秒,发送A后1.5秒并没有发射其他数据,所以A能成功发射。发射B后,在1秒之内,又发射了C和D,在D之后的2秒才发射E,所有B、C都失效,只有D有效;而E之后已经没有其他数据流了,所有E有效。

3、distinct(去重)

可作用于Flowable,Observable,去掉数据源重复的数据。

Observable.just(2, 3, 4, 4, 2, 1)
        .distinct()
        .subscribe(System.out::println);

// 打印:2 3 4 2 1
Observable.just(1, 1, 2, 1, 2, 3, 3, 4)
        .distinctUntilChanged()
        .subscribe(System.out::print);
//打印:1 2 1 2 3 4

distinctUntilChanged()去掉相邻重复数据。

4、elementAt(获取指定位置元素)

可作用于Flowable,Observable,从数据源获取指定位置的元素,从0开始。

 Observable.just(2,4,3,1,5,8)
        .elementAt(0)
        .subscribe(integer -> 
         Log.d("TAG","elmentAt->"+integer));
打印:2

Observable<String> source = Observable.just("Kirk", "Spock", "Chekov", "Sulu");
Single<String> element = source.elementAtOrError(4);

element.subscribe(
    name -> System.out.println("onSuccess will not be printed!"),
    error -> System.out.println("onError: " + error));
打印:onSuccess will not be printed!

elementAtOrError:指定元素的位置超过数据长度,则发射异常。

5、filter(过滤)

可作用于 Flowable,Observable,Maybe,Single。在filter中返回表示发射该元素,返回false表示过滤该数据。

Observable.just(1, 2, 3, 4, 5, 6)
        .filter(x -> x % 2 == 0)
        .subscribe(System.out::print);
打印:2 4 6

6、first(第一个)

作用于 Flowable,Observable。发射数据源第一个数据,如果没有则发送默认值。

Observable<String> source = Observable.just("A", "B", "C");
Single<String> firstOrDefault = source.first("D");
firstOrDefault.subscribe(System.out::println);
打印:A

Observable<String> emptySource = Observable.empty();
Single<String> firstOrError = emptySource.firstOrError();
firstOrError.subscribe(
        element -> System.out.println("onSuccess will not be printed!"),
        error -> System.out.println("onError: " + error));
打印:onError: java.util.NoSuchElementException

和firstElement的区别是first返回的是Single,而firstElement返回Maybe。firstOrError在没有数据会返回异常。

7、last(最后一个)

last、lastElement、lastOrError与fist、firstElement、firstOrError相对应。

Observable<String> source = Observable.just("A", "B", "C");
Single<String> lastOrDefault = source.last("D");
lastOrDefault.subscribe(System.out::println);
//打印:C

Observable<String> source = Observable.just("A", "B", "C");
Maybe<String> last = source.lastElement();
last.subscribe(System.out::println);
//打印:C

Observable<String> emptySource = Observable.empty();
Single<String> lastOrError = emptySource.lastOrError();
lastOrError.subscribe(
        element -> System.out.println("onSuccess will not be printed!"),
        error -> System.out.println("onError: " + error));
// 打印:onError: java.util.NoSuchElementException

8、ignoreElements & ignoreElement(忽略元素)

ignoreElements 作用于Flowable、Observable。ignoreElement作用于Maybe、Single。两者都是忽略掉数据,返回完成或者错误时间。

Single<Long> source = Single.timer(1, TimeUnit.SECONDS);
Completable completable = source.ignoreElement();
completable.doOnComplete(() -> System.out.println("Done!"))
        .blockingAwait();
// 1秒后打印:Donde!

Observable<Long> source = Observable.intervalRange(1, 5, 1, 1, TimeUnit.SECONDS);
Completable completable = source.ignoreElements();
completable.doOnComplete(() -> System.out.println("Done!"))
        .blockingAwait();
// 五秒后打印:Done!

9、ofType(过滤掉类型)

作用于Flowable、Observable、Maybe、过滤掉类型。

Observable<Number> numbers = Observable.just(1, 4.0, 3, 2.71, 2f, 7);
Observable<Integer> integers = numbers.ofType(Integer.class);
integers.subscribe((Integer x) -> System.out.print(x+" "));
//打印:1 3 7

10、sample

作用于Flowable、Observable,在一个周期内发射最新的数据。

Observable<String> source = Observable.create(emitter -> {
    emitter.onNext("A");

    Thread.sleep(500);
    emitter.onNext("B");

    Thread.sleep(200);
    emitter.onNext("C");

    Thread.sleep(800);
    emitter.onNext("D");

    Thread.sleep(600);
    emitter.onNext("E");
    emitter.onComplete();
});

source.subscribeOn(Schedulers.io())
        .sample(1, TimeUnit.SECONDS)
        .blockingSubscribe(
                item -> System.out.print(item+" "),
                Throwable::printStackTrace,
                () -> System.out.print("onComplete"));
                
// 打印: C D onComplete

与debounce的区别是,sample是以时间为周期的发射,一秒又一秒内的最新数据。而debounce是最后一个有效数据开始。

11、throttleFirst & throttleLast & throttleWithTimeout

作用于Flowable、Observable。throttleLast与smaple一致,而throttleFirst是指定周期内第一个数据。throttleWithTimeout与debounce一致。

Observable<String> source = Observable.create(emitter -> {
    emitter.onNext("A");

    Thread.sleep(500);
    emitter.onNext("B");

    Thread.sleep(200);
    emitter.onNext("C");

    Thread.sleep(800);
    emitter.onNext("D");

    Thread.sleep(600);
    emitter.onNext("E");
    emitter.onComplete();
});

source.subscribeOn(Schedulers.io())
        .throttleFirst(1, TimeUnit.SECONDS)
        .blockingSubscribe(
                item -> System.out.print(item+" "),
                Throwable::printStackTrace,
                () -> System.out.print(" onComplete"));
//打印:A D onComplete

source.subscribeOn(Schedulers.io())
        .throttleLast(1, TimeUnit.SECONDS)
        .blockingSubscribe(
                item -> System.out.print(item+" "),
                Throwable::printStackTrace,
                () -> System.out.print(" onComplete"));

// 打印:C D onComplete

12、throttleLatest

之所以拿出来单独说,我看不懂官网的解释。然后看别人的文章:throttleFirst+throttleLast的组合?开玩笑的吧。个人理解是:如果源的第一个数据总会被发射,然后开始周期计时,此时的效果就会跟throttleLast一致。

Observable<String> source = Observable.create(emitter -> {
            emitter.onNext("A");

            Thread.sleep(500);
            emitter.onNext("B");

            Thread.sleep(200);
            emitter.onNext("C");

            Thread.sleep(200);
            emitter.onNext("D");

            Thread.sleep(400);
            emitter.onNext("E");
            
            Thread.sleep(400);
            emitter.onNext("F");
            
            Thread.sleep(400);
            emitter.onNext("G");
            
            Thread.sleep(2000);
            emitter.onComplete();
        });
        source.subscribeOn(Schedulers.io())
        .throttleLatest(1, TimeUnit.SECONDS)
        .blockingSubscribe(
            item -> Log.e("RxJava",item),
                 Throwable::printStackTrace,
            () -> Log.e("RxJava","finished"));

打印结果:

13、take & takeLast

作用于Flowable、Observable,take发射前n个元素;takeLast发射后n个元素。

Observable<Integer> source = Observable.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

source.take(4)
    .subscribe(System.out::print);
//打印:1 2 3 4

source.takeLast(4)
    .subscribe(System.out::println);
//打印:7 8 9 10

14、timeout(超时)

作用于Flowable、Observable、Maybe、Single、Completabl。后一个数据发射未在前一个元素发射后规定时间内发射则返回超时异常。

Observable<String> source = Observable.create(emitter -> {
    emitter.onNext("A");

    Thread.sleep(800);
    emitter.onNext("B");

    Thread.sleep(400);
    emitter.onNext("C");

    Thread.sleep(1200);
    emitter.onNext("D");
    emitter.onComplete();
});

source.timeout(1, TimeUnit.SECONDS)
        .subscribe(
                item -> System.out.println("onNext: " + item),
                error -> System.out.println("onError: " + error),
                () -> System.out.println("onComplete will not be printed!"));
// 打印:
// onNext: A
// onNext: B
// onNext: C
// onError: java.util.concurrent.TimeoutException: 
            The source did not signal an event for 1 seconds 
            and has been terminated.

连接操作符

通过连接操作符,将多个被观察数据(数据源)连接在一起。

1、startWith

可作用于Flowable、Observable。将指定数据源合并在另外数据源的开头。

Observable<String> names = Observable.just("Spock", "McCoy");
Observable<String> otherNames = Observable.just("Git", "Code","8");
names.startWith(otherNames).subscribe(item -> Log.d(TAG,item));

//打印:
RxJava: Git
RxJava: Code
RxJava: 8
RxJava: Spock
RxJava: McCo

2、merge

可作用所有数据源类型,用于合并多个数据源到一个数据源。

Observable<String> names = Observable.just("Hello", "world");
Observable<String> otherNames = Observable.just("Git", "Code","8");

Observable.merge(names,otherNames).subscribe(name -> Log.d(TAG,name));

//也可以是
//names.mergeWith(otherNames).subscribe(name -> Log.d(TAG,name));

//打印:
RxJava: Hello
RxJava: world
RxJava: Git
RxJava: Code
RxJava: 8

merge在合并数据源时,如果一个合并发生异常后会立即调用观察者的onError方法,并停止合并。可通过mergeDelayError操作符,将发生的异常留到最后处理。

Observable<String> names = Observable.just("Hello", "world"); 
Observable<String> otherNames = Observable.just("Git", "Code","8");
Observable<String> error = Observable.error(    
                            new NullPointerException("Error!"));
Observable.mergeDelayError(names,error,otherNames).subscribe(
    name -> Log.d(TAG,name), e->Log.d(TAG,e.getMessage()));
    
//打印:
RxJava: Hello
RxJava: world
RxJava: Git
RxJava: Code
RxJava: 8
RxJava: Error!

3、zip

可作用于Flowable、Observable、Maybe、Single。将多个数据源的数据一个一个的合并在一起哇。当其中一个数据源发射完事件之后,若其他数据源还有数据未发射完毕,也会停止。

Observable<String> names = Observable.just("Hello", "world");
Observable<String> otherNames = Observable.just("Git", "Code", "8");
names.zipWith(otherNames, (first, last) -> first + "-" + last)
       .subscribe(item -> Log.d(TAG, item));

//打印:
RxJava: Hello-Git
RxJava: world-Code

4、combineLatest

可作用于Flowable, Observable。在结合不同数据源时,发射速度快的数据源最新item与较慢的相结合。
如下时间线,Observable-1发射速率快,发射了65,Observable-2才发射了C, 那么两者结合就是C5。

5、switchOnNext

一个发射多个小数据源的数据源,这些小数据源发射数据的时间发生重复时,取最新的数据源。

变换操作符

变化数据源的数据,并转化为新的数据源。

1、buffer

作用于Flowable、Observable。指将数据源拆解含有长度为n的list的多个数据源,不够n的成为一个数据源。

Observable.range(0, 10)
    .buffer(4)
    .subscribe((List<Integer> buffer) -> System.out.println(buffer));

// 打印:
// [0, 1, 2, 3]
// [4, 5, 6, 7]
// [8, 9]

2、cast

作用于Flowable、Observable、Maybe、Single。将数据元素转型成其他类型,转型失败会抛出异常。

Observable<Number> numbers = Observable.just(1, 4.0, 3f, 7, 12, 4.6, 5);

numbers.filter((Number x) -> Integer.class.isInstance(x))
    .cast(Integer.class)
    .subscribe((Integer x) -> System.out.println(x));
// prints:
// 1
// 7
// 12
// 5

3、concatMap

作用于Flowable、Observable、Maybe。将数据源的元素作用于指定函数后,将函数的返回值有序的存在新的数据源。

Observable.range(0, 5)
    .concatMap(i -> {
        long delay = Math.round(Math.random() * 2);

        return Observable.timer(delay, TimeUnit.SECONDS).map(n -> i);
    })
    .blockingSubscribe(System.out::print);

// prints 01234

4、concatMapDelayError

与concatMap作用相同,只是将过程发送的所有错误延迟到最后处理。

Observable.intervalRange(1, 3, 0, 1, TimeUnit.SECONDS)
    .concatMapDelayError(x -> {
        if (x.equals(1L)) return Observable.error(new IOException("Something went wrong!"));
        else return Observable.just(x, x * x);
    })
    .blockingSubscribe(
        x -> System.out.println("onNext: " + x),
        error -> System.out.println("onError: " + error.getMessage()));

// prints:
// onNext: 2
// onNext: 4
// onNext: 3
// onNext: 9
// onError: Something went wrong!

5、concatMapCompletable

作用于Flowable、Observable。与contactMap类似,不过应用于函数后,返回的是CompletableSource。订阅一次并在所有CompletableSource对象完成时返回一个Completable对象。

Observable<Integer> source = Observable.just(2, 1, 3);
Completable completable = source.concatMapCompletable(x -> {
    return Completable.timer(x, TimeUnit.SECONDS)
        .doOnComplete(() -> System.out.println("Info: Processing of item \"" + x + "\" completed"));
    });

completable.doOnComplete(() -> System.out.println("Info: Processing of all items completed"))
    .blockingAwait();

// prints:
// Info: Processing of item "2" completed
// Info: Processing of item "1" completed
// Info: Processing of item "3" completed
// Info: Processing of all items completed

6、concatMapCompletableDelayError

与concatMapCompletable作用相同,只是将过程发送的所有错误延迟到最后处理。

Observable<Integer> source = Observable.just(2, 1, 3);
Completable completable = source.concatMapCompletableDelayError(x -> {
    if (x.equals(2)) {
        return Completable.error(new IOException("Processing of item \"" + x + "\" failed!"));
    } else {
        return Completable.timer(1, TimeUnit.SECONDS)
            .doOnComplete(() -> System.out.println("Info: Processing of item \"" + x + "\" completed"));
    }
});

completable.doOnError(error -> System.out.println("Error: " + error.getMessage()))
    .onErrorComplete()
    .blockingAwait();

// prints:
// Info: Processing of item "1" completed
// Info: Processing of item "3" completed
// Error: Processing of item "2" failed!

ContactMap

8、flatMap

作用于Flowable、Observable、Maybe、Single。与contactMap类似,只是contactMap的数据发射是有序的,而flatMap是无序的。

Observable.just("A", "B", "C")
    .flatMap(a -> {
        return Observable.intervalRange(1, 3, 0, 1, TimeUnit.SECONDS)
                .map(b -> '(' + a + ", " + b + ')');
    })
    .blockingSubscribe(System.out::println);

// prints (not necessarily in this order):
// (A, 1)
// (C, 1)
// (B, 1)
// (A, 2)
// (C, 2)
// (B, 2)
// (A, 3)
// (C, 3)
// (B, 3)

9、flatMapXXX 和 contactMapXXX

太多了,减少篇幅,大家感兴趣自己查阅官网吧。功能与flatMap和contactMap类似。

10、flattenAsFlowable & flattenAsObservable

作用于Maybe、Single,将其转化为Flowable,或Observable。

Single<Double> source = Single.just(2.0);
Flowable<Double> flowable = source.flattenAsFlowable(x -> {
    return List.of(x, Math.pow(x, 2), Math.pow(x, 3));
});

flowable.subscribe(x -> System.out.println("onNext: " + x));

// prints:
// onNext: 2.0
// onNext: 4.0
// onNext: 8.0

11、groupBy

作用于Flowable、Observable。根据一定的规则对数据源进行分组。

Observable<String> animals = Observable.just(
    "Tiger", "Elephant", "Cat", "Chameleon", "Frog", "Fish", "Turtle", "Flamingo");

animals.groupBy(animal -> animal.charAt(0), String::toUpperCase)
    .concatMapSingle(Observable::toList)
    .subscribe(System.out::println);

// prints:
// [TIGER, TURTLE]
// [ELEPHANT]
// [CAT, CHAMELEON]
// [FROG, FISH, FLAMINGO]

12、scan

作用于Flowable、Observable。对数据进行相关联操作,例如聚合等。

Observable.just(5, 3, 8, 1, 7)
    .scan(0, (partialSum, x) -> partialSum + x)
    .subscribe(System.out::println);

// prints:
// 0
// 5
// 8
// 16
// 17
// 24

13、window

对数据源发射出来的数据进行收集,按照指定的数量进行分组,以组的形式重新发射。

Observable.range(1, 4)
    // Create windows containing at most 2 items, and skip 3 items before starting a new window.
    .window(2)
    .flatMapSingle(window -> {
        return window.map(String::valueOf)
                .reduce(new StringJoiner(", ", "[", "]"), StringJoiner::add);
    })
    .subscribe(System.out::println);

// prints:
// [1, 2]
// [3, 4]

错误处理操作符

1、onErrorReturn

作用于Flowable、Observable、Maybe、Single。但调用数据源的onError函数后会回到该函数,可对错误进行处理,然后返回值,会调用观察者onNext()继续执行,执行完调用onComplete()函数结束所有事件的发射。

Single.just("2A")
    .map(v -> Integer.parseInt(v, 10))
    .onErrorReturn(error -> {
        if (error instanceof NumberFormatException) return 0;
        else throw new IllegalArgumentException();
    })
    .subscribe(
        System.out::println,
        error -> System.err.println("onError should not be printed!"));

// prints 0

2、onErrorReturnItem

与onErrorReturn类似,onErrorReturnItem不对错误进行处理,直接返回一个值。

Single.just("2A")
    .map(v -> Integer.parseInt(v, 10))
    .onErrorReturnItem(0)
    .subscribe(
        System.out::println,
        error -> System.err.println("onError should not be printed!"));

// prints 0

3、onExceptionResumeNext

可作用于Flowable、Observable、Maybe。onErrorReturn发生异常时,回调onComplete()函数后不再往下执行,而onExceptionResumeNext则是要在处理异常的时候返回一个数据源,然后继续执行,如果返回null,则调用观察者的onError()函数。

Observable.create((ObservableOnSubscribe<Integer>) e -> {
            e.onNext(1);
            e.onNext(2);
            e.onNext(3);
            e.onError(new NullPointerException());
            e.onNext(4);
        })
                .onErrorResumeNext(throwable -> {
                    Log.d(TAG, "onErrorResumeNext ");
                    return Observable.just(4);
                })
                .subscribe(new Observer<Integer>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        Log.d(TAG, "onSubscribe ");
                    }

                    @Override
                    public void onNext(Integer integer) {
                        Log.d(TAG, "onNext " + integer);
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.d(TAG, "onError ");
                    }

                    @Override
                    public void onComplete() {
                        Log.d(TAG, "onComplete ");
                    }
                });

结果:


onExceptionResumeNext操作符也是类似的,只是捕获Exception。

4、retry

可作用于所有的数据源,当发生错误时,数据源重复发射item,直到没有异常或者达到所指定的次数。

boolean first=true;

Observable.create((ObservableOnSubscribe<Integer>) e -> {
            e.onNext(1);
            e.onNext(2);

            if (first){
                first=false;
                e.onError(new NullPointerException());

            }
            
        })
                .retry(9)
                .subscribe(new Observer<Integer>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        Log.d(TAG, "onSubscribe ");
                    }

                    @Override
                    public void onNext(Integer integer) {
                        Log.d(TAG, "onNext " + integer);

                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.d(TAG, "onError ");
                    }

                    @Override
                    public void onComplete() {
                        Log.d(TAG, "onComplete ");
                    }
                });

结果:

5、retryUntil

作用于Flowable、Observable、Maybe。与retry类似,但发生异常时,返回值是false表示继续执行(重复发射数据),true不再执行,但会调用onError方法。

 Observable.create((ObservableOnSubscribe<Integer>) e -> {
            e.onNext(1);
            e.onNext(2);
            e.onError(new NullPointerException());
            e.onNext(3);
            e.onComplete();
        })
                .retryUntil(() -> true)
                .subscribe(new Observer<Integer>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        Log.d(TAG, "onSubscribe ");
                    }

                    @Override
                    public void onNext(Integer integer) {
                        Log.d(TAG, "onNext " + integer);

                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.d(TAG, "onError ");
                    }

                    @Override
                    public void onComplete() {
                        Log.d(TAG, "onComplete ");
                    }
                });

结果:

retryWhen与此类似,但其判断标准不是BooleanSupplier对象的getAsBoolean()函数的返回值。而是返回的 Observable或Flowable是否会发射异常事件。

总结

太多操作符太累了,看得心好累。还是根据需要查阅文档才是正确的姿势。本文写的操作符只是冰山一角,更多请参阅官网。

参阅官网

我也想聊聊Binder机制,可我不会

一、前言

想写篇关于Binder的文章,可对其一无所知,无从下手。在阅读了大量的优秀文章后,心惊胆战的提笔,不怕文章被贻笑大方,怕的是误人子弟!望各位大佬抽空阅读本文的同时,能够对文章的知识点持怀疑态度,共同探讨,共同进步!

一、序列化

日常开发中,通过Intent携带数据跳转Activity时,数据通常要通过实现Serializable或Parcelable接口,才能在被Intent所携带,而Serializable接口和Parcelabel接口主要是完成对象的序列化过程。将对象持久化到设备上或者网络传输同样也需要序列化。

1.Serializable 接口

Serializable接口是Java所提供的,为对象提供标准的序列化和反序列化操作。通常一个对象实现Serializable接口,该对象就具有被序列化和反序列化的能力,而且几乎所有工作有系统自动完成。Serializable接口内serialVersionID可指定也可以不指定,其作用是用来判断序列化前和反序列化的类版本是否发生变化。该变量如果值不一致,表示类中某些属性或者方法发生了更改,反序列化则出问题。(静态成员变量和transient关键字标记的成员不参与序列化过程)

2.Parcelable 接口

Parcelable 接口是Android所提供的,其实现相对来说比价复杂。实现该接口的类的对象就可以在Intent和Binder进行传递。

3.两者的区别

Serializable是Java提供的接口,使用简单,但序列化与反序列化需要大量的IO操作,所以开销比较大。Parcelable是Android提供的序列化方法,使用麻烦当效率高。在Android开发中,将对象序列化到设备或者序列化后通过网络传输建议使用Serializable接口,其他情况建议是用Parcelable接口,尤其在内存的序列化上。例如Intent和Binder传输数据。

二、AIDL

在Java层,想利用Binder进行夸进程的通信,那就得通过AIDL(Android 接口定义语言)了,AIDL是客户端与服务使用进程间通信 (IPC) 进行相互通信时都认可的编程接口,只有允许不同应用的客户端用 IPC 方式访问服务,并且想要在服务中处理多线程时,才有必要使用 AIDL,如果是在单应用(单进程),建议使用Messager。

1、AIDL支持的数据类型

  • Java 编程语言中的所有原语类型(如 int、long、char、boolean 等等)
  • String 和 CharSequence
  • 所有实现了Parcelable接口的对象
  • AIDL接口
  • List,目前List只支持ArrayList类型,持有元素必须是以上讲到类型。
  • Map,目前只支持HashMap类型,持有元素必须是以上讲到类型。

自定义的Parcelable对象和AIDL接口必须显示导入到AIDL文件中。

数据的走向

Parcelable对象和AIDL接口在使用前必须标明数据的走向:

  • in 客户端流向服务端
  • out 服务端流向客户端
  • inout 服务端与客户端可进行交互

示例:

void addUser(inout User user);

2、服务端的实现

2.1、定义数据对象

定义一个实现了Parcelable 接口,作为客户端和服务端传输的数据对象。

public class User implements Parcelable {

    private String username;

    private String address;

    public User() {
    }
    
    public User(String username, String address) {
        this.username = username;
        this.address = address;
    }

    User(Parcel in) {
       readFromParcel(in);
    }
    //系统默认生成,反序列化过程,我们只需要要构造方法读取相关值就可以
    public static final Creator<User> CREATOR = new Creator<User>() {
        @Override
        public User createFromParcel(Parcel in) {
            return new User(in);
        }

        @Override
        public User[] newArray(int size) {
            return new User[size];
        }
    };
     //系统默认生成,内容描述功能,几乎所有情况下都返回0,
     //仅仅当前存在文件描述符,才返回1
    @Override
    public int describeContents() {
        return 0;
    }
    //序列化过程,通过一系列的write将值写到Parcel 对象
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(username);
        dest.writeString(address);
    }
    
    @Override
    public String toString() {
        return username+":"+address;
    }

    public void readFromParcel(Parcel in){
        username=in.readString();
        address=in.readString();
    }
}

2.2、抽象服务端服务

通过下面方法,建立一个UserManger.aidl文件,表示服务端能为客户端提供什么样的服务。

下面代码通过建立UserManager.aidl文件,为客户端提供addUsergetUser的能力。UserManager可以理解为,服务端和客户端的共同约定,两者能进行怎么样的交互。

package com.gitcode.server;

// 在这里要导入传递对象的类型,例如User
import com.gitcode.server.User;

interface UserManager {

    void addUser(inout User user);

    User getUser(int index);
}

定义UserManager.aidl文件后,系统默认会生成UserManager.java文件。


UserManager.java的代码如下,为了减少篇幅,去掉了一些实现。

public interface UserManager extends android.os.IInterface {

    public static abstract class Stub extends android.os.Binder implements com.gitcode.server.UserManager {
        private static final String DESCRIPTOR = "com.gitcode.server.UserManager";
        
        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }
        
        public static com.gitcode.server.UserManager asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.gitcode.server.UserManager))) {
                return ((com.gitcode.server.UserManager) iin);
            }
            return new Stub.Proxy(obj);
        }

        @Override
        public android.os.IBinder asBinder() {
            return this;
        }

        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
           ......
        }

        private static class Proxy implements com.gitcode.server.UserManager {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }

            public String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            @Override
            public void addUser(com.gitcode.server.User user) throws android.os.RemoteException {
                 ......
            }

            @Override
            public com.gitcode.server.User getUser(int index) throws android.os.RemoteException {
                .....
            }
        }

        static final int TRANSACTION_addUser = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_getUser = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }

    public void addUser(com.gitcode.server.User user) throws android.os.RemoteException;

    public com.gitcode.server.User getUser(int index) throws android.os.RemoteException;
}

从上文可知,UserManager本身是一个接口,并继承IInterface接口。UserManager.java声明了addUsergetUser,和在UserManager.aidl的声明是一致的。同时声明两个整型TRANSACTION_addUserTRANSACTION_getUser,用于在transact()方法中标识调用服务端哪个方法。如果服务端和客户端在不同进程,方法调用会走transact()方法,逻辑由Stub 和Proxy 内部类完成。

内部类Stub的一些概念和方法含义:

DESCRIPTOR

Binder的唯一标识,一般用当前的类名全名标识。

asInterface(IBinder obj)

将服务端的Binder对象转换成客户端的AIDL接口类型的对象,如果客户端和服务端同一进程,直接返回Stub对象本身,不在同一进程,则返回由系统封装的Stub.proxy对象。

asBinder

返回当前Binder对象

onTransact(int code, Parcel data, Parcel reply, int flags)

运行在服务端Binder线程池,当客户端跨进程发起请求后,系统封装后交由此方法来处理。code表示调用服务端什么方法,上文声明的整型。data表示客户端传递过来的数据,reply为服务端对客户端的回复。

内部代理类 Poxy,表示客户端远程能对服务端进行的操作。

addUser运行在客户端,当客户端远程调用时,


在相同目录下创建User.aidl,可以直接复制UserManager.aidl,内容修改如下。

package com.gitcode.server;

parcelable User;

在服务端中,服务一般以Service体现,定义UserServcie,继承Service。

public class UserService extends Service {
    private static final String TAG = "Server";
    private List<User> list = new ArrayList<>();

    @Override
    public void onCreate() {
        super.onCreate();
        list.add(new User("GitCode", "深圳"));
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.i(TAG,"on Bind");
        return stub;
    }


    private UserManager.Stub stub = new UserManager.Stub() {
        @Override
        public void addUser(User user) throws RemoteException {
            list.add(user);
            Log.i(TAG,"add user:"+user);
        }

        @Override
        public User getUser(int index) throws RemoteException {
            Log.i(TAG,"get user,index:"+index);
            return list.size() > index && index >= 0 ? list.get(index) : null;
        }
    };
}

在AndroidManifest.xml文件声明Service,以两个组件形成单独的app来体现两个进程,通过AIDL进行数据交互。在客户端通过bindService()来启动该服务。

<service android:name="com.gitcode.server.UserService"
    android:enabled="true"
    android:exported="true">
        <intent-filter>
            <action android:name="com.gitcode.server.userservice"/>
            <category android:name="android.intent.category.DEFAULT"/>
        </intent-filter>
</service>

3、客户端的实现

客户端主要是通过共同的约定(UserManger.aidl)向服务端进行请求,服务端响应客户端的请求。为了提高效率和减少出错,通过拷贝来实现客户端的AIDL文件。将服务端的aidl整个文件拷贝到客户端的main目录下,不做任何修改


在客户端建立与服务端User类同包的目录,并将User类拷贝过来,不做任何修改

在Activity中绑定服务端的Service,绑定成功后进行数据交互。

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Client";
    private UserManager mUserManager;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        toBindService();
    }

    private void toBindService() {
        Intent intent = new Intent("com.gitcode.server.userservice");
        intent.setPackage("com.gitcode.server");
        bindService(intent, connection, Context.BIND_AUTO_CREATE);
    }

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mUserManager = UserManager.Stub.asInterface(service);

            try {
                User user = mUserManager.getUser(0);
                Log.e(TAG, user.toString());

                mUserManager.addUser(new User("张三","北京"));
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };
}

运行效果:

客户端:

服务端:

3、小结

客户端调用服务的方法,被调用的方法运行在服务端的的Binder线程池,同时客户端会被挂起,如果服务端方法执行耗时操作,就会导致客户端ANR,所以不要在客户端主线程访问远程服务方法。同时服务端不应该自己新建新建线程运行服务方法,因为方法会交由线程池处理,同时对数据也要做好并发访问处理。

AIDL可以说为应用层开发提供了封装,不用过多的了解Binder的机制,通过生成的UserManager.java,初步可以了解Binder的IPC机制。使用AIDL在进程之间进行数据通信,更注重的是细节和业务的实现。

上文demo地址

三、Binder

Binder是Android系统提供的一种IPC机制。由于Android是基于Linux内核,因此,除了Binder以外,还有其他的IPC机制,例如Socket,共享内存,管道和消息队列等。之所有不使用原有的 IPC机制,是因为使用Binder机制,能从性能、稳定性、安全性带来更好的效果。例如,Socket是一套通用的接口,传输速率低下,适合网络传输这种情况,而管道和消息队列需要数据的两次拷贝,共享内容难以管控等。而Binder对数据只需要一次拷贝,使用C/S架构,职责明确,容易维护和使用。

通过下图可以了解到,Binder机制通过内存映射实现跨进程通信,Binder在IPC机制只是作为一个数据的载体,当进程A向虚拟内存空间中写入数据,数据会被实时反馈到进程B的虚拟内存空间。整个发送数据的过程,只从用户空间拷贝一次到虚拟内存空间。


在Binder机制中,主要涉及到Client、Server、ServiceManger三个端,三者通过Binder进行跨进程通信,支持着Android这个大网络。它们的关系如下图。


Server

Server进程需要注册一些Service到ServiceManger中,以对外告知其可提供的服务。例如上文AIDL中,会注册UserService,并为Client提供添加User和获取User的操作。注册的过程,Server进程就是客户端,而ServiceManger就是服务端。

Client

对Sever进程进行业务逻辑操作。通过Service的名称在ServiceManger查找对应的Service。

ServiceManager

ServiceManger集中管理系统内的所有Service,服务通过注册Service到ServiceManger的查找表中,当Client根据Service名称请求ServiceManger在查找表中查询对应的Service。

图表示三者的C/S架构,例如Client查询向ServiceManger查询Service时,Client就是客户端,而ServiceManger就是服务端。而虚线则表示两者之间通过Binder进行进程间的通信,因此通过了解一条虚线的流程,就可以知道Binder的机制。

1、Service的注册过程

通过Server进程的注册Service过程,可以了解到Binder机制的工作原理。


BpServcieManger和BnServcieManger是客户端与服务端进程业务层辑实现的封装,而BpBinder和BBinder是IPC机制的方式。此时Server进程是客户端,ServiceManger是服务端。

1.1 ProcessState

每个进程通过单例模式创建了唯一的ProcessState对象,在其构造器中,通过 open_driver()方法打开了/dev/binder设备,相当于Server进程打开了与内核的Binder驱动交互的通道,并设置最大支持线程数为15。binder设备是Android在内核中为完成进程间通信而专门设置的一个虚拟设备。

1.2 BpBinder和BBinder

BpBinder与BBinder都是Android与Binder通信相关的代表,两者一一对应,都从IBinder派生而来。如果说BpBinder代表客户端,那么BBinder就代表服务端,一个BpBinder通过handler标识与对应的BBinder进行交互。在Binder系统,handler标识为0代表着ServiceManger所对应的BBinder。BpBinder与BBinder并没有直接操作ProcessState打开的binder设备。

1.3 BpServiceManger和BnserviceManger

两者继承至IServiceManger,与业务逻辑相关,可以说将业务层的逻辑架构到Binder机制上。BnserviceManger从IServiceManger BBinder派生而来,可直接参与Binder的通信,而BpServiceManger通过mRemote指向BpBinder。

1.4 注册相关Service

通过上文三小节,BpServiceManger对象实现对IServiceManger的业务函数,又有BpBinder作为通信代表,下面分析一下注册的过程。

将字符串名字和Service对象作为参数传到BpServiceManger对象的addService()函数,该方法将参数数据打包后传递给BpBidner的transact()函数。业务层的逻辑到此就结束,主要作用是将请求信息打包交给通信层去处理。

在BpBinder的transact()函数调用了IPCThreadState对象的transact()函数,所以说BpBinder本身没有参与Binder设备的交互。每个线程都有一个IPCThreadState对象,其拥有一个mOut、mIn的缓冲区,mOut用来存储转发Binder设备的数据,而mIn用来接收Binder设备的数据。通过ioctl方式与Binder设备进行交互。

1.5 小结

通过上文Service的注册过程,分析了Binder的机制。Binder只是通信机制,业务可以基于Binder机制,也可以基于其他IPC方式的机制,也就是上文为啥有BpServiceManger和BpBinder。Binder之所以复杂,是Android通过层层的封装,巧妙的将业务与通信融合在一起。主要还是设计理想很牛逼。

2、ServiceManger

通过1小节的分析,是否应该也有一个类继承自BnServiceManger来处理远方请求呢?

很可惜的是在服务端并没有BnServiceManger子类来响应远程客户端的请求,而是交给了ServiceManger来处理。

2.1 成为Service管理中心

ServiceManger通过binder_open函数打开binder设备,并映射内存。通过handler等于0标识自己,让自己成为管理中心,所有service向ServiceManger注册时,都是通过handle标识为的0的BpBinder找到ServiceManger对应的BBinder,ServiceManager会保存要注册的Service的相关信息,方便Client查找。并不是所有的Service都可以在ServiceManger注册,如果Server进程的权限不够root或system,那么需要在allowed添加相应的项。

2.2 ServiceManger集中管理带来的好处

  • 统一管理,施加管控权
  • 通知字符串名称查找Service
  • Server进程生命无常,通过ServiceManger,Client可以实时知道Server进程的最行动态。

3、Client

Client想要使用Server进程提供的Service,又该进行哪些步骤呢?

3.1 查询ServiceManger

Client想要得到某个Service的信息,就得与ServiceManager打交道,通过调用getService()方法来获取对应Service信息。Client通过服务名称向ServiceManger查询对应的Service。如果Service未注册,则循环等待直到该Service注册;如果已注册,则会对应封装了一个能与远程Service通信的BpBinder的BpXXXService,通过该Service,Client客户调用相关业务逻辑函数。

3.2 请求信息的处理

Client调用的业务函数,莫非就是将请求参数打包发送给Binder驱动,BpBinder通过handler的值找到对应端的Service来处理。

在1.4小节中,说到IPCThreadState对象,在其executeCommand函数中,通过调用实现了BnServiceXXX的对象onTransact函数,直接定位到业务层。这就是在AIDL中,为什么在onTransact()函数中处理响应数据。

4、总结

通过对Binder机制的学习,了解Android是如何通过层层封装将Binder机制集成要应用程序,对Binder机制有一个较深入的理解。可以通过第Java层AIDL的使用,加深对Binder机制的理解。

个人水平有限,有误请帮忙勘正,谢谢大佬。喜欢就帮忙点个Star呗。

参考资料:

深入理解Android 卷一

转载权申请

哈喽,您好!

我叫大飞,我是一个 Android Coder,我是在掘金上看到你的,觉得你的文章写的非常棒!我平时也比较喜欢学习和分享,我不知道如何联系您,因此我只能在这里给提个 issue 了。

我读了你的「代码洁癖症的我,学习Lint学到心态爆炸」 这篇文章,觉得写的非常好,因此我想申请转载您的这篇文章到我的公众号,好文章是需要分享的,以便分享给更多的读者朋友们,真真切切的帮助到他们,当然我会注明转载的原文地址和出处的,如果可以的话,你可以加下我的微信:542270559,有机会的话我们可以互相学习和交流一下。

Best Regards.

Android Studio爬坑日记------持续更新

本文记录在Android Studio踩的坑....

1、Decompiled .class file,bytecode version:51.0(Java 7)

网上解决方法

但解决不了我的,最后我让Android Studio扫描整个SDK文件,就搞定了。

2、Gradle sync issue connection timeout

Mac 更新到Android Studio 3.4.2版本,Gradle 5.1.1怎么同步都同步不了,而在Windows一下就好。

网上解决方法

Google几天,都没有用,最后的最后,卸载干净,重新安装就好..........

3、Android Studio(Windows环境) Terminal控制台git log 中文乱码

乱码格式如下:

<E4><BF><AE><E6><94><B9><E6><96><87><E6><9C><AC><E6><96><87><E6><A1><A3>

不知道从3.X的哪个版本开始就会了,一直没解决。修改Windows环境变量即可。

  1. 我的电脑,点击鼠标右键->属性->高级系统设置->环境变量
  2. 添加系统变量。变量名:LC_ALL 变量值:C.UTF-8。
  3. 确定保存后,重启Android Studio即可。
    image
    网上解决方法

Android 消息机制

前言

Android 的消息机制原理是Android进阶必学知识点之一,在Android面试也是常问问题之一。在Android中,子线程是不能直接操作View,需要切换到主线程进行。那么这个切换动作就涉及到了Android的消息机制,也就是本文要讲的Handler、Looper、MessageQueue、Message它们之间的关系。

Handler

Handler在消息机制中扮演发送消息处理消息的角色,也是我们平常接触最多的类。

Handler如何处理消息?

下面代码展示Handler如何处理消息。新建Handler对象,并重写handleMessage,在方法内处理相关逻辑,一般处理和主线程相关的逻辑。Handler有很多的构造器,下面构造器常用在主线程。

  private Handler handler=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            if (msg.what==1){
                Toast.makeText(MainActivity.this,"handle message",Toast.LENGTH_LONG).show();
            }
            
        }
    };
    

Handler是如何发送消息的呢?

通过下面的代码可以了解到,Handler对象支持发送MessageRunable。Runable最终被包装成Messagecallback实例变量(Handler对象处理消息会优先处理callback的逻辑),和Message一样的方式放到消息队列中。而每个方法都有相关的变形,支持延迟发送,或者未来的某段时间里发送等等。

    //在消息池获取消息体,能达到消息重用,如果消息池没有消息,则新建消息
        Message msg = handler.obtainMessage();
        msg.what = 1;
        //发送消息
        handler.sendMessage(msg);
        //发送空消息,参数会自动被包装msg.what=1
        handler.sendEmptyMessage(1);
        //未来的时间里发送消息
        handler.sendEmptyMessageAtTime(1, 1000);
        //延迟发送消息
        handler.sendEmptyMessageDelayed(1, 1000);

        msg = handler.obtainMessage();
        msg.what = 2;
        //将消息发送消息队列前面
        handler.sendMessageAtFrontOfQueue(msg);
        //发送任务,run方法内容将handler被处理。
        handler.post(new Runnable() {
            @Override
            public void run() {
                Log.i("Handler", "Runnable");
            }
        });

如果平常使用,我们只需要主线程定义Hanlder处理消息的内容,在子线程发送消息即可达到切换流程。

Looper

Looper负责循环的从消息队列中取消息,发送给Handler处理。因为消息队列只用来存储消息,所以需要Looper不断的从消息队列中取消息给Handler。默认情况,所有线程并不拥有Looper。如果在子线程直接执行Looper.loop方法,就会发生异常。那主线程为什么不会报错?在App的启动流程中,创建ActivityThread时,会调用Looper.prepare来创建LooperMessageQueue,和Looper.loop开启循环。也就是系统为我们在主线程创建LooperMessageQueue。所以,在子线创建Handler前,需要先调用Looper.prepare方法,创建LooperMessageQueueIntentService就是这样实现的。点击看IntentService的知识点。

MessageQueue

MessageQueue内部是以链表的形式组织的,主要作用是存储Message。在创建Looper的时候,会自动创建MessageQueue

三者关系形成了Android的消息机制

Handler发送消息时会将消息插入到MessageQueue,而Looper不断的从MessageQueue中取消息分发给Handler处理。

源码解析

Handler的构建

我们先看一下Handler的构造器代码。

    public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }
        //分析一
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        //分析二
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
    

Handler有很多重载的构造器,我们常用在使用默认构造器,最终会调用上面的构造器。

分析一

通过Looper.myLooper(),获Looper的实例。而在myLooper的实现中,是通过ThreadLocalget方法来获取的。如果ThreadLocal不存在Looper,则放回nullThreaLocal这里可以简单理解为保存当前线程私有独立的实例,其他线程不可访问。如果ThreadLocal不存在Looper实例则,返回null。这也就是前面说的,在子线程创建Handler前,需要先调用Looper.prepare方法。否则会抛出RuntimeException

 public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

分析二

mQueueLooper中的消息队列,mCallBack定义了一个接口,用于回调消息处理。

  public interface Callback {
        /**
         * @param msg A {@link android.os.Message Message} object
         * @return True if no further handling is desired
         */
        public boolean handleMessage(Message msg);
    }

Handler发送消息

Handler所有发送消息方法的变体最终都会以下面方法放去到消息队列中。

    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

这里最重要的就是enqueueMessage方法中,将当前Handler对象设置给Messagetarget变量。然后调用队列queueenqueueMessage方法。

    boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            //如果队列为空或者插入message未来处理时间小于当前对头when
            //则将当前消息设为队列头
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

而在MessageQueueenqueueMessage方法中,会先检查target是否nullmessage是否应在使用,当前线程是否退出,死亡状态。如果是,则抛出异常。如果当前队列是空或者阻塞,直接当前Message对象设为队列的头并唤醒线程。如果不是,则根据Message对象的when插入到队列合适的位置。因此可以看得出,Handler发送消息时是将消息放到队列中。

Looper和MessageQueue的创建

前面讲过,子线程使用Handler,需要调用Looper的静态prepare方法。

 public static void prepare() {
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

如果当前线程已经有Looper,代用Looper就会报错。如果没有,new Looper并保存到ThreadLocal中。new Looper非常简单,只是新建一个MessageQueue,和持有当前线程。

 private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

Looper是如何实现循环的

在调用了Looper.prepare创建LooperMessageQueue对象后,要调用Loop.loop的开始循环分发消息队列中消息。

 public static void loop() {
        final Looper me = myLooper();
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        // Allow overriding a threshold with a system prop. e.g.
        // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
        final int thresholdOverride =
                SystemProperties.getInt("log.looper."
                        + Process.myUid() + "."
                        + Thread.currentThread().getName()
                        + ".slow", 0);

        boolean slowDeliveryDetected = false;
        //分析一
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            final long traceTag = me.mTraceTag;
            long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
            long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
            if (thresholdOverride > 0) {
                slowDispatchThresholdMs = thresholdOverride;
                slowDeliveryThresholdMs = thresholdOverride;
            }
            final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
            final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

            final boolean needStartTime = logSlowDelivery || logSlowDispatch;
            final boolean needEndTime = logSlowDispatch;

            if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
                Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
            }

            final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
            final long dispatchEnd;
            try {
                //分析二:
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            if (logSlowDelivery) {
                if (slowDeliveryDetected) {
                    if ((dispatchStart - msg.when) <= 10) {
                        Slog.w(TAG, "Drained");
                        slowDeliveryDetected = false;
                    }
                } else {
                    if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                            msg)) {
                        // Once we write a slow delivery log, suppress until the queue drains.
                        slowDeliveryDetected = true;
                    }
                }
            }
            if (logSlowDispatch) {
                showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
            }

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }


            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

            msg.recycleUnchecked();
        }
    }

分析一:
通过无限制的for循环,读取队列的消息。而MessageQueuenext方法内部通过链表的形式,根据when属性的顺序返回message

分析二:
调用Message对象的targetdipatchMessage方法。这里的target就是发送消息的Handler对象。而在Handler对象的dipatchMessage方法中,优先执行Message对象的callback方法,即优先执行我们发送消息时以Runable发送的任务,如果有的话。不然检测Callback对象的handleMessage方法,最后才是我们重写Hanlder对象的handleMessage方法。因为Handler不仅有默认构造函数,还有可以传入Callback,Looper等的构造函数。

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

Message的复用

通过handler.obtainMessage而不是new方式获得消息实例。因为obtainMessage方法会先检测消息池是否有可以复用的消息,没有再去new一个消息实例。下面是类Message的obtain方法。

public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

sPool的类型是Message,内部通过成员变量next,维护一个消息池。虽然叫消息池,内部却通过next不断的指向下一个Message,以链表维护的这个消息池,默认大小为50。在链表sPool不为空的情况,取表头Message元素,并将相关属性进行初始化。

那么Message对象是在什么时候被放进消息池中的呢?

Looperloop方法中,最后调用MessagerecycleUnchecked方法

void recycleUnchecked() {
        // Mark the message as in use while it remains in the recycled object pool.
        // Clear out all other details.
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null;
        callback = null;
        data = null;

        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }

在同步代码块,可以看到,将sPool指向当前要被回收的Message对象,而Messagenext指向之前的表头。

总结

  1. 在子线程使用Handler,需要先调用Looper.prepare方法,再调用Looper.loop方法。
  2. 消息队列以链表的形式维护着,消息的存放和获取顺序根据when时间依次排列。
  3. 通过Handler,在子线程耗时操作,主线程更新UI。应用场景:IntentServiceHandlerTreadAsyncTack

知识点分享

HandlerThread必知必会

IntentService必知必会

Android AsyncTask

前言

AsyncTaskHandlerThradIntentService都是对Android消息机制的封装和应用,解决在子线程耗时任务,主线程更新UI的问题。

AsyncTask的使用

AsyncTask是一个抽象类,通过子类继承重写doInBackground,该方法在子线程运行。

public class DownloadTask extends AsyncTask<String,Integer,String> {

    @Override
    protected void onPostExecute(String s) {
        super.onPostExecute(s);
    }

    @Override
    protected String doInBackground(String... strings) {
        publishProgress(1);
        return "AsyncTask";
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        super.onProgressUpdate(values);
    }
}

AsyncTask<Params, Progress, Result> 的泛型参数,可以根据业务需求,传不同的类型。第一个参数Params是指传到doInBackground的参数类型,例如瞎子啊进度的URL。第二个参数Progress是指在执行doInBackground方法时,通过publishProgress更新进度状态的类型,例如下载的进度。第三个参数ResultdoInBackground方法执行结束后传到onPostExecute的参数类型,例如下载结果。

相关方法

  • onPreExecute()

运行在主线程。可以开始任务之前,对UI或者数据进行初步准备。非必需方法。

  • doInBackground

在子线程(线程池)运行。一般进行耗时操作,例如下载。为AsyncTask的抽象方法,必须实现。

  • onProgressUpdate

运行在主线程,在doInBackground方法中调用publishProgress方法会回调该方法,显示当前任务状态。常用来更新下载进度。非必需方法。

  • onPostExecute

在主线程运行。在doInBackground方法中return值之后,将回调该方法。非必需方法。

  • onCancelled

在主线程运行,任务完成时回调该方法,表示任务结束。

开始任务

task的execute只能调用一次,不然报错。

    DownloadTask task=new DownloadTask();
    task.execute("Git");

源码分析

AsyncTask的构造函数

Android中代码注释是必须在UI线程中调用,因为UI线程默认拥有Looper,以及AsyncTask需要更新UI。如果不需要更新UI,在有Looper的子线程都可以创建。

    //构造方法1
    public AsyncTask() {
        this((Looper) null);
    }
    //构造方法2
    public AsyncTask(@Nullable Handler handler) {
        this(handler != null ? handler.getLooper() : null);
    }
    //构造方法3
    public AsyncTask(@Nullable Looper callbackLooper) {
        分析一:
        mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
            ? getMainHandler()
            : new Handler(callbackLooper);
        分析二:
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);
                Result result = null;
                try {
                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                    //noinspection unchecked
                    result = doInBackground(mParams);
                    Binder.flushPendingCommands();
                } catch (Throwable tr) {
                    mCancelled.set(true);
                    throw tr;
                } finally {
                    postResult(result);
                }
                return result;
            }
        };
       
        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                try {
                    postResultIfNotInvoked(get());
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occurred while executing doInBackground()",
                            e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                }
            }
        };
    }

构造方法1、2,最终调用构造方法3。看看构造方法3做了什么?

分析一

给mHanlder赋值初始化。先判断是否传入Looper对象,如果没有传入,或者传入Looper对象并且和UI线程的Looper相同,则通过调用getMainHandler方法调用UI线程的Handler对象(这里可以简单理解,AsyncTask在UI线程创建的)。如果不相等,new一个 Handler对象。

    //AsyncTask
    private static Handler getMainHandler() {
        synchronized (AsyncTask.class) {
            if (sHandler == null) {
                sHandler = new InternalHandler(Looper.getMainLooper());
            }
            return sHandler;
        }
    }
    //Looper
    public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }

AsyncTask的getMainHandler方法,通过调用Looper的getMainLooper方法来获得Looper对象(UI线程的Looper对象)并创建InternalHandler。这里可以看到,在没有传入Looper的情况,且不在UI线程创建AsyncTask,会获取不到Looper对象。sHandler变量是InternalHandler类型,继承Handler。

    private static class InternalHandler extends Handler {
        public InternalHandler(Looper looper) {
            super(looper);
        }

        @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    // There is only one result
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }

在方法处理中,可以看到是对MESSAGE_POST_RESULT消息和MESSAGE_POST_PROGRESS消息的处理,用来在任务结束和进度更新时,切换到主线程,回调相关方法。

分析二

静态抽象类WorkerRunnable继承Callable,只多添加了数组mParams,用于保存参数。在WorkerRunnable的call方法中,主要调用我们重写的doInBackground。最终对调用postResult方法。

    private Result postResult(Result result) {
        @SuppressWarnings("unchecked")
        Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
                new AsyncTaskResult<Result>(this, result));
        message.sendToTarget();
        return result;
    }

在分析一中,最后看到InternalHandler对象对MESSAGE_POST_RESULT消息的处理,调用AsyncTask对象的finish方法。

     private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            onPostExecute(result);
        }
        mStatus = Status.FINISHED;
    }

根据当前AsyncTask的状态调用onCancelled或者onPostExecute方法。

在AsyncTask的构造方法中。通过构造Handler对象,和构造WorkRunnable对象,并将WorkRunnable对象用于创建FutureTask对象。FutureTask实现了RunnableFuture接口,而RunnableFuture接口继承了Runnable接口和Future接口。所以FutureTask既能做一个Runnable对象直接被Thread执行,也能作为Future对象用来得到Callable的计算结果。

AsyncTask的执行


        DownloadTask task=new DownloadTask();

        task.execute("Git");

我们在UI线程创建DownloadTask对象,并将调用DownloadTask对象的execute方法,Git方法是我们要传到doInBackground方法的值;

    public final AsyncTask<Params, Progress, Result> execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }

调用AsyncTask的execute方法会调用executeOnExecutor方法。

   public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
            Params... params) {
        if (mStatus != Status.PENDING) {
            switch (mStatus) {
                case RUNNING:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task is already running.");
                case FINISHED:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task has already been executed "
                            + "(a task can be executed only once)");
            }
        }

        mStatus = Status.RUNNING;

        onPreExecute();

        mWorker.mParams = params;
        exec.execute(mFuture);

        return this;
    }

如果当前AsyncTask对象正在运行或者结束,会抛出异常,一个task只能运行一次。在这里调用了onPreExecute方法,并参数赋值给了前面讲到的WorkerRunable对象的omParams变量。通过线程池对象exex执行在构造方法创建的FutureTask对象,最终对调用WorkerRunable对象的call方法,从而执行我们重写的doInBackground方法。

这里我们看看线程池对象sDefaultExecutor。

public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;

类SerialExecutor的构造方法

    private static class SerialExecutor implements Executor {
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
        Runnable mActive;

        public synchronized void execute(final Runnable r) {
            mTasks.offer(new Runnable() {
                public void run() {
                    try {
                        r.run();
                    } finally {
                        scheduleNext();
                    }
                }
            });
            if (mActive == null) {
                scheduleNext();
            }
        }

        protected synchronized void scheduleNext() {
            if ((mActive = mTasks.poll()) != null) {
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }
    }

通过ArrayDequ保存任务,并以加锁机制同步execute方法,串行执行任务。

THREAD_POOL_EXECUTO是什么东东?

  public static final Executor THREAD_POOL_EXECUTOR;

    static {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
                sPoolWorkQueue, sThreadFactory);
        threadPoolExecutor.allowCoreThreadTimeOut(true);
        THREAD_POOL_EXECUTOR = threadPoolExecutor;
    }
    
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final int KEEP_ALIVE_SECONDS = 30;

毫无意外是一个线程池。

总结

通过AsyncTask的构造器的跟踪,我们了解AsyncTask的实现的整体流程。通过Looper对象创建Handler对象,用于在线程池中发送消息,切换到UI线程,进行相关操作。并创建WorkRunnable对象调用后台任务(我们重写的doInBackground方法),将WorkRunnable对象传给FutureTask对象,这样在线程池中执行时,对结果和过程可控。

在AsyncTask的execute方法追踪,我们知道后台任务采用双向队列保存,线程池通过锁同步串行运行。

另外需要注意的是,AsyncTask的生命周期和Activity的生命周期并不一致,若AsyncTask持有Activity,容易造成内存泄漏。类似下载耗时操作建议采用IntentServcie

Android Jetpack ViewModel

以注重生命周期的方式管理和界面相关的数据,Jetpack为我们带来了ViewModel,从本文你可以学习到使用ViewModel的正确姿势

一、ViewModel

ViewModel被用来以注重生命周期的方式来保存数据和管理界面相关的数据。同时可以在相关配置发生变化是保存数据,例如屏幕旋转。

由于Android framework管理着像Activity、Fragment这样具有生命周期的UI控制器,所以在用户的某些操作或者系统分发的某些事件,会导致framework在没有经过我们控制下,销毁和重建UI控制器。例如,屏幕发生旋转时,framwork会调用Activity的onSaveIntanceState()保存数据,在新Activity的onCreate()方法中恢复这些数据,不过这仅仅是恢复可序列化和反序列化的小数据上,像list或者bitmap这种大数据就不合适了。此外,也会导致大量已经存在的数据被销毁,然后重新生成,造成资源的浪费。

LiveData也说到,不要在Activity和Fragment做大量的逻辑操作,会导致代码臃肿,难以维护,建议把相关逻辑抽到单独的类进行维护,让UI控制器负责它们的本质工作。

为此,使用ViewModel可以轻松解决以上问题。

1. 实现ViewModel

Architecture Components提供ViewModel工具类,用来为UI提供数据。ViewModel对象在配置发生变化时会自动保存数据,并且会在新建的Activity或Fragment实例使用。例如,在APP中需要显示持有多个user对象的list,应该将请求和保存users数据的动作在ViewModel对象实现,而不是Activity或Fragment。

class MyViewModel : ViewModel() {
    private val users: MutableLiveData<List<User>> by lazy {
        MutableLiveData().also {
            loadUsers()
        }
    }

    fun getUsers(): LiveData<List<User>> {
        return users
    }

    private fun loadUsers() {
        // Do an asynchronous operation to fetch users.
    }
}

在Activity中访问数据:

class MyActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        // Create a ViewModel the first time the system calls an activity’s onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.
        val model = ViewModelProviders.of(this).get(MyViewModel::class.java)
        model.getUsers().observe(this, Observer<List<User>>{ users ->
            // update UI
        })
    }
}

当Activity被重建时,它会接收到来自第一个Activity的ViewModel对象,所以数据不会发生变化。当Activity销毁后,framework会自动调用ViewModel对象的onCleared()方法来进行释放资源。记得,ViewModel一定不要持有View、Lifecycle或者任何有Activity context对象的引用。因为ViewModel的生命周期比它们的长,会导致内存泄漏。如果ViewModel需要持有应用的conetxt,可以继承AndroidViewModel类,在它的构造方法中对接收应用的context。

2. ViewModel的生命周期

ViewModelProvider获取ViewModel对象时,ViewModel的生命周期作用域会传递给ViewModelProvider,ViewModel会一直保持在内存中,直到其生命周期作用域失效。在下面两种情况,ViewModel对象生命周期会失效:一种是Activity对象finished,一种是Fragment对象detached。

下图(图来自官网)显示了Activity的生命周期和ViewModel的作用域,第一列显示了Activity对象的状态,第二列显示Activity生命周期方法,第三列ViewModel的作用域。

从图可以看出,在系统第一次调用Activity对象的onCreate()方法,我们通常要在第一次时间创建ViewModel对象。在Activity对象的生命周期内,onCreate()方法可能由于系统配置改变而被系统调用多次,而ViewModel对象只有一次,ViewModel对象会一直存在直到Activity被终结和销毁掉。

3. 在Fragment之间共享数据

在实际开发中,经常会在两个或多个fragement对象共享数据,通常做法是实定义接口,由Activity绑定在fragment中。此外,Activity还要处理fragment的创建和可见的情况。

fragment之间可以在Activity的作用域内共享同个ViewModel对象来处理这个麻烦的数据交互问题。例如:

class SharedViewModel : ViewModel() {
    val selected = MutableLiveData<Item>()

    fun select(item: Item) {
        selected.value = item
    }
}

class MasterFragment : Fragment() {

    private lateinit var itemSelector: Selector

    private lateinit var model: SharedViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        model = activity?.run {
            ViewModelProviders.of(this).get(SharedViewModel::class.java)
        } ?: throw Exception("Invalid Activity")
        itemSelector.setOnClickListener { item ->
            // Update the UI
        }
    }
}

class DetailFragment : Fragment() {

    private lateinit var model: SharedViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        model = activity?.run {
            ViewModelProviders.of(this).get(SharedViewModel::class.java)
        } ?: throw Exception("Invalid Activity")
        model.selected.observe(this, Observer<Item> { item ->
            // Update the UI
        })
    }
}

由于两个Fragment对象绑定在同一个Activity对象,它们通过ViewModelProvider获取在Activity范围内的同一个ViewModel对象。

这样做的好处是:

  • Activity对象不会感知两个Fragment对象的交互,所以也不用做任何事。
  • Fragment对象不需要知道另外一个Fragment对象,除了和ViewModel的约定。当一个对象消失,另外一个对象还能正常使用。
  • Fragment对象的生命周期不受其他的对象影响,一个Fragment对象替换另一个,UI还能正常运行。

4. ViewModel替代Loader

加载器类像CursorLoader,经常用来保持UI中的数据与数据库的同步。可以通过ViewModel,和其他的类,替换Loader类。使用ViewModel分离UI控制器的数据加载动作,意味着可以减少更多的类强引用。
使用Loader一个常见的方法,就是在应用程序中使用一个CursorLoader观察的数据库内容,当数据库的值发生变化时,Loader自动重新加载数据和更新用户界面:


使用ViewModel,并和Room或LiveData一起代替Loader。ViewModel对象在配置发生变化是保持数据,而Room通知LiveData数据库数据改变,LiveData则将修改后的数据更新到UI上。

其他更多信息可阅读官网

总结

Jetpack讲到这里,基本都明白Jetpack是干嘛了,Jetpack总结我们平常开发的各种效率和架构,为我们提供更标准的组件。Room操作数据库,LiveData通知数据更改,DataBinding更新View,Lifecycle管理周期,及其后面要讲的其他组件。都在给我们应用层开发定义一套统一开发架构标准,以便可以开发更好APP。通过这么一套架构组件,可以快速开发可维护性高,扩展性好的APP。

Welcom to visit my github

Android Jetpack LiveData

又到周末好时光,茫茫人海中,与你在掘金相遇,好幸运~请君珍惜缘分,赏阅本文。相处不易,开门见山,不扯皮。本文讲的是Jetpack系列第三个架构组件LiveData,LiveData是Lifecycle-aware 组件的一个应用,这意味着LiveData遵守Activity、Fragment和Service等组件的生命周期,在它们生命周期处于活跃状态(CREATEDRESUMED)才进行更新Views。

使用LiveData步骤

  1. 创建持有某种类型的LiveData对象。通常在ViewModel类来实现该对象。
  2. 定义一个具有onChanged()方法的Observer对象,当LiveData持有数据变化是回调该方法。通常在UI控制器类中实现创建该Observer对象,如Activity或Fragment。
  3. 通过使用observe()方法,将上述的LiveData对象和Observer对象关联在一起。这样Observer对象就与LiveData产生了订阅关系,当LiveData数据发生变化时通知,而在Observer更新数据,所以Observer通常是Activity和Fragment。

三个步骤就定义了使用LiveData的方式,从步骤可以看出,使用了观察者模式,当LiveData对象持有数据发生变化,会通知对它订阅的所有处于活跃状态的订阅者。而这些订阅者通常是UI控制器,如Activity或Fragment,以能在被通知时,自动去更新Views。

创建LiveData对象

LiveData可以包装任何数据,包括集合对象。LiveData通常存储在ViewModel中,并通过getter方法获得。示例:

class NameViewModel : ViewModel() {

    // Create a LiveData with a String
    val currentName: MutableLiveData<String> by lazy {
        MutableLiveData<String>()
    }

    // Rest of the ViewModel...
}

为什么是ViewModel持有LiveData而不是Activity或者Fragment中呢?

  • 这样导致Activity或Fragment代码臃肿,Activity或Fragment一般用来展示数据而不是持有数据。
  • 将LiveData解耦,不和特定的Activity或Fragment绑定在一起。

创建 观察LiveData 的对象

有了数据源之后,总需要有观察者来观察数据源,不然数据源就失去了存在的意义。

那么在哪里观察数据源呢?

在大多数情况下,在应用组件的onCreate()方法中访问LiveData是个合适的时机。这样可以确保系统不在Activity或Fragment的onResume()方法进行多余的调用;另外这样也确保Activity和Fragment尽早有数据可以进行显示。

class NameActivity : AppCompatActivity() {

    private lateinit var model: NameViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Other code to setup the activity...

        // Get the ViewModel.
        model = ViewModelProviders.of(this).get(NameViewModel::class.java)


        // Create the observer which updates the UI.
        val nameObserver = Observer<String> { newName ->
            // Update the UI, in this case, a TextView. 
            nameTextView.text = newName
        }

        // Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
        model.currentName.observe(this, nameObserver)
    }
}

在讲nameObserver对象传给observe()方法后,存储在LiveData最近的值以参数的形式立即传递到onChange()方法中。当然,如果此时LiveData没有存储值的话,onChange()方法不会被调用。

更新 LiveData 对象

LiveData本身没有提供公共方法更新值。如果需要修改LiveData的数据的话,可以通过MutableLiveData来暴露共有方法setValue()postValue()。通常在在ViewModel中使用MutableLiveData,而MutableLiveData暴露不可变的LiveData给Observer。与Observer建立关系后,通过修改LiveData的值从而更新Observer中的视图。

button.setOnClickListener {
    val anotherName = "GitCode"
    model.currentName.setValue(anotherName)
}

当单击button时,字符串GitCode会存储到LiveData中,nameTextView的文本也会更新为GitCode。这里通过button的点击来给LiveData设置值,也可以网络或者本地数据库获取数据方式来设置值。

扩展 LiveData

可以通过下面的栗子来看看如何扩展LiveData。

class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
    private val stockManager = StockManager(symbol)

    private val listener = { price: BigDecimal ->
        value = price
    }

    override fun onActive() {
        stockManager.requestPriceUpdates(listener)
    }

    override fun onInactive() {
        stockManager.removeUpdates(listener)
    }
}

首先建立一个StockLiveData并继承自LiveData,并重写两个重要方法。

  • onActivite() 当有活跃状态的订阅者订阅LiveData时会回调该方法。意味着需要在这里监听数据的变化。
  • onInactive() 当没有活跃状态的订阅者订阅LiveData时会回调该方法。此时没有必要保持StockManage服务象的连接。
  • setValue() 注意到value=price这里是调用了setValue(price)方法,通过该方法更新LiveData的值,进而通知处于活跃状态的订阅者。

LiveData会认为订阅者的生命周期处于STARTEDRESUMED状态时,该订阅者是活跃的。

那么如何使用StockLiveData呢?

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    val myPriceListener: LiveData<BigDecimal> = ...
    myPriceListener.observe(this, Observer<BigDecimal> { price: BigDecimal? ->
        // Update the UI.
    })
}

以Fragment作LifecycleOwner的实例传递到observer()方法中,这样就将Observer绑定到拥有生命周期的拥有者。由于LiveData可以在多个Activity、Fragment和Service中使用,所以可以创建单例模式。

class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
    private val stockManager: StockManager = StockManager(symbol)

    private val listener = { price: BigDecimal ->
        value = price
    }

    override fun onActive() {
        stockManager.requestPriceUpdates(listener)
    }

    override fun onInactive() {
        stockManager.removeUpdates(listener)
    }

    companion object {
        private lateinit var sInstance: StockLiveData

        @MainThread
        fun get(symbol: String): StockLiveData {
            sInstance = if (::sInstance.isInitialized) sInstance else StockLiveData(symbol)
            return sInstance
        }
    }
}

那么在Fragment可以这样使用:

class MyFragment : Fragment() {

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        StockLiveData.get(symbol).observe(this, Observer<BigDecimal> { price: BigDecimal? ->
            // Update the UI.
        })

    }

转换 LiveData

有时候在把数据分发给Observer前,转换存储在LiveData中的值,或者返回一个 基于已有值的LiveData对象 的另外一个LiveData对象。这时候就需要用到 Transformations类来处理了。

使用Transformations.map()方法可以改变其下游的结果:

LiveData<User> userLiveData = ...;
LiveData<String> userName = Transformations.map(userLiveData, user -> {
    user.name + " " + user.lastName
});

使用Transformations.switchMap()同样可以改变下游的结果,但传递给switchMap()的函数必须返回一个LiveData对象。

private fun getUser(id: String): LiveData<User> {
  ...
}
val userId: LiveData<String> = ...
val user = Transformations.switchMap(userId) { id -> getUser(id) }

这种转换方式都惰性的,也就是只有Observer来订阅数据的时候,才会进行转换。当在ViewModel需要一个Lifecycle对象,或许这种转换会是很好的解决方案。

合并多个LiveData 源

MediatorLiveData是LiveData的子类,它主要用途是用来合并多个LiveData源。当其中一个源数据发生变化是,都会回调订阅MediatorLiveData的观察者的onChanged()方法。例如我们在实际开发中,我们的数据源要么来自服务器,要么来自本地数据库。这里就考虑使用MediatorLiveData。

总结

LiveData的入门使用来说还是相对简单的,等到后面讲完Jetpack系列文章,再以一个综合的Demo示例Jetpack涉及到的一些知识点。光看文档,都可以感觉到Android 对设计模式,和MVP模式、MVVM模式的推荐情况。所以建议各位同学在代码方面的编写一定要有大局观念,代码规范的还是要有,方便别人就是方便自己。不要为应付功能实现而代码臃肿,后续又不重新架构,一直积累成垃圾码。

如果翻译有误或者个人理解不正确,还望多加指导。另外希望各位同学在实战碰到的问题可以反馈一下,到后面总结可以一起列为问题点。

官网总是原滋原味的

坚持初心,写优质好文章

开文有益,Star支持好文

UDP和TCP协议

前言

如果大学时好好读书,对本文的知识点应该会很熟悉,大部分知识和图片来源于《计算机网络》这本书。最近老是想到这本书,就从网上找了资源来学习,并和大家分享。该书的PDF在GitHub,感兴趣可以下载和Star(条件允许还是买书吧),或者利用零散时间学习,就看我的文章吧。

Star支持吧

传输层

本文主要讲的是传输中的两大重要协议TCP和UDP,虽然在Android开发中,并不需要了解到这么底层,但有理论的支撑,写代码总是很自信的啦。理论指导着实践,实践是理论检验的唯一标准。站在巨人的肩膀窥伺网络世界。

UDP

用户数据报协议UDP只在IP的数据报服务至上增加了复用和分用的功能以及差错检测的功能。只有面向无连接的报文,不可靠传输的特点。UDP对应用层交下来的数据只添加首部,并进行特别的处理,就交给网络层;对网络层传递上来的用户数据报拆封首部后,原封不动的交给应用层。

UDP的首部格式

用户数据报UDP分为两个字段:数据字段和首部字段,从图来分析用户数据报UDP的首部格式。

UDP首部字段很简单,由4个字段组成,每个字段的长度都是两个字节,共8字节。

  • 源端口 原端口号,在需要对方回信时选用,不需要时可全0
  • 目的端口 目的端口号,这在终点交付报文时必须使用,不然数据交给谁呢?
  • 长度 UDP的长度,最小值为8字节,仅有首部
  • 检验和 检测用户数据报在传输过程是否有错,有错就丢弃。

在传输的过程中,如果接收方UDP发现收到的报文中的目的端口不存在,会直接丢弃,然后由网际控制报文协议ICMP给发送方发送“端口不可达”差错报文。

伪首部

计算校验和时,需要在UDP之前增加12个字节的伪首部。这种首部并不是用户数据报的真正首部。伪首部并不在网络中传输,只是在计算检验和,临时添加在UDP用户数据报前,得到一个临时的用户数据报。

UDP的校验和是把首部和数据部分一起校验,发送方计算校验和的一般步骤:

  1. 将首部的校验和字段填充为0(零)
  2. 把伪首部和用户数据报UDP看出16位的字符串连接起来
  3. 如果数据部分不是偶数字节,则填充一个全零字节(该字节不发送到网络层)
  4. 按二进制反马计算出这些16位字的和
  5. 然后将和写入校验和字段,就可以发送到网络层了。

接收方收到用户数据报后,连同伪首部一起,按二进制反码求这些16位字的和,无差错结果是应全为1.否则出错,直接丢弃该报文。

TCP协议

TCP协议作为传输层主要协议之一,具有面向连接,端到端,可靠的全双工通信,面向字节流的数据传输协议。

TCP报文段

虽然TCP面试字节流,但TCP传输的数据单元却是报文段。TCP报文段分为TCP首部和数据部分,TCP报文段首部的前20个字节是固定的,后面有4n字节是更具需要而增加的选项,最大为40字节。

  • 源端口和目的端口 各占两个字节,TCP的分用功能也是通过端口实现的。
  • 序号 占4个字节,范围是[0,232],TCP是面向字节流的,每个字节都是按顺序编号。例如一个报文段,序号字段是201,携带数据长度是100,那么第一个数据的序号就是201,最后一个就是300。当达到最大范围,又从0开始。
  • 确认号 占4个字节,是期望收到对方下一个报文段的第一个字节的序号。若确认号=N,则表示序号N前所有的数据已经正确收到了。
  • 数据偏移 占4位,表示报文段的数据部分的起始位置,距离报文段的起始位置的距离。间接的指出首部的长度。
  • 保留 占6位,保留使用,目前为0.
  • URG(紧急) 当URG=1,表明紧急指针字段有效,该报文段有紧急数据,应尽快发送。
  • ACK(确认) 仅当ACK=1时,确认号才有效,连接建立后,所有的报文段ACK都为1。
  • PSH(推送) 接收方接收到PSH=1的报文段,会尽快交付接收应用经常,不再等待整个缓存填满再交付。实际较少使用。
  • RST(复位) RST=1时,表明TCP连接中出现严重差错,必须是否连接,再重连。
  • SYN(同步) 在建立连接时用来同步序号。当SYN=1,ACK=0,则表明是一个连接请求报文段。SYN=1,ACK=1则表示对方同意连接。TCP建立连接用到。
  • FIN(终止) 用来释放一个连接窗口。当FIN=1时,表明此报文段的发送方不再发送数据,请求释放单向连接。TCP断开连接用到。
  • 窗口 占2个字节,表示自己的发送方自己的接收窗口,窗口值用来告诉对方允许发送的数据量。
  • 校验和 占2字节,检验和字段查验范围包括首部和数据部分。
  • 紧急指正 占2字节,URG=1时,紧急指针指出本报文段中的紧急数据的字节数(紧急字节数结束后为普通字节)。
  • 选项 长度可变,最长可达40字节。例如最大报文段长度MSS。MSS指的是数据部分的长度而不是整个TCP报文段长度,MSS默认为536字节长。窗口扩大,时间戳选项等。

TCP建立连接-三次握手

三次握手图例如下,与文字解释配合使用效果更佳。


第一次:客户端发送连接请求报文给服务端,其中SYN=1,seq=x。发送完毕后进入YSN_END状态。

第二次:服务端接收到报文后,发回确认报文,其中ACK=1,ack=x+1,因为需要客户端确认,所以报文中也有SYN=1,seq=y的信息。发送完后进入SYN_RCVD状态。

第三次:客户端接收到报文后,发送确认报文,其中ACK=1,ack=y+1。发送完客户端进入ESTABLISHED状态,服务端接收到报文后,进入ESTABLISHED状态。到此,连接建立完成。

三次握手原因

避免资源被浪费掉。如果在第二步握手时,由于网络延迟导致确认包不能及时到达客户端,那么客户端会认为第一次握手失败,再次发送连接请求,服务端收到后再次发送确认包。在这种情况下,服务端已经创建了两次连接,等待两个客户端发送数据,而实际却只有一个客户端发送数据。

TCP断开连接-四次挥手

四次挥手指客户端和服务端各发送一次请求终止连接的报文,同时双方响应彼此的请求。
四次挥手图例如下,请配置文字解释使用哦。
四次挥手图例
第一次挥手:客户端发送FIN=1,seq=x的包给服务端,表示自己没有数据要进行传输,单面连接传输要关闭。发送完后,客户端进入FIN_WAIT_1状态。

第二次挥手:服务端收到请求包后,发回ACK=1,ack=x+1的确认包,表示确认断开连接。服务端进入CLOSE_WAIT状态。客户端收到该包后,进入FIN_WAIT_2状态。此时客户端到服务端的数据连接已断开。

第三次挥手:服务端发送FIN=1,seq=y的包给客户端,表示自己没有数据要给客户端了。发送完后进入LAST_ACK状态,等待客户端的确认包。

第四次挥手:客户端收到请求包后,发送ACK=1,ack=y+1的确认包给服务端,并进入TIME_WAIT状态,有可能要重传确认包。服务端收到确认包后,进入CLOSED状态,服务端到客户端的连接已断开。客户端等到一段时间后也会进入CLOSED状态。

四次挥手原因
由于TCP的连接是全双工,双方都可以主动传输数据,一方的断开需要告知对方,让对方可以相关操作,负责任的表现。

使用TCP协议有:FTP(文件传输协议)、Telnet(远程登录协议)、SMTP(简单邮件传输协议)、POP3(和SMTP相对,用于接收邮件)、HTTP协议等

TCP流量控制

滑动窗口协议

TCP滑动窗口协议主要为了解决数据在网络传输的过程中,发送方和接收方速率不一致的问题,从而保证数据传输的可靠性,达到流量控制的效果。
发送方中的数据分为三种:

  • 发送已确认
  • 发送未确认
  • 未发送

接收方数据分为三种:

  • 已接收和确认但未被上层读取
  • 接收未确认

在发送方的滑动窗口中,可分为发送窗口和可用窗口。发送窗口中的数据已发送接收方,但未接到接收方的确认;可用窗口则表示发送方还可以发送多少数据。发送方的窗口大小会受到接收方窗口的改变而改变。


利用滑动窗口机制能有效的控制发送方的发送数据速率。下面是个栗子:

TCP的窗口单位是字节,不是报文端,所以上文假设一个报文包含100个字节。ACK是确认位,ack是确认号,seq是序列号,对应报文的数据格式的。A和B在TCP三次握手时候,B会告诉A自己的接收窗口rwnd大小。上图中,A向B先发送了三次数据,但第三次丢失了,同时受到B的流量控制,当前ack=201,还需允许继续发送序号为201到500共300个字节。当A发送到序号为500时,就不能发新的数据了,但能接收第三次丢失的数据。

接收方数据被上层读取后,又可以接收序号为501-600共100个字节,所以通知A,接收窗口大小为100,序号为501开头....在上图的整个过程中,A共收到B三次流量控制。

TCP报文段发送的机制

应用层把数据传递给传输层的TCP的发送缓存后,TCP通过不同的机制来控制报文段的发送时机。
主要有下面三种机制:

  • TCP维护一个变量,等于最大报文段长度MSS,缓存中存放的数据达到MSS字节时,则以一个报文段发送出去。
  • 发送应用层指明要求的报文段,即TCP支持的推送操作。
  • 发送法计时器期限到了,就要把前面缓存的数据以报文段发送出去,前提是长度不能超过MSS。

TCP传输效率问题

不同的发送机制都会带来一定的效率问题,例如用户发送一个字符,加上20字节首部,得到21字节长的TCP报文段,再加上21字节的IP首部,就变成41字节长的IP数据报。发送一个字节,线路上就需要发送41字节长的IP数据报,若等待接收方确认,线程就又多了40字节长的数据报。所以在线程带宽不富裕时,这种传输效率非常不高。因此应当推迟发回确认报文,并尽量使用捎带确认的方法。

Negle算法

Negle算法主要为了解决TCP的传输效率问题。Negle算法规定:若要把发送的数据逐个字节缓存起来,则发送方需要把第一个字节发送出去,然后缓存后面的字节,在收到接收方第一个字节的确认,再将现有缓存中所有字节组成一个报文段发送出去,继续缓存后续数据。只有在收到前一个报文的确认之后发送后面的数据。这是为了减少所用带宽。当发送数据到达TCP发送窗口的一半或已达到报文段的最大长度也会立即发送报文段,而不是等待接收方确认。这是为了提高网络吞吐量。

糊涂窗口综合征

TCP接收方的缓存已满,若上层一次从缓存中读取一个字节,这样接收方就可以继续接纳一个字节的窗口,然后向发送方发送确认,把窗口设为1个字节(上文所讲,IP数据报为41字节长)。如果这样持续下去,那么网络效率非常低。

所以有效的解决方法,就是让接收方等待一定时间,让缓存空间能够接纳一个最长的报文段,或者等待接收缓存已有一半的空闲空间,再发出确认报文和通知当前窗口大小。

TCP的拥塞控制

拥塞

什么是拥塞呢,在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络性能就会变坏了,这种情况就叫拥塞。网络资源常指网络链路容量(带宽)、交换结点中的缓存和交换处理机。

当出现拥塞,条件允许一般都是通过添加网络资源,例如带宽换成更大的,但这治标不治本,而且不一定总是有用。网络拥塞往往是有许多因素引起的,因此就需要拥塞控制了。

拥塞控制

拥塞控制指防止过多的的数据注入到网络中,这样可以使网络中的路由器或链路不过载。拥塞机制是一个全局性的过程,涉及到所有主机、所有路由器,以及与降低网络传输性能有光的所有因素。

而滑动窗口协议的流量控制,是指点到点的通信量控制,是端到端的问题。

TCP的拥塞控制方法

TCP进行拥塞控制的算法有四种:慢开始、拥塞避免、快重传、快恢复

拥塞控制是基于拥塞窗口的,发送方维持一个拥塞窗口 cwnd的状态变量。窗口大小取决网络的拥塞程度,并且动态变化,发送方会让自己的发送窗口等于拥塞窗口。判断网络拥塞的依据就是发送方接收接收方的确认报文是否超时

1、慢开始算法

慢开始指主机由小到大逐渐增大发送窗口,即增大拥塞窗口的数值。初始拥塞窗口cwnd设置为不超过2到4个最大报文段SMSS的数值,具体规定:

  • 若SMSS≤1095字节,cwnd=4 x SMSS字节,不得超过4个报文段。
  • 若SMSS>1095且≤2190字节,cwnd=3 X SMSS字节,不得超过3个报文段。
  • 若SMSS>2190字节,则 cwnd=2 x SMSS字节,不得超过2个报文段。

从上面的规定限制了初始拥塞窗口的大小。

慢开始在每收到一个对新的报文段的确认后,cwnd就可以增加最多一个SMSS的数值。


N是刚收到确认的报文段所确认的字节数,当N<SMSS时,拥塞窗口每次的增加量要小于SMSS。下文举例说明慢开始的原理(实际上,TCP的窗口是以字节大小为单位,下文为了方便以报文端形容):


从图可知,初始化窗口未1,所有发送M1报文段,收到确认号之后,发送M2-M3两个报文段,因为拥塞窗口增大了,后面的轮次也是这样翻倍增加的。随着轮次的增多,那么发送到网络的数据就会急剧增加,容易出现拥塞,因此需要慢开始门限(ssthresh)状态变量。

  • 当cwnd<ssthresh时,使用慢开始算法
  • 当cwnd>ssthresh时,使用拥塞避免算法
  • 当cwnd=ssthresh时,忙开始或者拥塞避免算法
2、拥塞避免算法

拥塞避免算法就是让cwnd缓慢增大,每一个轮次把拥cwnd增加1,而不是像慢开始算法那样翻倍增加。需要注意的是,拥塞避免算法只是让网络不那么快出现拥塞,而不是避免拥塞出现。

上文已经说到,判断网络是否拥塞以报文是否超时为准,当网络出现拥塞时,会把ssthresh设为原有的一半,然后开始慢开始算法。如下图所示:


在上黑色园圈4点的时候,发送方收到对同一个报文端重复确认(3-ACK)。这种情况,个别报文端会在网络中丢失,但实际上未发生网络拥塞,发送方未及时收到确认,就会产生超时,误认为出现拥塞,发送方会重新开始慢开始算法。减低了传输效率。因此,就需要快重传算法了。

3、 快重传算法

快重传算法是让发送方今早知道发生了个别报文段的丢失。快重传算法要求接收方不要等待自己发送数据时才进行捎带确认,而是立即发送确认。也就是说,但出现丢包情况,接收方在接收新数据时会重复发送对丢失包的前一个报文段的确认号。发送方接收到三次确认号后,就判断该丢失报文段确实丢失,会立即进行重传(快重传)。

4、快恢复算法

在上文,知道只是报文段丢失,而不是网络出现拥塞后,发送方会调整ssthresh为原来的一半,然后继续进行拥塞避免算法,这个过程就叫快开恢复算法。

5、小结

可见,TCP拥塞控制四个算法是相辅相成,少了谁都不行,共同维护这拥塞控制机制。下面是总体的流程图。


从流量控制和拥塞控制整体看,涉及到三个窗口,接收窗口、发送窗口、拥塞窗口。发送窗口的数值是不能大于接收窗口的,但是拥塞窗口由网络的拥塞程度决定的(所以上文的发送窗口等于拥塞窗口,是假设在接收窗口数值足够大,能够容纳拥塞窗口的数据)。因此,发送窗口的上限值应该是拥塞窗口cwnd和接收窗口rwnd之间的最小值。也就是说,通过流量控制和拥塞控制,发送的发送速率取决于cwnd和rwnd中数值较小的一个。

Android Studio Lint & Custom Issue

前言

以前对下面的问题,我的态度是,不报错就是没问题,报错就用快捷键,根据Android Studio提示修复问题,从来不去问个为什么?现在代码洁癖症越来越严重的我,忍不住想看清什么东西在搞鬼。

认真看完本文,一定可以学到最新的知识。就算看不下去,也要点个赞收藏,绝对不亏。本文并不是吐槽Lint的不好,而是在学习Lint过程碰到问题,心态是奔溃的,以及解决每个问题带来的喜感。

不知道大家有没有注意项目中黄色代码块的提示,如下图所示:

或者红色标记的代码(并没有任何错误),如下图所示:


上文黄色的提醒和红色警告,都是来自Android Studio内置的Lint工具检查我们的代码后而作出的动作。
通过配置Lint,也可以消除上面的提醒。例如,我开发系统APK,根本不需要考虑用户是否授权。
那么Lint是什么呢?

Lint

Android Studio 提供一个名为Lint的静态代码扫描工具,可以发现并纠正代码结构中的质量问题,而无需实际执行该应用,也不必编写测试用例。
Lint 工具可检查您的 Android 项目源文件是否包含潜在错误,以及在正确性、安全性、性能、易用性、便利性和国际化方面是否需要优化改进。

也就是说,通过Lint工具,我们可以写出更高质量的代码和代码潜在的问题,妈妈再也不用担心我的同事用中文命名了。也可以通过定制Lint相关配置,提高开发效率。

Lint禁止检查

由于Android Studio内置了Lint工具,好像不需要我们干嘛。可是呀,我有强迫症,看着上面的黄色块,超级不爽的。所以我们得了解如何配置Lint,让它为我们服务,而不是为Google服务。

本文开始的红色错误可以通过注解来消除(一般建议是根据提示进行修正,除非明白自己在做什么),可以在类或该代码所在的方法添加@SuppressLint


上图中是禁止Lint检查特定的问题检查,如果要禁止该Java文件所有的Lint问题,可以在类前添加如下注解:@SuppressLint(all)
对XMl文件的禁止,则可以采用如下形式:

  1. 在lint.xml声明命名空间
namespace xmlns:tools="http://schemas.android.com/tools"
  1. 在布局中使用:
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:ignore="UnusedResources" >

    <TextView
        android:text="@string/auto_update_prompt" />
</LinearLayout>

父容器声明了ignore属性,那么子视图会继承该属性。例如上文LinearLayout中声明了禁止Lint检查LinearLayout的UnusedResources问题,TextView自然也禁止检查该问题。禁止检查多个问题,问题之间用逗号隔开;禁止检查所有问题则使用all关键字。

tools:ignore="all"

我们也可以通过配置项目的Gradle文件来禁止检查。

例如禁止Lint检查项目AndroidManifest.xml文件的GoogleAppIndexingWarning问题。在项目对应组件工程的Gradle文件添加如下配置,这样就不会有黄色提醒了。

defaultConfig{
    lintOptions {
        disable 'GoogleAppIndexingWarning'
    }
}

那么,可以禁止lint工具检查什么问题?

配置Lint

在上文中通过注解和在xml使用属性来禁止Lint工具检查相关问题,其实已经是对Lint的配置了。Lint将多个问题归为一个issue(规则),例如下图右边的的六大规则。


上图是Lint工具的工作流程,下面了解相关概念。
App Source Files
源文件包含组成 Android 项目的文件,包括 Java 和 XML 文件、图标和 ProGuard 配置文件等。
lint.xml 文件
此配置文件可用于指定您希望排除的任何 Lint 检查以及自定义问题严重级别。
lint Tool
我们可以通过Android Studio 对 Android 项目运行此静态代码扫描工具。也可以手动运行。Lint 工具检查可能影响 Android 应用质量和性能的代码结构问题。
Lint 检查结果
我们可以在控制台(命令行运行)或 Android Studio 的 Inspection Results 窗口中查看 Lint 检查结果。

通过Lint工具的工作流程了解到,可以在lint.xml文件配置一些信息。一般新建项目都是没有lint.xml文件的,在项目的根目录创建lint.xml文件。格式如下:

<?xml version="1.0" encoding="UTF-8"?>
<lint>
        <!-- list of issues to configure -->
</lint>

那么有哪些Issues(规则)呢?

在Android主要有如下六大类:

  • Security 安全性。在AndroidManifest.xml中没有配置相关权限等。
  • Usability 易用性。重复图标;上文开始黄色警告也属于该规则等。
  • Performance 性能。内存泄漏,xml结构冗余等。
  • Correctness 正确性。超版本调用API,设置不正确的属性值等。
  • Accessibility 无障碍。单词拼写错误等。
  • Internationalization国际化。字符串缺少翻译等。

其他更多Issues,可以通将命令行切换到../Android/sdk/tools/bin目录下,然后输入lint --list。例如在Mac下:
cd /Users/gitcode8/Library/Android/sdk/tools/bin输入./lint --list
结果如下:

例如官网提供的参考例子:

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <!-- 忽略整个工程目录下指定问题的检查 -->
    <issue id="IconMissingDensityFolder" severity="ignore" />

    <!-- 忽略对指定文件指定问题的检查 -->
    <issue id="ObsoleteLayoutParam">
        <ignore path="res/layout/activation.xml" />
        <ignore path="res/layout-xlarge/activation.xml" />
    </issue>

    <!-- 更改检查问题归属的严重性  -->
    <issue id="HardcodedText" severity="error" />
</lint>

学习Lint工具仅仅是为了安抚我的强迫症?不,还不知道Lint真正用来干嘛呢?

检查项目质量

不好容易开发了个APP,准备开始上班摸鱼了。还让代码自查?那就通过Lint来看看代码质量如何吧。

  1. 通过Android Studio 的菜单栏Analyze选项下拉选择第一个选项Inspect Code.


2、在弹出框根据自己需要选择lint工具的检查范围,这里选择整个项目。检查时间也是根据项目大小来定的。


3、等待一段时间后,会列出检查结果。从下图看到,不仅会检查Android存在的问题,也会检查Java等其他问题。通过单击问题,可以从右边提示框看到问题发生的地方和相关建议。


到这里,就开始对项目修修补补吧。

自定义规则

为什么要自定义呢?已有规则不符合自己或团队开发需求,或者觉得Lint存在一些缺陷。在网上大多数文章千篇一律,都是通过将Log打印来举例,看着都好累哦。由于没有相关官方文档和第三方教程(可能由于lint的api更新太快,没人愿意做这种吃力不讨好的工作),也这就只有这样了。本文通过自定义命名规范规则来讲解整个过程。

Lint中重点的API

先学习相关api,可以快速理解一些概念,可以粗略看过,下结实践再回来看。

1、Issue

Issue如上文所说,表示lint 工具检查的一个规则,一个规则包含若干问题。常在Detector中创建。下文是创建一个Issue的例子。

   private  static final Issue ISSUE = Issue.create("NamingConventionWarning",
            "命名规范错误",
            "使用驼峰命名法,方法命名开头小写,类大写字母开头",
            Category.USABILITY,
            5,
            Severity.WARNING,
            new Implementation(NamingConventionDetecor.class,
                    EnumSet.of(Scope.JAVA_FILE)));
  • 第一个参数id 唯一的id,简要表面当前提示的问题。
  • 第二个参数briefDescription 简单描述当前问题
  • 第三个参数explanation 详细解释当前问题和修复建议
  • 第四个参数category 问题类别,例如上文讲到的Security、Usability等等。
  • 第五个参数priority 优先级,从1到10,10最重要
  • 第六个参数Severity 严重程度:FATAL(奔溃), ERROR(错误), WARNING(警告),INFORMATIONAL(信息性),IGNORE(可忽略)
  • 第七个参数Implementation Issue和哪个Detector绑定,以及声明检查的范围。Scope有如下选择范围:
    RESOURCE_FILE(资源文件),BINARY_RESOURCE_FILE(二进制资源文件),RESOURCE_FOLDER(资源文件夹),ALL_RESOURCE_FILES(所有资源文件),JAVA_FILE(Java文件), ALL_JAVA_FILES(所有Java文件),CLASS_FILE(class文件), ALL_CLASS_FILES(所有class文件),MANIFEST(配置清单文件), PROGUARD_FILE(混淆文件),JAVA_LIBRARIES(Java库), GRADLE_FILE(Gradle文件),PROPERTY_FILE(属性文件),TEST_SOURCES(测试资源),OTHER(其他);

这样就能很清楚的定义一个规则,上文只定义了检查命名规范的规则。

2、IssueRegistry

用于注册要检查的Issue(规则),只有注册了Issue,该Issue才能被使用。例如注册上文的命名规范规则。

public class Register extends IssueRegistry {
    @NotNull
    @Override
    public List<Issue> getIssues() {
        return Arrays.asList(NamingConventionDetector.ISSUE);
    }
}

4、Detector

查找指定的Issue,一个Issue对应一个Detector。自定义Lint 规则的过程也就是重写Detector类相关方法的过程。具体看下小结实践。

5、Scanner

扫描并发现代码中的Issue,Detector需要实现Scaner,可以继承一个到多个。

  • UastScanner 扫描Java文件和Kotlin文件
  • ClassScanner 扫描Class文件
  • XmlScanner 扫描Xml文件
  • ResourceFolderScanner 扫描资源文件夹
  • BinaryResourceScanner 扫描二进制资源文件
  • OtherFileScanner 扫描其他文件
  • GradleScanner 扫描Gradle脚本

旧版本的JavaScanner、JavaPsiScanner随着版本的更新已经被UastScanner替代了。

自定义Lint规则实践

通过实现命名规范Issue来熟悉和运用上小节相关的api。自定义规则需要在Java工程中创建,这里通过Android Studio来创建一个Java Library。

步骤:File->New->New Mudle->Java Library

这里Library Name为lib。

定义类NamingConventionDetector,并继承自Detector。因为这里是检测Java文件类名和方法是否符合规则,所以实现Detector.UastScanner接口。

public class NamingConventionDetector 
    extends Detector 
    implements Detector.UastScanner {
}

在NamingConventionDetector类内定义上文的Issue:

public class NamingConventionDetector 
    extends Detector 
    implements Detector.UastScanner {
    
    public static final Issue ISSUE = Issue.create("NamingConventionWarning",
        "命名规范错误",
        "使用驼峰命名法,方法命名开头小写",
        Category.USABILITY,
        5,
        Severity.WARNING,
        new Implementation(NamingConventionDetector.class,
            EnumSet.of(Scope.JAVA_FILE)));
}

重写Detector的createUastHandler方法,实现我们自己的处理类。

public class NamingConventionDetector extends Detector implements Detector.UastScanner {
    //定义命名规范规则
    public static final Issue ISSUE = Issue.create("NamingConventionWarning",
            "命名规范错误",
            "使用驼峰命名法,方法命名开头小写",
            Category.USABILITY,
            5,
            Severity.WARNING,
            new Implementation(NamingConventionDetector.class,
                    EnumSet.of(Scope.JAVA_FILE)));

    //返回我们所有感兴趣的类,即返回的类都被会检查
    @Nullable
    @Override
    public List<Class<? extends UElement>> getApplicableUastTypes() {
        return Collections.<Class<? extends UElement>>singletonList(UClass.class);
    }

    //重写该方法,创建自己的处理器
    @Nullable
    @Override
    public UElementHandler createUastHandler(@NotNull final JavaContext context) {
        return new UElementHandler() {
            @Override
            public void visitClass(@NotNull UClass node) {
                node.accept(new NamingConventionVisitor(context, node));
            }
        };
    }
    //定义一个继承自AbstractUastVisitor的访问器,用来处理感兴趣的问题
    public static class NamingConventionVisitor extends AbstractUastVisitor {

        JavaContext context;

        UClass uClass;

        public NamingConventionVisitor(JavaContext context, UClass uClass) {
            this.context = context;
            this.uClass = uClass;
        }

        @Override
        public boolean visitClass(@org.jetbrains.annotations.NotNull UClass node) {
            //获取当前类名
            char beginChar = node.getName().charAt(0);
            int code = beginChar;
            //如果类名不是大写字母,则触碰Issue,lint工具提示问题
            if (97 < code && code < 122) {
                context.report(ISSUE,context.getNameLocation(node),
                        "the  name of class must start with uppercase:" + node.getName());
                //返回true表示触碰规则,lint提示该问题;false则不触碰
                return true;
            }

            return super.visitClass(node);
        }

        @Override
        public boolean visitMethod(@NotNull UMethod node) {
            //当前方法不是构造方法
            if (!node.isConstructor()) {
            char beginChar = node.getName().charAt(0);
            int code = beginChar;
                //当前方法首字母是大写字母,则报Issue
                if (65 < code && code < 90) {
                    context.report(ISSUE, context.getLocation(node),
                            "the method must start with lowercase:" + node.getName());
                    //返回true表示触碰规则,lint提示该问题;false则不触碰
                    return true;
                }
            }
            return super.visitMethod(node);

        }

    }
}

上文NamingConventionDetector类,已经是全部代码,只检查类名和方法名是否符合驼峰命名法,可以根据具体需求,重写抽象类AbstractUastVisitor的visitXXX方法。

如果处理特定的方法或者其他,也可以使用默认的处理器。重写Scanner相关方法。例如:

 @Override
public List<String> getApplicableMethodNames() {
    return Arrays.asList("e","v");
}

表示e(),v()方法会被检测到,并调用visitMethod()方法,实现自己的逻辑。

    @Override
    public void visitMethod JavaContext context,  JavaElementVisitor visitor,  PsiMethodCallExpression call, PsiMethod method) {
        //todo something
        super.visitMethod(context, visitor, call, method);
    }

接下来就是注册自定义的Issue:

public class Register extends IssueRegistry {
    @NotNull
    @Override
    public List<Issue> getIssues() {
        return Arrays.asList(NamingConventionDetector.ISSUE);
    }
}

在lib项目的build.gradle文件添加相关代码:

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.tools.lint:lint-api:26.4.2'
    implementation 'com.android.tools.lint:lint-checks:26.4.2'

}
//添加如下代码
jar {
    manifest {
        attributes 'Lint-Registry': 'com.gitcode.lib.Register'
    }
}

sourceCompatibility = "7"
targetCompatibility = "7"

到这里就自定义Lint自定义规则就搞定了,接着是使用和确定规则是否正确。

使用自定Lint规则

使用自定义Lint规则有两种形式:jar包和AAR文件。

jar形式使用

在Android Studio的Terminal输入下面命令:

./gradlew lib:assemble

看到BUILD SUCCESSFUL 则表示生成jar包成功,可以在下面路径找到:

lib->build->libs

如图:


将lib.jar拷贝下面目录:

~/.android/lint/

如果lint文件夹不存在,则创建。通过命令行输入lint --list。滑到最后可以看到配置的规则,如图:


重启Android Studio,让规则生效。
检测到方法大写,不符合命名规范,报导该问题。

类名不符合规范:


从上文可以看到,放在目录下的jar包对所有工程都是有效的。如果要针对单个工程,那么就需要需要AAR形式了。

AAR形式

在同个工程新建一个Android Library,名为lintLibrary,修改相关配置。

1、修改Java工程的依赖

修改自定义lint规则的Java库的build.gradle(这里是上文的Java lib库),注意到要将implementation改为compileOnly。

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //将implementation改为compileOnly,不然报错
    compileOnly 'com.android.tools.lint:lint-api:26.4.2'
    compileOnly 'com.android.tools.lint:lint-checks:26.4.2'

}

jar {
    manifest {
        attributes 'Lint-Registry-v2': 'com.gitcode.lib.Register'
    }
}

sourceCompatibility = "7"
targetCompatibility = "7"
2、修改Android Library依赖

Android Library主要用来输出AAR文件,要注意到Android Studio新特性的变更(在这里踩了大坑)。

dependencies {
    ......
    
    lintPublish project(':lib')
}

在Android Studio 3.4+,lintChecks project(':lib'):lint检查只在当前工程生效,也就是Android Library,并不会打包到AAR文件中。 lintPublish project(':lib')才会将lint检查包含AAR文件中。

3、输出AAR文件

此时跟输出普通的AAR文件没什么区别,但为了手把手教会第一个自定义Issue,我写!

步骤:

菜单栏:View->Tool Windows->Gradle

此时Android Studio在右边会打开如下窗口:


根据上图操作,双击assemble,稍等一会,在控制台看BUILD SUCCESSFUL ,则可在下面目录找到AAR文件。

lintLibrary->build->outputs->aar

这一小节的步骤也可以通过命令行执行。

4、使用AAR文件

有本地依赖或者上传远程仓库,这里只介绍本地依赖。将上小结生成的AAR文件拷贝在app的libs文件夹。并配置app组件的build.gradle

repositories {
    flatDir {
        dirs 'libs'
    }
}
dependencies {
    implementation (name:'lintlibrary-release', ext:'aar')
}

到这里,就能使用自定义的lint规则了,效果和上面使用jar包是一致的。如果不生效,重启Android Studio看看。

采坑记

1、Found more than one jar in the 'lintChecks' configuration. Only one file is supported

这是因为在输出AAR文件中,参考其他人的文章。没有将Java Library的依赖改为compileOnly。而且Android Library中使用lintChecks

2、输出AAR文件没有生效

不知道为什么,Linkedin的参考文章没有生效,可能是Android Studio版本的问题。

另外使用lintChecks输出AAR不生效,Android Studio 3.4+新特性变更,采用lintPublish。

总结

花了好长好长的时间写本文,差点就放弃了。因为自己Android Studio看不了lint的源码,只能从网上找,网上又找不到最新的doc。过滤太多雷同文章,差点想哭,一些最新的文章也跟不上相关技术的更新。。。

但是一切都值得,因为能帮助到想学习Android Studio lint工具的同学,一起向往美好的生活。

GitHub

点个赞行不

写此文找到的一些具有参考意义的文章:

Android 官方指导

Linkedin 指导

美团->Android自定义Lint实践

lint-custom-rules

另外:本文没有demo,demo的代码已经贴在文章里了。

经典蓝牙实现一对一聊天

前言

原来蓝牙现在还分经典蓝牙、低功耗蓝牙和双模蓝牙,技术的发展真的超过个人的认知速度,不学习意味退步。本来写着低功耗蓝牙和智能蓝牙音箱的交互,但写到最后,因为蓝牙音箱还没有做好,没办法给本文的结果做个保障,故最后改成蓝牙聊天。蓝牙聊天可能适合在搭飞机和高铁这种没有网络或者网络不好等特殊情况下使用。本文的Demo可以正常使用。

本文总体流程:发现蓝牙->配对蓝牙->连接蓝牙->数据交互

在这个流程,主要是一些细节和异常的处理,如何更好的体现用户体验。

声明权限

在项目的配置文件AndroidManifest.xml加入如下代码即可,让APP具有蓝牙访问权限和发现周边蓝牙权限。

//使用蓝牙需要该权限
<uses-permission android:name="android.permission.BLUETOOTH"/>
//使用扫描和设置需要权限
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
//Android 6.0以上声明一下两个权限之一即可。声明位置权限,不然扫描或者发现蓝牙功能用不了哦
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

Android 6.0以上动态申请权限位置权限,否则默认是禁止的,无法获取到蓝牙列表。

/**
 * Android 6.0 动态申请授权定位信息权限,否则扫描蓝牙列表为空
 */
private void requestPermissions() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {

            if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                    Manifest.permission.ACCESS_COARSE_LOCATION)) {
                Toast.makeText(this, "使用蓝牙需要授权定位信息", Toast.LENGTH_LONG).show();
            }
            //请求权限
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
                    REQUEST_ACCESS_COARSE_LOCATION_PERMISSION);
        }
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    if (requestCode == REQUEST_ACCESS_COARSE_LOCATION_PERMISSION) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            //用户授权
        } else {
            finish();
        }

    }

    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

这里处理授权的用户体验比较不好,授权则继续,不授权则退出。可以根据自己的需求对授权逻辑另外处理。

设备是否支持蓝牙

并不是所有的Android 设备都支持蓝牙,所以在使用之前,检测当前设备是否支持蓝牙。

    private void isSupportBluetooth() {
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();

        if (bluetoothAdapter == null
                || !getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) {
            showNotSupportBluetoothDialog();
            Log.e(TAG, "not support bluetooth");
        } else {
            Log.e(TAG, " support bluetooth");
        }
    }

类BluetoothAdapter代表着当前设备的蓝牙,可以通过BluetoothAdapter获取所有已绑定的蓝牙,扫描蓝牙,自身的名字和地址,通过地址可以获取周边蓝牙设备。可以通过两种方式获得BluetoothAdapter对象。

//方式一
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
//方式二
BluetoothManager manager= (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
mBluetoothAdapter= manager.getAdapter();

开启蓝牙

先检测蓝牙是否打开。没有打开则提示用户打开,或者直接打开。

    //提示用户开启蓝牙,会有弹出框让用户选择是否同意打开
    private void enableBLE() {
        if (!bluetoothAdapter.isEnabled()) {
            Intent intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            startActivityForResult(intent, REQUEST_BLE_OPEN);
        }
    }
    
        @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        if (requestCode == 1) {
            if (resultCode == RESULT_OK) {
                scanBle();
            }
        }
    }
    
    //直接设置代码开启
   private void enableBLE() {
        if (!bluetoothAdapter.isEnabled()) {
            bluetoothAdapter.enable();
        }
    }

设置可被发现

蓝牙打开后,要将自身设置为可以被周边蓝牙搜索到,以便可以进行下一步操作。蓝牙默认可被周边设备在120秒内搜索到,最长设置不过300秒。听说设置0可以永久被搜索哦。

    /**
     * 设置蓝牙可以被其他设备搜索到
     */
    private void beDiscovered() {
        if (mBluetoothAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
            Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
            discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 0);
            startActivity(discoverableIntent);
        }
    }

收集周边蓝牙设备

通过注册广播监听,对发现的蓝牙设备添加到集合中,在listview进行展示。

    private void registerBluetoothReceiver() {
        IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
        filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
        registerReceiver(bluetoothReceiver, filter);
    }
    
    BroadcastReceiver bluetoothReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();

            if (action.equals(BluetoothDevice.ACTION_FOUND)) {
                BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                list.add(device);
                Log.e(TAG, "discovery:" + device.getName());
                bleAdapter.notifyDataSetChanged();
            } else if (action.equals(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)) {
                // Toast.makeText(MainActivity.this, "discovery finished", Toast.LENGTH_LONG).show();
                btnSearch.setText("重新搜索");
                mBluetoothAdapter.cancelDiscovery();
            }
        }
    };

搜索结果如下,可以看到,部分蓝牙设备只有Mac地址,没有名称。这里没有进一步处理,如果需要处理,参考蓝牙名为空处理方案
搜索到周边设备

设备详情

获取蓝牙列表之后,点击对应的蓝牙,进入蓝牙详情。一个BluetoothDeviced对象代表着一个周边蓝牙设备,通过该对象,可以获取该蓝牙设备的名称,绑定状态,Mac地址,uuid等。
未配对蓝牙
未配对蓝牙设备通过 mDevice.createBond();进行配对。

聊天框实现

配对成功后,通过BluetoothDevice的createRfcommSocketToServiceRecord()方法和蓝牙设备连接,连接成功会返回BluetoothSocket对象,进而获得输入输出流,进行数据交互。

mSocket = device.createRfcommSocketToServiceRecord(UUID.fromString(uuid));

 mSocket.connect();
 
 mOutputStream = mSocket.getOutputStream();

mInputStream = mSocket.getInputStream();

连接过程会堵塞线程,请在子线程执行。uuid和服务端的一致且唯一就可以(这里自己定义),通过uuid和服务端绑定BluetoothSocket。我们将输入输出的内容同步到聊天框,就达到了聊天效果。

服务端的BluetoothSocket对象的实现和此类似,具体的话可以看源码哦。

聊天框

总结

本文简单描述蓝牙连接到实现聊天框的流程,源代码可打开GitHub下载运行,有用就star一下。

由于BluetoothSocket的关闭或者读写异常,还有一些未能同步到,各位客官根据自己需要进行处理。另外一些耗时的操作,例如连接蓝牙,没有做进度条反馈,可以根据自己需求进行定制。Demo可以正常食用。

Android Jetpack Data Binding Library

又到周末好时光,开始嗨之前再抽点时间看看本文,能看到最后的都是大佬,收下我的膜拜。本文技术内容讲的是关于Data Binding Library的那点事,有的同学可能解过了,有的娃可能都不知道是什么东东...。为了不落伍,和大家一样优秀,决定写Jetpack方面的文章。与别人不一样的是,会加入自己的理解和栗子,而不是简单的翻译(我英文水平也不行)。如果大家发现有误的地方,希望多加指点,在此谢过。

About Jetpack

一年前有缘看了一下Jetpack,但并没有过多的去关注,最近在看Google IO 2019相关资料,看到了Jetpack的身影,不得不陷入深思,无法自拔。

JetPack的官方说法:

Jetpack 是 Android 软件组件的集合,使您可以更轻松地开发出色的 Android 应用。这些组件可帮助您遵循最佳做法、让您摆脱编写样板代码的工作并简化复杂任务,以便您将精力集中放在所需的代码上。

总结性

  • 加速开发:以组件的形式供我们依赖使用。
  • 消除样板代码:还记得在Activity中一大堆findViewById么?能做的不止这么多。
  • 构建高质量应用:现在化设计、避开bug、向后兼容。

Android Jetpack 组件是库的集合,这些库是为协同工作而构建的,不过也可以单独采用,同时利用 Kotlin 语言功能帮助提高工作效率。可全部使用,也可混合搭配!

以上是对官网的摘录。作为开山之篇,先从架构方向的数据绑定库入门开始,让同学感受它的魅力。

Data Binding Library(数据绑定库)

借助数据绑定库(Data Binding Library),可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。数据绑定库要求在Android 4.0以上,Gradle 1.5.0以上。实践证明Android SDK和Gradle版本越高,对Data Binding的支持越好,越简单,速度越快。

举个栗子,这个栗子不重,两只手指可以举起来:

findViewById<TextView>(R.id.sample_text).apply {
    text = viewModel.userName
}

栗子中通过findViewById找到TextView组件,并将其绑定到 viewModel 变量的 userName 属性。而下面在布局文件中使用数据绑定库将文本直接分配到TextView组件上,这样就无需调用上述任何 Java 代码。

<TextView  android:text="@{viewmodel.userName}" />

竟然这么好用,为啥不了解看看呢?

配置

在我们的项目build.gradle文件下配置如下代码。

android {
    ...
    dataBinding {
        enabled = true
    }
}

如果Gradle插件版本在3.1.0-alpha06以上,可以使用新的Data Binding编译器,有利于加速绑定数据文件的生成。在项目的gradle.properties文件添加如下配置。

android.databinding.enableV2=true

同步一下,没什么问题的话,配置已经成功了~

入门

  • 定义一个数据对象
data class User(var name: String, var age: Int)
  • 布局绑定

我们创建名为activity_main.xml的布局文件,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
   <data>
       <variable name="user" type="com.gitcode.jetpack.User"/>
   </data>

   <LinearLayout
           android:layout_width="match_parent"
           android:layout_height="match_parent">
       //在TextView中使用
       <TextView android:layout_width="match_parent"
                 android:gravity="center"
                 android:text="@{user.name}"
                 android:layout_height="match_parent"/>
   </LinearLayout>
</layout>

布局文件的根元素不再是以往的LinearLayout、RelativeLayout等等,而是layout。在data元素内添加variable,其属性表示声明一个com.gitcode.jetpack.User类型的变量user。如果多个变量的话,可在data元素下添加多个varialbe元素,格式是一致的。

<data>
   <variable name="user" type="com.gitcode.jetpack.User"/>
   <variable name="time" type="com.gitcode.jetpack.Time"/>
</data>

@{}语法中使用表达式将变量赋值给view的属性。例如:这里将user变量的firstName属性赋值给TextView的text属性。

android:text="@{user.firstName}"
  • 绑定数据

此时布局声明的user变量值还是初始值,我们需要为其绑定数据。

默认情况下,会根据目前布局文件名称来生成一个绑定类(binding class),例如当前布局文件名是activity_main,那么生成的类名就是ActivityMainBinding。

绑定类会拥有当前布局声明变量,并声明getter或者setter方法,也就是说ActivityMainBinding类会带有user属性和getUser、setUser方法,变量的默认初始化与Java一致:引用类型为null,int为0,bool为false。

在MainActivity的onCreate()方法中添加如下代码,将数据绑定到布局上。

val binding: ActivityMainBinding 
        = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.user = User("GitCode", 3)

经典代码是这样的:

setContentView(R.layout.activity_main)
val user=User("GitCode",3)
val tvName=findViewById<TextView>(R.id.tvName)
tvName.text = user.name

可有看出,使用数据绑定库会使代码简洁很多,可读性也很高。
运行一下项目,既可以考到效果了~

运行效果

如果是在Fragment、Adapter中使用,那就要换个姿势了。

val listItemBinding = ListItemBinding
            .inflate(layoutInflater, viewGroup, false)
//或者
val listItemBinding = DataBindingUtil
            .inflate(layoutInflater, R.layout.list_item, viewGroup, false)

恭喜,你已经入门了

可以选择继续学习,

看下文

也可以当做了解

点个赞

看看其他文章了~

布局与绑定表达式

在一开始介绍Data Binding Libaray时,就使用了@{}语法,花括号里面的内容称为绑定表达式,绑定表达式其实并不复杂,跟我们正常使用Java和Kotlin语言的表达式没多大区别。那我们可以在表达式中使用什么类型的运算符或者关键字呢?

常用运算符

运算符 符号
算术 加、减、乘、除、求余(+ 、 - 、* 、/、 %)
逻辑 与、或(&&、||)
一元 + 、-、 !、 ~
移位 >>、 >>>、 <<
关系 == 、> 、<、 >= 、<=(使用符号<时,要换成&lt;)

其他常用的

同时也支持字符拼接+,instanceof,分组、属性访问、数组访问、?:、转型、访问调用,基本类型等等等。
也就是说,绑定表达式语言大多数跟宿主代码(Java or Kotlin)的表达式差不多。为什么说是大多数,因为不能使用thissupernewExplicit generic invocation(明确的泛型调用)等。

丢个栗子:

android:text="@{String.valueOf(index + 1)}"
android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}"
android:transitionName='@{"image_" + id}'

再举丢个栗子:

android:text="@{user.displayName ?? user.lastName}"

如果user.displayName 不为null则使用,否则使用user.lastName.在这里也看得出,可以通过表达式访问类的属性。绑定类会自动检查当前变量是否为null,以避免发生空指针异常。栗子:如果user变量为null,那么user.lastName也会是null。

集合

像数组,链表,Maps等常见的集合,都可以采用下标[]访问它们的元素。

<data>
    <import type="android.util.SparseArray"/>
    <import type="java.util.Map"/>
    <import type="java.util.List"/>
    <variable name="list" type="List&lt;String>"/>
    <variable name="sparse" type="SparseArray&lt;String>"/>
    <variable name="map" type="Map&lt;String, String>"/>
    <variable name="index" type="int"/>
    <variable name="key" type="String"/>
</data>
…
android:text="@{list[index]}"
…
android:text="@{sparse[index]}"
…
android:text="@{map[key]}"
//或者
android:text="@{map.key}"

注意在data元素内添加了import元素,表示导入该类型的定义,这样表达式中引用属性可读性高点,使用也方便。

来个容易掰的栗子:

<data>
    <import type="android.view.View"/>
</data>

<TextView
   android:text="@{user.lastName}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

通过导入View类型,就可以使用相关属性,例如这里的View.VISIBLE

有时导入的类全名太长了或者存在相同类型的类名,我们就可以给它取个别名,然后就可用别名进行coding~

<import type="android.view.View"/>

<import type="com.gitcode.jetpack.View"
        alias="JView"/>

使用资源

使用下面语法:

android:padding="@{@dimen/largePadding}"

相关资源的的表达式引用,贴张官网截图:

事件处理

数据绑定库允许我们在事件到View时候通过表达式去处理它。
在数据绑定库中支持两种机制:方法调用和监听器绑定。

好想一笔带过,因为原文看不明白~~~~(>_<)~~~~
方法调用

点击事件会直接绑定到处理方法上,当一个事件发生,会直接传给绑定的方法。类似我们在布局上使用android:onclick与Activity 的方法绑定。在编译的时候已经绑定,在@{}表达式中的方法如果在Activity找不到或者方法名错误,就会在编译时期报错,方法签名(返回类型和参数相同)一致。

丢个栗子:

定义一个接口,用于处理事件。

//定义一个处理点击事件的类
interface  MethodHandler {
    fun onClick(view: View)
}

在布局声明了methodHandler变量,并在Button的onClick方法使用表达式@{methodHandler::onClick},onClick方法需要与上面接口一致,不然编译器期报错。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        ...
        <variable name="methodHandler"
            type="com.gitcode.jetpack.MethodHandler"/>
    </data>

    <LinearLayout
            android:layout_width="match_parent"
            android:orientation="vertical"
            android:gravity="center_horizontal"
            android:layout_height="match_parent">
         ...
        <Button android:layout_width="wrap_content"
                android:text="Method references"
                android:layout_marginTop="10dp"
                android:onClick="@{methodHandler::onClick}"
                android:layout_height="wrap_content"/>
    </LinearLayout>
</layout>

然后在Activity中实现MethodHandler,并赋值给绑定类的变量。

class MainActivity : AppCompatActivity(), MethodHandler{
    lateinit var binding: ActivityMainBinding

      override fun onCreate(savedInstanceState: Bundle?) {
        ...
        binding.methodHandler = this
    }
    
    override fun onClick(view: View) {
        Log.i(TAG, "Method handling")
    }
}

因此,当我们点击Button的时候,Activity的onClick方法就会被回调。

监听器绑定

监听器绑定与方法调用不同的是,监听器不再编译器与处理方法绑定,而是在点击事件传递到当前view时,才与处理方法绑定,而且监听器并不要表达式方法名与处理方法同名,只要返回类型一致即可,如果有返回值得话。

来个栗子:

  • 定义接口用于处理事件
interface  ListenerHandler {
    fun onClickListener(view: View)
}
  • 在布局中定义变量和表达式
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="listener" type="com.gitcode.jetpack.ListenerHandler"/>
    </data>

    <Button android:layout_width="wrap_content"
            android:text="Listener"
            android:layout_marginTop="10dp"
            android:onClick="@{(view)->listener.onClickListener(view)}"
            android:layout_height="wrap_content"/>
    </LinearLayout>
<layout>

注意到使用lambda表达式,因此可以在@{}内做更多操作,如预处理数据等。

  • 处理方法
    同样在Activity实现ListenerHandler方法,并赋值给绑定类的变量。
class MainActivity : AppCompatActivity(), ListenerHandler {
    lateinit var binding: ActivityMainBinding
    
    override fun onClickListener(view: View) {
        Log.i(TAG, "Listener handling")
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        binding.listener=this
    }
}

点击Button,就能看到onClickListener回调了~

不过瘾的,看官网

好了,讲到这里,大家喝杯奶茶续命,休息会吧~

吃瓜啦

吃完瓜了没?吃完了就该继续撸文了,毕竟革命尚未成功~

绑定类

前面讲的大多数是在布局中去使用表达式,从这开始,讲点代码中的操作。在一开始入门时候,讲到会根据当前布局生成绑定类,绑定类类名由布局名称根据Pascal规则和添加Binding后缀生成。举个栗子就明白了,当前布局名称:activity_shared.xml。生成绑定类名称:ActivitySharedBinding。

那么绑定类的作用是什么?

绑定类是数据绑定库为让我们可以访问布局中的变量和视图而生成的类。

如何创建或者定制绑定类呢?

创建绑定类

  • 使用静态inflate()方法
ActivityMainBinding.inflate(LayoutInflater.from(this))

重载版本

ActivityMainBinding.inflate(getLayoutInflater(), viewGroup, false)
  • 使用静态bind()方法
//一般这种情况是布局有作其他用途
ActivityMainBinding.bind(viewRoot)
  • 在Fragment,ListView,或RecyclerView的adapter使用
val listItemBinding = ListItemBinding.inflate(layoutInflater,
                        viewGroup, false)
// 或者
val listItemBinding = DataBindingUtil
                 .inflate(layoutInflater, R.layout.list_item,
                 viewGroup, false)

定制绑定类

通过修改data元素的class属于达到定制不同名称的绑定类,和其所存储位置。

//生成绑定类名为:ContactItem,存放在当前组件的绑定类包中
<data class="ContactItem">
    …
</data>

//生成绑定类名为:ContactItem,存放在当前组件包中
<data class=".ContactItem">
    …
</data>
//生成绑定类名为:ContactItem,存放在com.gitcode包中
<data class="com.gitcode.ContactItem">
    …
</data>

访问Views

如果需要访问布局中Views,需要给Views添加id,数据绑定库会尽快通过findViewById去绑定。并在Activity中通过绑定类使用。例如:

binding.tvName.text="GitCode"

访问变量

数据绑定库会为在布局中声明的变量在绑定类中生成setter和getter。例如:

binding.user=User("GitCode",3)

绑定类官网

绑定适配器

每个布局表达式都对应着一个绑定适配器,用于进行设置相应属性或监听器所需的框架调用.通俗点说,我们通过调用什么方法去给属性赋值?我们在代码通过setText()方法给view的text属性赋值。讲的就是下面的代码:

binding.tvAge.text="20" //通过tvAge的setText()给TextView的android:text属性赋值

好像跟我们平常调用的没什么区别:

tvAge.text="20"

这里讲的就是这个,当数据变化时,我们调用合适的方法(例如setText方法),去给view的属性赋值(例如android:text的text属性)。还不懂的话,继续看~

给View的属性赋值

数据绑定库提供三种方式让我们去给View的属性赋值:库自己决定选择调用方法;明确指定调用方法;自定义调用逻辑方法。

库自动选择

假如View有个属性color,库会尝试去查找setColor(args)方法,参数args的类型需要和表达式的返回类型一致。例如android:color=@{"black"},因为"black"是字符串类型,所以args的参数类型就是String。命名空间android并没有作强制要求,也可以是gitcode:color=@{"black"}。库查找方法的标准是setXXX()方法名和参数类型,这里的XXX是指属性名。

明确指定

虽然库自动选择已经很智能了,但有时view的属性和方法名并不一致,这是就需要我们明确指定,避免库自动选择找不到。例如ImageView的android:tint属性是关联到setImageTintList(ColorStateList)方法,而不是setTint(),这时,就需要明确指定了。

@BindingMethods(value = [
BindingMethod(
    type = android.widget.ImageView::class,
    attribute = "android:tint",
    method = "setImageTintList")])

BindingMethods是注解在类上的,例如Activity。可以包含一个到多个BindingMethod注解。BindingMethod中type表示当前方法(method)匹配到到哪个View的属性(attribute)上。

定制逻辑方法

虽然上面两者已经满足了大多数情况,但一些特殊情况还是需要自己处理逻辑的。例如,view的android:paddingLeft属性,没有setPaddingLeft(int)方法,但提供了setPadding(left, top, right, bottom)方法。这时候就需要我们自定义逻辑了。

@BindingAdapter("android:paddingLeft")
fun setPaddingLeft(view: View, padding: Int) {
    view.setPadding(padding,
                view.getPaddingTop(),
                view.getPaddingRight(),
                view.getPaddingBottom())
}

BindingAdapter注解允许定制属性的setter逻辑。setPaddingLeft方法的第一个参数必须是我们要处理属性的逻辑的View,后面的参数是根据BindingAdapter注解的属性来定位的。例如这里BindingAdapter注解只声明了android:paddingLeft属性,那么参数padding就是paddigLeft对应的值。设置多个属性是这样子的:

@BindingAdapter("imageUrl", "error")
fun loadImage(view: ImageView, url: String, error: Drawable) {
    Picasso.get().load(url).error(error).into(view)
}

<ImageView app:imageUrl="@{venue.imageUrl}"
        app:error="@{@drawable/venueError}" />

从这里可以看出,库对命名空间并没有作要求。注解的值imageUrl和error类型必须对应方法参数url和error的类型String和Drawable,只有ImageView同时匹配到两个属性,上述方法才会生效。为此,可以通过设置requireAll = false,匹配一个值也会生效。

@BindingAdapter(value = ["imageUrl", "placeholder"], requireAll = false)
fun setImageUrl(imageView: ImageView, url: String, placeHolder: Drawable) {
    if (url == null) {
        imageView.setImageDrawable(placeholder);
    } else {
        MyImageLoader.loadInto(imageView, url, placeholder);
    }
}

类型转换

在绑定表达式返回一个对象时,库会选择一个方法来设置属性的值,而该对象会转型为方法参数的类型。这种机制可以方便使用ObservableMap来存储数据。

<TextView
   android:text='@{userMap["lastName"]}'
   android:layout_width="wrap_content"
   android:layout_height="wrap_content" />

绑定表达式的userMap["lastName"]会返回值,该值会查找setText(CharSequence) 方法中自动转型为字符串并设置给TextView的text属性。但参数类型不确定的时候,就需要进行强制类型转换了,以表明类型。

有时候,绑定表达式返回的类型与设置属性方法的参数类型并不一致。例如:android:background属性期待的是Drawable(setBackground(drawable),但设置color值时确实一个Int。

<View
   android:background="@{@color/red}"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"/>

这时候我们需要使用BindingConversion注解将返回值类型Int转换成期待的类型Drawable。

@BindingConversion
fun convertColorToDrawable(color: Int) = ColorDrawable(color)

总结

写本文的时候,参考官网,看英文文档,对一个英语刚过四级的人...词我都认识,但组成句子,我就一脸懵逼了...

写到一半的时候,想放弃,或者想一笔带过...但,说过,要打造高质量文章,和对读者负责,所以熬了几个夜...夜太黑,没人担心明天会不会后悔~

看了一下别人的文章,基本都是支持参考官网翻译的,并没有加入个人理解和筛选。而本文是在多次参考阅读官网文章之下加入个人理解,让本文更加通俗易懂,更清晰表达官网的意图。

能看到结尾的同学也是很牛逼,需要很大的耐心,给你点个👍。那能不能举个爪,让我看看你们的👐。

Data Binding还有其他知识点,我发现的英语水平已经不够用,大家可以看看原汁原味的官网,或者等到后面我再把它写完...

坚持初心,写优质好文章

开文有益,Star支持好文

ConstraintLayout看完一篇就是高手了

1. 前言

ConstraintLayout入门到深入,如果看不完还不会,我手把手教你..

2. ConstraintLayout

ConstraintLayout作为一款可以灵活调整view位置和大小的Viewgroup被Google疯狂推荐,以前创建布局,默认根元素都是LinearLayout,现在是ConstraintLayout了。ConstraintLayout能够以支持库的形式最小支持到API 9,同时也在不断的丰富ConstraintLayout的API和功能。ConstraintLayout在复杂布局中能够有效的,降低布局的层级,提高性能,使用更加灵活。

在app组件的Graldle默认都有如下依赖:

//可能版本不一样哦
implementation 'com.android.support.constraint:constraint-layout:1.1.3

迫不及待想了解ConstraintLayout能在布局做点什么了。

2.1 相对定位

相对定位,其实这跟RelativeLayout差不多,一个View相对另外一个View的位置。

相对布局
通过简单的使用ConstraintLayout的属性也就可以实现以上布局。World对于Hello的右边,GitCode对位于Hello的下边

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 
    ...>
    <TextView
            ...
            android:text="Hello"
            android:id="@+id/tvHello"/>

    <TextView
            ...
            android:text="World"
            app:layout_constraintLeft_toRightOf="@+id/tvHello"/>

    <TextView
            ...
            android:text="GitCode"
            app:layout_constraintTop_toBottomOf="@id/tvHello"/>
</android.support.constraint.ConstraintLayout>

以TextView World相对位置属性layout_constraintLeft_toRightOf来说,constraintLeft表示TextView World本身的左边,一个View有四条边,因此TextView的上、右、下边分别对应着constraintTop、constraintRight、constraintBottom。toRightOf则表示位于另外一个View的右边,例如此处位于Hello的右边,因此对应还有toLeftOf、toRghtOf、toBottomOf,分别位于View Hello的左、右、下边。总结的说,constraintXXX表示View自身约束的边,toXXXOf表示另一个View的边,而XXX的值可以是Left、Top、Right、Bottom,分别对应左,上、右、下边。layout_constraintStart_toEndOf也是类似的道理。

另外需要注意的是,view的位置可以相对于同层的view和parent,在相对于parent的时候toLeftOf、toTopOf、toRghtOf、toBottomOf分别表示位于parent的内部左上右下边缘。如图:红色框表示parent view。


再来看看一个特殊的场景:


此时想要Hello和World文本中间对齐怎么办?ConstraintLayout提供了layout_constraintBaseline_toBaselineOf属性。

<TextView
   ...
    android:text="Hello"
    android:id="@+id/tvHello"/>

<TextView
    ...
    android:text="World"
    app:layout_constraintBaseline_toBaselineOf="@id/tvHello"
    app:layout_constraintLeft_toRightOf="@+id/tvHello"/>

此时界面就如愿了,比Relativelayout方便多了。

什么是baseline?贴张官网的图。

2.2 边距

边距与平常使用并无太大区别,但需要先确定view的位置,边距才会生效。如:

<TextView
        ...
        android:layout_marginTop="10dp"
        android:layout_marginLeft="10dp"/>

在其他的ViewGroup,TextView的layout_marginToplayout_marginLeft属性是会生效的,但在ConstraintLayout不会生效,因为此时TextView的位置还没确定。下面的代码才会生效。

<TextView
        ...
        android:layout_marginTop="10dp"
        android:layout_marginLeft="10dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
         />

常用属性如下:

  • android:layout_marginStart
  • android:layout_marginEnd
  • android:layout_marginLeft
  • android:layout_marginTop
  • android:layout_marginRight
  • android:layout_marginBottom

GONE Margin

有时候,会有这种需求,在World可见的时候,GitCode与World的左边距是0,当World不见时,GitCode的左边距是某个特定的值。

World可见的效果,GitCode的左边距为0

World不可见的效果,GitCode的左边距为10

为此,ConstraintLayout提供了特殊的goneMargin属性,在目标View隐藏时,属性生效。有如下属性:

  • layout_goneMarginStart
  • layout_goneMarginEnd
  • layout_goneMarginLeft
  • layout_goneMarginTop
  • layout_goneMarginRight
  • layout_goneMarginBottom

Centering positioning and bias

在RelativeLayout居中,通常是使用以下三个属性:

  • layout_centerInParent 中间居中
  • layout_centerHorizontal 水平居中
  • layout_centerVertical 垂直居中

而在ConstraintLayout居中则采用左右上下边来约束居中。

  • 水平居中 layout_constraintLeft_toLeftOf & layout_constraintRight_toRightOf
  • 垂直居中 layout_constraintTop_toTopOf & layout_constraintBottom_toBottomOf
  • 中间居中 水平居中 & 垂直居中
    举个栗子:
<TextView
    ...
    android:text="Hello"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"/>

效果图:


那,要是想把Hello往左挪一点,怎么办?

那很简单,使用margin呀。不不不,这里要介绍的是另外两个属性,与LinearLayout的权重类似(当然,ConstraintLayout也可以使用权重属性),但简单很多。

  • layout_constraintHorizontal_bias 水平偏移
  • layout_constraintVertical_bias 垂直偏移

两个属性的取值范围在0-1。在水平偏移中,0表示最左,1表示最右;在垂直偏移,0表示最上,1表示最下;0.5表示中间。

<TextView
    ...
    android:text="Hello"
    app:layout_constraintHorizontal_bias="0.8"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"/>

效果:

2.3 圆形定位(Added in 1.1)

圆形定位指的是View的中心点相对于另外View中心点的位置。贴张官网图。


涉及三个属性:

  • layout_constraintCircle : 另外一个view的id,上图的A view
  • layout_constraintCircleRadius : 半径,上图的radius
  • layout_constraintCircleAngle : 角度,上图angle,范围为0-360
    根据上面上个属性就可以确定B View的位置。从图也可以知道,角度以时间12点为0,顺时针方式。

吃个栗子:

<TextView
    ...
    android:text="Hello"
    android:id="@+id/tvHello"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"/>

<TextView
    android:text="World"
    app:layout_constraintCircle="@id/tvHello"
    app:layout_constraintCircleRadius="180dp"
    app:layout_constraintCircleAngle="135"/>

效果图:Hello中间居中,World 135角度

2.4 尺寸约束

ConstraintLayout 最大最小尺寸

ConstraintLayout的宽高设为WRAP_CONTENT时,可以通过以下熟悉设置其最大最小尺寸。

  • android:minWidth 最小宽度
  • android:minHeight 最小高度
  • android:maxWidth 最大宽度
  • android:maxHeight 最大高度

ConstraintLayout中的控件尺寸约束

在ConstraintLayout中控件可以三种方式来设置其尺寸约束。

  • 指定具体的值。如123dp
  • 使用值WRAP_CONTENT,内容自适配。
  • 设为0dp,即MATCH_CONSTRAINT,扩充可用空间。

第一二种跟平常使用没什么区别。第三种会根据约束情况重新计算控件的大小。
在ConstraintLayout中,不推荐使用MATCH_PARENT,而是推荐使用MATCH_CONSTRAINT(0dp),它们的行为是类似的。

吃个栗子吧:

 <TextView
        android:text="Hello"
        android:id="@+id/tvHello"
        android:gravity="center"
        android:padding="20dp"
        app:layout_constraintTop_toTopOf="parent"
        android:textColor="@color/colorWhite"
        android:background="@color/colorPrimary"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_width="0dp"
        android:layout_marginRight="20dp"
        android:layout_height="wrap_content"/>

设置layout_width为0dp;layout_heightwrap_content;layout_marginRight为20dp,与parent左右对齐。

效果图:

在1.1之前的版本,控件尺寸设为WRAP_CONTENT,控件默认是由组件文本大小控制,其他约束是不生效的。可以通过以下属性设置是否生效。

  • app:layout_constrainedWidth=”true|false”
  • app:layout_constrainedHeight=”true|false”

控件设为MATCH_CONSTRAINT时,控件的大小会扩展所有可用空间,在1.1版本后,可以通过以下属性改变控件的行为。

  • layout_constraintWidth_min 最小宽度
  • layout_constraintHeight_min 最小高度
  • layout_constraintWidth_max 最大宽度
  • layout_constraintHeight_max 最大高度
  • layout_constraintWidth_percent 宽度占parent的百分比
  • layout_constraintHeight_percent 高度占parent的百分比

吃个栗子:

<TextView
        android:text="Hello"
        android:id="@+id/tvHello"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintWidth_percent="0.5"
        app:layout_constraintWidth_default="percent"
        android:layout_width="0dp"
        android:layout_height="wrap_content"/>

android:layout_width设为MATCH_CONSTRAINT,即0dp;将app:layout_constraintWidth_default设为percent;将app:layout_constraintWidth_percent设为0.5,表示占parent的50%,取值范围是0-1。

效果图:

比例约束

控件的宽高比,要求是宽或高至少一个设为0dp,然后设置属性layout_constraintDimensionRatio即可。

<TextView
    android:text="Hello"
    app:layout_constraintDimensionRatio="3:1"
    android:layout_width="0dp"
    android:layout_height="100dp"
    />

这里设置宽高比为3:1,高度为100dp,那么宽度将为300dp。


也可以在比例前加W,H表示是宽高比还是高宽比。如下面表示高宽比。

<Button android:layout_width="0dp" android:layout_height="0dp"
    app:layout_constraintDimensionRatio="H,16:9"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintTop_toTopOf="parent"/>

2.5 链

链在水平或者垂直方向提供一组类似行为。如图所示可以理解为横向链。这里需要了解一点,A与parent的左边缘约束,B与parent的右边边缘约束,A右边和B左边之间相互约束,才能使用一条链。多个元素之间也是如此,最左最右与parent约束,元素之间边相互约束。不然下面的链式永远无法生效。


横向链最左边第一个控件,垂直链最顶边第一个控件称为链头,可以通过下面两个属性链头统一定制链的样式。

  • layout_constraintHorizontal_chainStyle 水平方向链式
  • layout_constraintVertical_chainStyle 垂直方向链式

它两的值默认可以是

  • CHAIN_SPREAD 展开样式(默认)
  • Weighted chain 在CHAIN_SPREAD样式,部分控件设置了MATCH_CONSTRAINT,那他们将扩展可用空间。
  • CHAIN_SPREAD_INSIDE 展开样式,但两端不展开
  • CHAIN_PACKED 抱团(打包)样式,控件抱团一起。通过偏移bias,可以改变packed元素的位置。

    从实际开发,这么应用还是挺广泛的。
    提供份代码参考,避免走冤枉路:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <TextView
            android:text="Hello"
            android:id="@+id/tvHello"
            android:gravity="center"
            android:padding="20dp"
            app:layout_constraintHorizontal_chainStyle="spread"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toLeftOf="@id/tvWorld"
            android:textColor="@color/colorWhite"
            android:background="@color/colorPrimaryDark"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    <TextView
            android:text="World"
            android:gravity="center"
            android:padding="20dp"
            android:id="@+id/tvWorld"
            app:layout_constraintLeft_toRightOf="@id/tvHello"
            app:layout_constraintRight_toRightOf="parent"
            android:textColor="@color/colorWhite"
            android:background="@color/colorPrimary"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    
</android.support.constraint.ConstraintLayout>

效果:

在链中,剩余空余空间默认平均给各元素,但有时可以通过权重属性layout_constraintVertical_weight 来指定分配空间的大小。

1.1之后,在链中使用边距时,边距是相加的,也就说,假设Hello的右边距为5,World的左边距为20,那么它们之间的边距就是25。在链式,边距先从剩余空间减去的,然后再用剩余的空间在元素之间进行定位。

2.6 优化器

在1.1之后,公开了优化器,通过在app:layout_optimizationLevel来决定控件在哪方面进行优化。

  • none : 不进行优化
  • standard : 默认方式, 仅仅优化direct和barrier约束
  • direct : 优化direct约束
  • barrier : 优化barrier约束
  • chain : 优化链约束 (实验性质)
  • dimensions : 优化尺寸 (实验性质), 减少测量次数

3.工具类

3.1 Guideline(参考线)

参考线实际上不会在界面进行显示,只是方便在ConstraintLayout布局view时候做一个参考。

通过设置Guideline的属性orientation来表示是水平方向还是垂直方向的参考线,对应值为verticalhorizontal。可以通过三种方式来定位Guideline位置。

  • layout_constraintGuide_begin 从左边或顶部指定具体的距离
  • layout_constraintGuide_end 从右边或底部指定具体的距离
  • layout_constraintGuide_percent 从宽度或高度的百分比来指定具体距离

丢个栗子:

<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <android.support.constraint.Guideline
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/guideline"
            android:orientation="vertical"
            app:layout_constraintGuide_begin="10dp"/>

    <Button android:text="Button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/button"
            app:layout_constraintLeft_toLeftOf="@+id/guideline"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toTopOf="parent"/>

    <Button android:text="Button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/button2"
            app:layout_constraintLeft_toLeftOf="@+id/guideline"
            android:layout_marginTop="16dp"
            app:layout_constraintTop_toBottomOf="@id/button"/>
</android.support.constraint.ConstraintLayout>

Guideline设置为垂直参考线,距离开始的位置为10dp。如下图所示,实际中需要把鼠标移到button才会显示出来哦。

3.2 Barrier(栅栏)

Barrier有点类似Guideline,但Barrier会根据所有引用的控件尺寸的变化重新定位。例如经典的登录界面,右边的EditText总是希望与左右所有TextView的最长边缘靠齐。
如果两个TextView其中一个变得更长,EditText的位置都会跟这变化,这比使用RelativeLayout灵活很多。

代码:

<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <android.support.constraint.Barrier
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:barrierDirection="right"
            android:id="@+id/barrier"
            app:constraint_referenced_ids="tvPhone,tvPassword"
            />

    <TextView android:layout_width="wrap_content"
              android:text="手机号码"
              android:id="@+id/tvPhone"
              android:gravity="center_vertical|left"
              android:padding="10dp"
              android:layout_height="50dp"/>

    <TextView android:layout_width="wrap_content"
              android:text="密码"
              android:padding="10dp"
              android:gravity="center_vertical|left"
              android:id="@+id/tvPassword"
              app:layout_constraintTop_toBottomOf="@id/tvPhone"
              android:layout_height="wrap_content"/>

    <EditText android:layout_width="wrap_content"
              android:hint="输入手机号码"
              android:id="@+id/etPassword"
              app:layout_constraintLeft_toLeftOf="@id/barrier"
              android:layout_height="wrap_content"/>

    <EditText android:layout_width="wrap_content"
              android:hint="输入密码"
              app:layout_constraintTop_toBottomOf="@id/etPassword"
              app:layout_constraintLeft_toLeftOf="@id/barrier"
              android:layout_height="wrap_content"/>


</android.support.constraint.ConstraintLayout>

app:barrierDirection所引用控件对齐的位置,可设置的值有:bottom、end、left、right、start、top.constraint_referenced_ids为所引用的控件,例如这里的tvPhone,tvPasswrod。

3.3 Group(组)

用来控制一组view的可见性,如果view被多个Group控制,则以最后的Group定义的可见性为主。

吃个香喷喷栗子吧:
Group默认可见时,是这样的。

设置Group的visible属性为gone.

<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <android.support.constraint.Group
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/group"
            android:visibility="gone"
            app:constraint_referenced_ids="tvPhone,tvPassword"
            />

    <TextView android:layout_width="wrap_content"
              android:text="手机号码"
              android:id="@+id/tvPhone"
              android:gravity="center_vertical|left"
              android:padding="10dp"
              android:layout_height="50dp"/>

    <TextView android:layout_width="wrap_content"
              android:text="密码"
              android:padding="10dp"
              android:gravity="center_vertical|left"
              android:id="@+id/tvPassword"
              app:layout_constraintLeft_toRightOf="@id/tvPhone"
              app:layout_constraintTop_toBottomOf="@id/tvPhone"
              android:layout_height="wrap_content"/>

    <TextView android:layout_width="wrap_content"
              android:text="GitCode"
              android:padding="10dp"
              android:gravity="center_vertical|left"
              app:layout_constraintLeft_toRightOf="@id/tvPassword"
              android:layout_height="wrap_content"/>

</android.support.constraint.ConstraintLayout>

效果就变成了这样了,tvPhone,tvPassword都被隐藏了。

3.4 Placeholder(占位符)

一个view占位的占位符,当指定Placeholder的content属性为另一个view的id时,该view会移动到Placeholder的位置。

代码中,将TextView的定位在屏幕中间,随着将id设置给Placeholder的属性后,TextView的位置就跑到Placeholder所在的地方,效果图跟上图一直。

<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    <android.support.constraint.Placeholder
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:content="@id/tvGitCode"
              />
    
    <TextView android:layout_width="wrap_content"
              android:text="GitCode"
              android:id="@+id/tvGitCode"
              android:padding="10dp"
              app:layout_constraintLeft_toLeftOf="parent"
              app:layout_constraintRight_toRightOf="parent"
              android:gravity="center_vertical|left"
              android:layout_height="wrap_content"/>

</android.support.constraint.ConstraintLayout>

3.5 其他

在2.0,为ConstraintLayout增加了ConstraintProperties、ConstraintsChangedListener等,感兴趣可以自己看看官网。

ConstraintLayout

4.总结

在写本文之前,其实还不会用ConstraintLayout,写完本文之后,已经上手和喜欢上了,满足自己在实际开发中想要的效果,能够有效的减少布局的层级,从而提高性能。不知道看完本文,你会使用ConstraintLayout了没有?

如果文章有误,请帮忙指正,在此谢过!

坚持初心,写优质好文章

开文有益,Star支持好文

Android HandlerThread

前言

在Android中,主线程与子线程的交互,例如在子线程进行网络请求,请求后将数据更新到View上,我们常用Handler或者AsyncTask。HandlerThread与它们的区别在于会创建工作线程、Hanlder和Looper。这样就不用在主线程创建Handler或者AsyncTask的硬性要求。可以说HandlerThread是Handler的应用场景。

HandlerThread的使用

HandlerThread继承至Thread,所以本身也是一个线程。先撸代码为敬,下面是一般例子代码。

        HandlerThread handlerThread = new HandlerThread("handler");

        handlerThread.start();

        Handler workerHandler = new Handler(handlerThread.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                //doSomething
            }
        };

        workerHandler.post(new Runnable() {
            @Override
            public void run() {
                //doSomething
            }
        });

在HandlerThread构造方法中可以随意传入一个字符串,该字符串只用于区别不同线程而已,方便在jstack分析或者问题定位,这里是handler。接着将HandlerThread对象的Looper对象作为Handler的参数,新建Handler对象。这样就将工作线程的Looper和Handler绑定在一起了,也就是工作线程和Handler绑定在一起了。通过Handler对象发送消息或者任务到Looper对象中的消息队列,在工作线程中处理消息或任务。

如果不再使用工作线程,建议调用HandlerThread对象的quitSafely方法退出,避免不要的资源浪费。

源码分析

在理解了Android的消息机制之后,ThreadHandler的原理可以说非常的简单,只是Android消息机制的一个应用。

HandlerThread 的构造器

    public HandlerThread(String name) {
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }
    
    public HandlerThread(String name, int priority) {
        super(name);
        mPriority = priority;
    }

HandlerThread有两个重载构造器,一个是使用线程默认优先级的构造器,一个是可以供用户自己定义线程优先权的构造器。

HandlerThread的Run方法

在线程中,调用Thread对象的start方法,最终会run方法。而HandlerThread重写了run方法。

    @Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }

在run方法中,主要是创建了Looper对象,并启动循环。onLooperPrepared方法是受保护的空方法,可以通过继承HandlerThread重写该方法,在开启消息循环前做一些准备。

如果对于Looper和Handler比较陌生,可以查看Android的消息机制

总结

  • Handler和AsyncTask之所以要在主线程创建实例,是因为要和主线程的Looper绑定。
  • HandlerThread通过创建独立的拥有Looper的工作线程来进行耗时任务。Looper可以可以用来创建Handler。在主线程通过Handler对象将耗时任务发送到工作线程中。
  • HandlerThread本质上只是对Android消息机制的应用,更方便开发者。知道Android消息机制原理,完全可以实现类似的功能。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.