CallteFoot's blog

Victory belongs to the most persevering


  • 首页

  • 归档

  • 分类

  • 标签

  • 关于

java之HttpURLConnection

发表于 2019-02-15 | 分类于 Java , HTTP

概要

在使用 HttpURLConnection前应对一些基础概念有所认识,比如 TCP/IP 协议,HTTP 报文, Socket 等。

  • Socket是 TCP 协议层的概念,如果要使用 Socket 直接通信,需要使用远程地址和端口号。其中,端口号根据具体的协议而不同,比如 HTTP 协议默认使用的端口号为 80/tcp。
  • HttpURLConnection 是在底层连接上的一个请求,最终也是通过 Socket 连接网络,所谓的 underlaying Socket。使用 HttpURLConnection 不需要指定远程地址和端口号。
  • HttpURLConnectionU 只是一个抽象类,只能通过 url.openConection() 方法创建具体的实例。严格来说,openConection() 方法返回的是 URLConnection 的子类。根据 url 对象的不同,如可能不是 http:// 开头的,那么 openConection() 返回的可能就不是 HttpURLConnection。

⚠️ 由于Android是基于java开发,因此关于http请求也是基于HttpURLConnection实现的,但是在android API 23之前的sdk提供过HttpClient网路请求处理类,它们区别如下:

  • 标准Java接口(java.NET) —-HttpURLConnection,可以实现简单的基于URL请求、响应功能;
  • Apache接口(org.appache.http)—-HttpClient,使用起来更方面更强大,不易扩展;

请求流程

HttpURLConnection-flow
首先看一个简单的GET请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
try {
//构建URL对象
URL url = new URL(requestUrl);
//获取HttpURLConnection实例,httpurlconnection为同步请求
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
//设置请求方法
connection.setRequestMethod("GET");
//设置链接超时时间
connection.setConnectTimeout(10000);
//设置读取超时时间
connection.setReadTimeout(15000);
//添加Header
connection.setRequestProperty("Connection", "keep-Alive");
//建立链接,请求前需设定请求行、请求报头;
connection.connect();
StringBuffer response = new StringBuffer();
BufferedReader reader;
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
//将响应流转换成字符串,响应流只能读取一次
reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
System.out.println(response.toString());
} else {
reader = new BufferedReader(new InputStreamReader(connection.getErrorStream()));
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
System.out.println(response.toString());
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

⚠️:对于get请求

  1. url与参数之间用?隔开。
  2. 键值对中键与值用=连接。
  3. 两个键值对之间用&连接。

请求属性设置

  • setRequestMethod:设定请求的方法,默认是GET
  • setDoInput:设置是否从httpUrlConnection读入,默认情况下是true;
  • setDoOutput:设置是否向httpUrlConnection输出,为post请求时,参数要放在http正文内,因此需要设为true, 默认情况下是false;
  • setUseCaches:设置是否使用缓存,Post请求不能使用缓存
  • setConnectTimeOut:单位:毫秒,设置连接超时时间,如果不设置超时(timeout),在网络异常的情况下,可能会导致程序僵死而不继续往下执行。
  • setReadTimeOut:单位:毫秒,设置读取超时时间
    以上属性需要在调用 connect() 之前完成,调用connect()后就与服务器建立 Socket 连接,而连接以后,连接属性就不可以再修改;但是可以查询服务器返回的头信息了(header information)

请求头或响应头设置

HTTP请求允许一个key带多个用逗号分开的values,但是HttpURLConnection只提供了单个操作的方法:

  • setRequestProperty: 设置HTTP HEAD 中的一些参数,如果参数存在则覆盖;
  • addRequestProperty: 向HTTP HEAD 中的添加参数,允许一个key存在多个value;

发送URL请求

建立实际连接之后,就是发送请求,把请求参数传到服务器,这就需要使用outputStream把请求参数传给服务器:

  • getOutputStream(),此处getOutputStream会隐含的进行connect(即:如同调用上面的connect()方法,所以在开发中不调用上述的connect()也可以)。

获取响应

请求发送成功之后,即可获取响应的状态码,如果成功既可以读取响应中的数据,获取这些数据的方法包括:

  • getContent
  • getHeaderField
  • getInputStream
    对于大部分请求来说,getInputStream和getContent用的最多的。
    获取响应头标准信息通过以下方式获取:
    • getContentEncoding
    • getContentLength
    • getContentType
    • getDate
    • getExpiration
    • getLastModifed
      查询头信息信息:
      • getHeaderFieldKey
      • getHeaderField
      • getHeaderFields() 返回一个包含消息头所有字段的标准 map 对象

对于get请求一般不需要向http正文内写数据与发送数据,但对于post需要的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//向服务端发送一个json内容请求,(这里的json可以替换成字符串或者文件等)
try {
URL url = new URL(requestUrl);
HttpURLConnection mConnection = (HttpURLConnection) url.openConnection();
//设置链接超时时间
mConnection.setConnectTimeout(10000);
//设置读取超时时间
mConnection.setReadTimeout(15000);
//设置请求方法
mConnection.setRequestMethod(“POST”);
//添加Header,设定请求参数类型为json类型
mConnection.setRequestProperty("Content-Type", "application/json;charset=utf-8");
//接受输入流
mConnection.setDoInput(true);
//有请求数据时,必须开启此项!
mConnection.setDoOutput(true);
//POST不支持缓存
mConnection.setUseCaches(false);

String body = new Gson().toJson(new User("18667148156", "123456"));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(mConnection.getOutputStream(), "UTF-8"));
writer.write(body);
writer.close();

StringBuffer response = new StringBuffer();
BufferedReader reader;
System.out.println(mConnection.getResponseCode() + " " + mConnection.getResponseMessage());
if (mConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
//将响应流转换成字符串,响应流只能读取一次
reader = new BufferedReader(new InputStreamReader(mConnection.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
System.out.println(response.toString());
} else {
InputStream inputStream = mConnection.getErrorStream();
if (null != inputStream) {
reader = new BufferedReader(new InputStreamReader(mConnection.getErrorStream()));
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
} else {
response.append(mConnection.getResponseMessage());
}
System.out.println(response.toString());
}
} catch (MalformedURLException e) {
e.printStackTrace();

} catch (ProtocolException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

小结:

  • HttpURLConnection的connect()函数,实际上 只是建立了一个与服务器的tcp连接,并没有实际发送http请求。无论是post还是get,http请求实际上直到HttpURLConnection的getInputStream()这个函数里面才正式发送出去;
  • 在用POST方式发送URL请求时,URL请求参数的设定顺序是重中之重, 对connection对象的一切配置(那一堆set函数都必须要在connect()函数执行之前完成。而对outputStream的写操作,又必须要在inputStream的读操作之前。 这些顺序实际上是由http请求的格式决定的。如果inputStream读操作在outputStream的写操作之前,会抛出例外:java.net.ProtocolException: Cannot write output after reading input;
  • http请求实际上由两部分组成, 一个是http头,所有关于此次http请求的配置都在http头里面定义,一个是正文content。connect()函数会根据HttpURLConnection对象的配置值生成http头部信息,因此在调用connect函数之前,就必须把所有的配置准备好。
  • 在http头后面紧跟着的是http请求的正文,正文的内容是通过outputStream流写入的,实际上outputStream不是一个网络流,充其量是个字符串流,往里面写入的东西不会立即发送到网络,而是存在于内存缓冲区中,待outputStream流关闭时,根据输入的内容生成http正文。至此,http请求的东西已经全部准备就绪。在getInputStream()函数调用的时候,就会把准备好的http请求正式发送到服务器了,然后返回一个输入流,用于读取服务器对于此次http请求的返回信息。由于http 请求在getInputStream的时候已经发送出去了(包括http头和正文),因此在getInputStream()函数 之后对connection对象进行设置(对http头的信息进行修改)或者写入outputStream(对正文进行修改)都是没有意义的了,执行这些操作会导致异常的发生。

⚠️: HTTP 传输的消息要使用 URL UTF-8 编码,英文字母、数字和部分符号保持不变,空格编码成’+’。其他字符编码成 “%XY” 形式的字节序列,特别是中文字符,不能直接传输。可以考虑使用
URLEncoder.encode(string, “UTF-8”) 方法。HttpURLConnection 只能发送一个请求

简单的上传文件

  • 基础的文件上传
    在post请求传递参数时知道,可以从连接中得到一个输出流,输出流可以像服务器写数据。同理,可以使用这个输出流将文件写到服务器。代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    try {
    URL url = new URL(getUrl);
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setRequestMethod("POST");
    connection.setDoOutput(true);
    connection.setDoInput(true);
    connection.setUseCaches(false);
    connection.setRequestProperty("Content-Type", "file/*");//设置数据类型
    connection.connect();

    OutputStream outputStream = connection.getOutputStream();
    //这里的file即为需要上传的文件
    FileInputStream fileInputStream = new FileInputStream("file");//把文件封装成一个流
    int length = -1;
    byte[] bytes = new byte[1024];
    while ((length = fileInputStream.read(bytes)) != -1){
    outputStream.write(bytes,0,length);//写的具体操作
    }
    fileInputStream.close();
    outputStream.close();

    int responseCode = connection.getResponseCode();
    if(responseCode == HttpURLConnection.HTTP_OK){
    InputStream inputStream = connection.getErrorStream();
    if (null != inputStream) {
    reader = new BufferedReader(new InputStreamReader(mConnection.getErrorStream()));
    String line;
    while ((line = reader.readLine()) != null) {
    response.append(line);
    }
    } else {
    response.append(mConnection.getResponseMessage());
    }
    connection.disconnect();
    System.out.println(response.toString());
    }

    } catch (Exception e) {
    e.printStackTrace();
    }
  • 通过multipart上传文件或者参数
    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136


    /**
    * This utility class provides an abstraction layer for sending multipart HTTP
    * POST requests to a web server.
    *
    * @author www.codejava.net
    */
    public class MultipartUtility {
    private final String boundary;
    private static final String LINE_FEED = "\r\n";
    private HttpURLConnection httpConn;
    private String charset;
    private OutputStream outputStream;
    private PrintWriter writer;

    /**
    * * This constructor initializes a new HTTP POST request with content type
    * * is set to multipart/form-data
    * * @param requestURL
    * * @param charset
    * * @throws IOException
    */
    public MultipartUtility(String requestURL, String charset)
    throws IOException {
    this.charset = charset;

    // creates a unique boundary based on time stamp
    boundary = "---" + System.currentTimeMillis() + "---";

    URL url = new URL(requestURL);
    httpConn = (HttpURLConnection) url.openConnection();
    httpConn.setUseCaches(false);
    httpConn.setDoOutput(true); // indicates POST method
    httpConn.setDoInput(true);
    httpConn.setRequestProperty("Content-Type",
    "multipart/form-data; boundary=" + boundary);
    outputStream = httpConn.getOutputStream();
    writer = new PrintWriter(new OutputStreamWriter(outputStream, charset),
    true);
    }

    /**
    * * Adds a form field to the request
    * * @param name field name
    * * @param value field value
    */
    public void addFormField(String name, String value) {
    writer.append("--" + boundary).append(LINE_FEED);
    writer.append("Content-Disposition: form-data; name=\"" + name + "\"")
    .append(LINE_FEED);
    writer.append("Content-Type: text/plain; charset=" + charset).append(
    LINE_FEED);
    writer.append(LINE_FEED);
    writer.append(value).append(LINE_FEED);
    writer.flush();
    }

    /**
    * * Adds a upload file section to the request
    * * @param fieldName name attribute in <input type="file" name="..." />
    * * @param uploadFile a File to be uploaded
    * * @throws IOException
    */
    public void addFilePart(String fieldName, File uploadFile)
    throws IOException {
    String fileName = uploadFile.getName();
    writer.append("--" + boundary).append(LINE_FEED);
    //⚠️这里的fileName为表单指定的name值
    writer.append(
    "Content-Disposition: form-data; name=\"" + fieldName
    + "\"; filename=\"" + fileName + "\"")
    .append(LINE_FEED);
    writer.append(
    "Content-Type: "
    + URLConnection.guessContentTypeFromName(fileName))
    .append(LINE_FEED);
    writer.append("Content-Transfer-Encoding: binary").append(LINE_FEED);
    writer.append(LINE_FEED);
    writer.flush();

    FileInputStream inputStream = new FileInputStream(uploadFile);
    byte[] buffer = new byte[4096];
    int bytesRead = -1;
    while ((bytesRead = inputStream.read(buffer)) != -1) {
    outputStream.write(buffer, 0, bytesRead);
    }
    outputStream.flush();
    inputStream.close();

    writer.append(LINE_FEED);
    writer.flush();
    }

    /**
    * * Adds a header field to the request.
    * * @param name - name of the header field
    * * @param value - value of the header field
    */
    public void addHeaderField(String name, String value) {
    writer.append(name + ": " + value).append(LINE_FEED);
    writer.flush();
    }


    /**
    * * Completes the request and receives response from the server.
    * * @return a list of Strings as response in case the server returned
    * * status OK, otherwise an exception is thrown.
    * * @throws IOException
    */
    public List<String> finish() throws IOException {
    List<String> response = new ArrayList<>();

    writer.append(LINE_FEED).flush();
    writer.append("--" + boundary + "--").append(LINE_FEED);
    writer.close();

    // checks server's status code first
    int status = httpConn.getResponseCode();
    if (status == HttpURLConnection.HTTP_OK) {
    BufferedReader reader = new BufferedReader(new InputStreamReader(
    httpConn.getInputStream()));
    String line;
    while ((line = reader.readLine()) != null) {
    response.add(line);
    }
    reader.close();
    //关闭链接
    httpConn.disconnect();
    } else {
    throw new IOException("Server returned non-OK status: " + status);
    }
    return response;
    }
    }

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
File directory = new File("");//参数为空

String author = directory.getAbsolutePath();

String charset = "UTF-8";
File uploadFile1 = new File(author + "aop.png");
String requestURL = "http://xxxxx/upload";

try {
MultipartUtility multipart = new MultipartUtility(requestURL, charset);

multipart.addFormField("description", "Cool Pictures");
multipart.addFormField("keywords", "Java,upload,Spring");
//
multipart.addFilePart("file", uploadFile1);

List<String> response = multipart.finish();

System.out.println("SERVER REPLIED:");

for (String line : response) {
System.out.println(line);
}
} catch (IOException ex) {
System.err.println(ex);
}

七请求输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-----1550237637934---
Content-Disposition: form-data; name="description"
Content-Type: text/plain; charset=UTF-8

Cool Pictures

-----1550237637934---
Content-Disposition: form-data; name="keywords"
Content-Type: text/plain; charset=UTF-8

Java,upload,Spring

-----1550237637934---
Content-Disposition: form-data; name="file"; filename="aop.png"
Content-Type: image/png
Content-Transfer-Encoding: binary
......//二进制数据
-----1550237637934---

其中—–1550237637934—代表一个数据块的开始或结束,这个数据需要在头文件中指定,如上文中的boundary。
⚠️http最早出现时就是为了浏览器与服务器之间的数据传输,所以有固定的协议,协议规范了一定的数据格式,所以在浏览器中传递数据时会自动按照一定的格式封装。在android中不能自动封装,所以这些操作需要手动操作。

  • 下载文件
    从服务器下载文件是比较简单的操作,只要得到输入流,就可以从流中读出数据。使用示例如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    try {
    String urlPath = "https://www.baidu.com/";
    URL url = new URL(urlPath);
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setRequestMethod("GET");
    connection.connect();
    int responseCode = connection.getResponseCode();
    if(responseCode == HttpURLConnection.HTTP_OK){
    InputStream inputStream = connection.getInputStream();
    File dir = new File("fileDir");
    if (!dir.exists()){
    dir.mkdirs();
    }
    File file = new File(dir, "fileName");//根据目录和文件名得到file对象
    FileOutputStream fos = new FileOutputStream(file);
    byte[] buf = new byte[1024*8];
    int len = -1;
    while ((len = inputStream.read(buf)) != -1){
    fos.write(buf, 0, len);
    }
    fos.flush();
    }

    } catch (Exception e) {
    e.printStackTrace();
    }

注意

  • 对于有些请求服务可能会对客户端进行过滤,如是否是浏览器,是否为android手机等客户端:
    这个主要看头部信息的user-agent,如设定客户端为浏览器:
    1
    2
    connection.setRequestProperty("user-agent",
    "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.7 Safari/537.36");

参考:
[1] http://www.blogjava.net/supercrsky/articles/247449.html
[2] https://blog.csdn.net/woxueliuyun/article/details/43267365
[3] https://www.codejava.net/java-se/networking/upload-files-by-sending-multipart-request-programmatically
[4] https://www.techcoil.com/blog/how-to-upload-a-file-via-a-http-multipart-request-in-java-without-using-any-external-libraries/

Android通过系统获取图片及截图

发表于 2019-02-12 | 分类于 Android , Media

概述

日常开发当中,调用相机或者从相册中选择照片裁剪并上传是很常见的功能,虽然网上有很多框架,但是导入
别人的库, 无疑会增加App的体积和后期维护难度,因此这里讲一下如何使用系统自带的相机,相册,并裁剪。
这里是在Android7.0以上版本上运行。

第一步FileProvider准备

  1. 在AndroidManifest.xml中增加provider节点,代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="一般为包名"
    android:grantUriPermissions="true"
    android:exported="false">
    <meta-data
    android:name="android.support.FILE_PROVIDER_PATHS"
    android:resource="@xml/filepaths" />
    </provider>
    • android:authorities 表示授权列表,填写你的应用包名,当有多个授权时,用分号隔开;
    • android:exported 表示该内容提供器(ContentProvider)是否能被第三方程序组件使用,必须为false,
      否则会报异常:Java.lang.RuntimeException: Unable to get provider android.support.v4.content.FileProvider: java.lang.SecurityException: Provider must not be exported;
    • android:grantUriPermissions=”true” 表示授予 URI 临时访问权限;
    • android:resource 属性指向创建的xml文件的路径,文件名可以随便起;
  2. 在资源(res)目录下创建一个xml目录,并建立一个以上面名字(filepaths)为文件名的xml文件,内容如下:

    1
    2
    3
    4
    <?xml version="1.0" encoding="utf-8"?>
    <paths>
    <external-path path="." name="external_path" />
    </paths>
    • external-path 代表根目录为: Environment.getExternalStorageDirectory() ,也可以写其他的,
      如: files-path 代表根目录为:Context.getFilesDir()
      cache-path 代表根目录为:getCacheDir() 其path属性的值代表路径后层级名称,为空则代表就是根目录,
      假如为“pictures”,就代表对应根目录下的pictures目录

第二步使用FileProvider

⚠️在使用FileProvider前需要在需要在AndroidManifest.xml中增加必要的读写权限:

1
2
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

  1. 通过相机获取图片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /**
    * 拍照
    */
    private void takePhoto() {
    //用于保存调用相机拍照后所生成的文件
    if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
    return;
    }
    captureFile = new File(rootFile, "temp.jpg");
    //跳转到调用系统相机
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    //判断版本 如果在Android7.0以上,使用FileProvider获取Uri
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    intent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    Uri contentUri = FileProvider.getUriForFile(mContext, getPackageName(), captureFile);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
    } else {
    //否则使用Uri.fromFile(file)方法获取Uri
    intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(captureFile));
    }
    startActivityForResult(intent, REQUEST_PERMISSION_CAMERA);
    }
  2. 通过相册获取图片

    1
    2
    3
    4
    5
    6
    7
    8
    /**
    * 从相册选择
    */
    private void choosePhoto() {
    Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
    photoPickerIntent.setType("image/*");
    startActivityForResult(photoPickerIntent, REQUEST_PERMISSION_WRITE);
    }
  3. 图片剪裁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    private void cropPhoto(Uri uri) {
    cropFile = new File(rootFile, "avatar.jpg");
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(uri, "image/*");
    //crop=true是设置在开启的Intent中设置显示的VIEW可裁剪
    intent.putExtra("crop", "true");
    //aspectX aspectY 是宽高的比例
    intent.putExtra("aspectX", 1);
    intent.putExtra("aspectY", 1);
    //outputX outputY 是裁剪图片宽高
    intent.putExtra("outputX", 300);
    intent.putExtra("outputY", 300);
    intent.putExtra("return-data", false);//注意这里返回false,因为在部分手机上获取不到返回的数据
    intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(cropFile));
    intent.putExtra("outputFormat", Bitmap.CompressFormat.PNG.toString());
    intent.putExtra("noFaceDetection", true); //人脸识别
    intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    startActivityForResult(intent, CROP_REQUEST_CODE);
    }

第三部接收图片信息

在onActivityResult方法中获得返回的图片信息,在这里先调用剪裁去剪裁图片,然后对剪裁返回的图片进行设置、保存、上传等操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
switch (requestCode) {
//拍照后逻辑
case REQUEST_PERMISSION_CAMERA:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Uri contentUri = FileProvider.getUriForFile(mContext, getPackageName(), captureFile);
cropPhoto(contentUri);
} else {
cropPhoto(Uri.fromFile(captureFile));
}
break;
//图片获取后逻辑
case REQUEST_PERMISSION_WRITE:
cropPhoto(data.getData());
break;
//图片裁剪后逻辑
case CROP_REQUEST_CODE:
saveImage(cropFile.getAbsolutePath());
ivAvatar.setImageBitmap(BitmapFactory.decodeFile(cropFile.getAbsolutePath()));
break;
default:
break;
}
}
super.onActivityResult(requestCode, resultCode, data);
}

  • 保存图片在本地
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
    return null;
    }
    Bitmap bitmap = BitmapFactory.decodeFile(path);
    try {
    FileOutputStream fos = new FileOutputStream(cropFile);
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
    fos.flush();
    fos.close();
    return cropFile.getAbsolutePath();
    } catch (IOException e) {
    e.printStackTrace();
    }
    return null;
    }

⚠️在使用相机等动态权限时要注意权限的获取

参考:
[1] https://www.jianshu.com/p/0a2d8466fe6d
[2]7.0FileProvider使用 http://yifeng.studio/2017/05/03/android-7-0-compat-fileprovider/

kotlin笔记

发表于 2019-02-12 | 分类于 kotlin

基础

  1. 变量 & 常量:
    • 可变性: var 可变; val 不可变
    • 编译期常量: val定义的是运行时常量, 如果定义编译器常量, 需要添加 const
    • 变量的声明和赋值
      var p0 = 1 // 类型推断
      var p1: Int = 2 // 指明类型
  2. 基本类型

    • 数字(Number): (Kotlin中的数字没有隐式拓宽, 比如 Int 不能自动转换为 Long)
      • 类型
        • Byte 8 bit
        • Short 16bit
        • Int 32bit
        • Long 64bit 整数默认是 Int, 可以使用 L 表示 Long 123L
        • Float 32bit 浮点数默认为 Double, 可以使用 f/F表示 Float 1.1f
        • Double 64bit
      • 进制
        • 二进制: 0b00001011
        • 八进制: 不支持
        • 十进制: 123
        • 十六进制: 0x0F
      • 数字下划线: 提高数字常量的易读性
        val a = 1_000_000;
        val b = 123_456L
        var c = 0xFF_EC_DE
        var d = 0b11010010_01101001
      • 显示转换:
        • 显示转换: toByte()、toShort()、toInt()、toLong()、toFloat()、toDouble()、toChar()、、、、、
          val b: Byte = 1
          val i: Int = b.toInt()
        • 计算时自动转换
          val l = 1L + 3 // Long + Int => Long
    • 字符(Char):
      • 字符字面量用 单引号 表示 val c: Char = ‘a’
      • 特殊字符可以使用 \ 转义: \t、\n、\b、\r、\、\$、…
      • 字符不能直接作为数字使用, 但是可以通过方法显示的转为 Int
        fun decimalDigit(c: Char): Int{
        if(c !in '0'..'9')
            throw IllegalArgumentException("out of range")
        return c.toInt() - '0'.toInt
        
        }
    • 布尔(Boolean)
      • 两个取值: true, false
      • 内置的布尔运算符: ||, &&, !
    • 数组(Array)
      • 定义数组的方式
        var arr0 = arrayOfNulls() // 创建一个指定大小、元素都为空的数组
        var arr1 = arrayOf(1, 2, 3) // [1, 2, 3]
        val arr2 = Array(5, { i -> (i * i).toString() }) // 创建一个 Array 初始化为 [“0”, “1”, “4”, “9”, “16”]
      • 通过 [] 访问(取值 和 赋值)数组元素 (运算符重载, 实际调用 get set 方法)
      • 无装箱开销的原生类型数组: ByteArray、 ShortArray、IntArray …
        • 这些类和 Array 并没有继承关系,但是 它们有同样的方法属性集。它们也都有相应的工厂方法:
          val x: IntArray = intArrayOf(1, 2, 3)
          x[0] = x[1] + x[2]
    • 字符串(String)

      • 字符串的不可变性
      • 转义字符串: 用 “ “ 表示, 其内部可以使用 转义字符
        val s0: String = “Hello \n World”
      • 原始字符串: 用 “”” “”” 表示, 其内部可以包含换行和任意字符(都表示字面量), 不识别 转义字符
        val s1: String = “””

           for (c in "foo")
             print(c)
        """
        

        PS: 因为原始字符串内部任意字符($除外)都表示字面量, 所以其每一行前面可能都会带有空格, 可以使用 trimMargin() 去除每一行前面的空格

        - trimMargin() 默认参数 "|", 所以可以使用 | 作为每一行的前缀
            val text1 = """
                            |第一行
                            |第二行
                        """.trimMargin()
        - 也可以自定义前缀, 比如使用 >
            val text2 = """
                            >第一行
                            >第二行
                        """.trimMargin(">")
        
      • 字符串模板(转义字符串 和 原始字符串 都支持)
        val a = 1
        val s = “$a + $a = ${a + a}”
        PS: 因为原始字符串也支持 字符串模板, 但是不支持 转义符号, 所有如果要在原始字符串中表示 $ 字面量, 方式如下

        val price = """
                ${'$'}9.99
                """
        
      • 字符串 就是 一串字符
        • 用字符数组构建字符串:
          val s3: String = String(charArrayOf(‘a’,’b’,’c’))
        • 可以通过下标访问 字符串 中的 字符
          val c: Char = s3[0];
        • 可以使用 for 循环迭代字符串
          for (c in str) {
          println(c)
          
          }
  3. 同一性 & 相等性
    val a: Int = 1000;
    val a1: Int? = a;
    val a2: Int? = a;
    print(a1 === a2); // false, 可空引用会对变量装箱(不要和Java的装箱混淆), 不再相等
    print(a1 == a2); // true,
  4. 运算符
    • 基本运算符: + - * / % += -=
    • 比较符: == === != !== // === 比较地址, == 比较字面量
    • 逻辑运算符: || && !
    • 位运算(只用于 Int 和 Long)
      shl(bits) – 有符号左移 (Java 的 <<)
      shr(bits) – 有符号右移 (Java 的 >>)
      ushr(bits) – 无符号右移 (Java 的 >>>)
      and(bits) – 位与
      or(bits) – 位或
      xor(bits) – 位异或
      inv() – 位非
    • 自定义基本运算
      operator fun plus(xxx){xxx} // 要对某个类自定义运算符, 用 operator 修饰相关成员方法
      infix fun on(place: String){} // 可以通过 xxx on “aaa” 调用, 类似于运算符的效果, 其实等效于 xxx.on(“aaa”)
  5. 空类型 & 空安全
    • 可空类型
      var s: String? = null
    • 安全的调用
      val l1: Int? = s?.length // 如果 s 为 null , 则直接返回 null
      val l2: Int = s?.length ?: -1 // 如果 ?: 左侧表达式非空, 就返回其左侧表达式,否则返回右侧表达式。
      val l3: Int = s!!.length // 如果 s 为 null, 则抛出异常
    • 安全的类型转换
      val aInt: Int? = a as? Int // 如果转换失败, 则返回 null
    • 空条件
      data?.let{
      // 如果不为空执行该语句块
      
      }
      data?:let{
      // 当data为空时才会执行
      
      }
    • 可空类型的集合
      val nullableList: List = listOf(1, 2, null, 4)
      val intList: List = nullableList.filterNotNull() // 过滤非空元素
  6. 类型检查与类型转换
    • 智能类型转换: 检查类型后, 可以直接按照该类型使用
      if (obj is String) {
      print(obj.length)
      
      }
      if (obj !is String) { // 与 !(obj is String) 相同
      print("Not a String")
      
      } else {
      print(obj.length)
      
      }
    • 智能类型转换适用的情况
      • val 局部变量
      • val 属性(private 或 internal),或者该检查在声明属性的同一模块中执行.不适用于 open 的属性或者具有自定义 getter 的属性;
      • var 局部变量——如果变量在检查和使用之间没有修改、并且没有在会修改它的 lambda 中捕获;
    • 强制类型转换
      val x: String = y as String
      val x: String? = y as? String // 转换失败则返回 null
  7. 包
    • 源文件通常以包声明开头: 包名可不与文件路径一致; 源文件的内容都在包名所在空间(外部使用需要导包), 如果不声明包, 则文件内容都属于无名称的默认包
      package my.demo
    • 导入包
      import foo.Bar // 现在 Bar 可以不用限定符访问
      import foo.* // “foo”中的一切都可访问
      import bar.Bar as bBar // 名字冲突时, 使用别名 bBar 代表“bar.Bar”
    • import不仅限于导入类, 还可以导入: 顶层函数及属性、枚举常量、对象中声明的函数和属性
  8. 流程控制
    • 区间 (Ranges)
      for (i in 1..100) { … } // [1, 100]
      for (i in 1 until 100) { … } // [1, 100)
      for (x in 2..10 step 2) { … } // 步进
      for (x in 10 downTo 1) { … } // 倒序
      if (x in 1..10) { … } // 判断
    • 分支
      • if
        • 传统用法:
          var max = a
          if (a < b) max = b
        • 在 kotlin 中, if 可以作为表达式使用, 它每个分支的最后的表达式作为该分支的返回值
          val max = if (a > b) a else b
          val max = if (a > b) {
          print("Choose a")
          a
          
          } else {
          print("Choose b")
          b
          
          }
      • when: 增强版的 switch
        • 最简单的用法
          when (x) {
          1 -> print("x == 1")
          2 -> print("x == 2")
          else -> { // 注意这个块
              print("x is neither 1 nor 2")
          }
          
          }
        • 如果很多分支需要用相同的方式处理,则可以把多个分支条件放在一起,用逗号分隔:
          when (x) {
          0, 1 -> print("x == 0 or x == 1")
          else -> print("otherwise")
          
          }
        • 可以用任意表达式(而不只是常量)作为分支条件
          when (x) {
          parseInt(s) -> print("s encodes x")
          else -> print("s does not encode x")
          
          }
        • 也可以检测一个值在(in)或者不在(!in)一个区间或者集合中:
          when (x) {
          in 1..10 -> print("x is in the range")
          in validNumbers -> print("x is valid")
          !in 10..20 -> print("x is outside the range")
          else -> print("none of the above")
          
          }
        • 检测一个值是(is)或者不是(!is)一个特定类型的值
          val hasPrefix = when(x) { // 和if一样, 也可以作为表达式使用, 每个分支的最后的表达式作为该分支的返回值
          is String -> x.startsWith("prefix")
          else -> false
          
          }
        • when 也可以用来取代 if-else if链。 如果不提供参数,所有的分支条件都是简单的布尔表达式,而当一个分支的条件为真时则执行该分支:
          when {
          x.isOdd() -> print("x is odd")
          x.isEven() -> print("x is even")
          else -> print("x is funny")
          
          }
    • 异常
      • 普通用法
        • 抛异常:
          throw MyException(“Hi There!”)
        • 捕获异常
          try { }catch (e: SomeException) { }finally { }
      • 作为表达式使用, 每个分支中最后的表达式作为该分支的结果
        val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }
    • 循环
      • for
        • 循环 数组 或 list 的索引
          for (index in list.indices) {
          println("item at $index is ${items[index]}")
          
          }
        • 遍历 索引 和 值
          for ((index, value) in array.withIndex()) {
          println("the element at $index is $value")
          
          }
        • 可以对任何提供迭代器(iterator)的对象进行遍历
          for (item in collection) print(item)
        • 遍历 Map 集合
          for ((k, v) in map) {
          print("$k -> $v")
          
          }
        • 遍历 区间 (如上)
      • while & do … while
        while (x > 0) {
        x--
        
        }
        do {
        val y = retrieveData()
        
        } while (y != null) // y 在此处可见
  9. 返回与跳转: return、break、continue
    • 基本用法
      return 默认从最直接包围它的函数或者匿名函数返回
      break 终止最直接包围它的循环
      continue 继续下一次最直接包围它的循环
    • 使用标签来限制 break 和 continue
      loop@ for (i in 1..100) {
      for (j in 1..100) {
          if (……) break@loop
      }
      
      }
    • 使用标签来限制 reutrn : 函数可以嵌套使用, return会从最直接包围它的函数返回, 但是会忽略 lambda表达式, 可以通过标签使其从 lambda表达式返回
      fun foo() {
      ints.forEach lit@ {
          if (it == 0) return@lit
          print(it)
      }
      
      }
      fun foo() {
      ints.forEach {
          if (it == 0) return@forEach            // 隐式标签, 该标签与接受该 lambda 的函数同名
          print(it)
      }
      
      }
    • return 的标签语法也可以带有返回值
      return@a 1 // 从标签 @a 返回 1

类

  1. 类

    • 类的声明:
      • class关键字 + 类名 + 类头(主构造函数、参数) + 类体
        class Person public constructor(name: String){}
      • 如果没有类体, 可生路 {}:
        class Person public constructor(name: String)
      • 如果主构造函数没有 注解 或 权限修饰(默认 public), 可省略 constructor 关键字
        class Person()
      • 如果主构造函数没有参数, 类头也可以省略
        class Person
    • 构造函数
      • 构造函数中的参数可在 init 代码块中访问
        class Person public constructor(name: String){
        val age = 18                                 // 只读属性
        var xxx = "yyy"                              // 读写属性
        init{ print(name) }                          // 初始化代码块: 按照其在类中的顺序执行(包括属性初始化)
        
        }
      • 构造函数中的参数也可以在类属性的初始化器中使用
        class Person (name: String){
        val upperName : String = name.toUpperCase()
        
        }
      • 可以直接在主构造函数中声明 属性
        class Person (val name: String, val age: Int = 18, var xxx: String){ … }
      • 次级构造函数: 可在类体中通过 constructor 关键字 声明任意个次级构造函数
        class Person{
        constructor(name: String){
            print(name)
        }
        
        }
      • 如果存在主构造函数, 那么所有 次级构造函数必须 直接(或间接)委托给 主构造函数.
        class Person(val name: String, val age: Int){
        constructor(name: String) : this(name, 18) {
            print(name)
        }
        
        }
      • 实际上即使没有主构造函数, 次级构造函数任然隐式的存在这种委托关系
        class Person{
        init{
            print("这段代码会在次级构造函数体之前执行, 因为 init 代码块是主构造函数的一部分, 而委托代码是在次级构造函数的含数体之前")
        }
        constructor(name: String){
            print(name)
        }
        
        }
      • 如果没有声明任何构造函数(主和次), 默认会自动有一个 public 的无参主构造函数
    • 属性和字段

      • 声明属性的方式
        class A{
        // 可变属性: 初始化器、getter、setter都是可选的; 如果类型可推断(从初始化器或 getter返回值), 那么类型也可以省略
        var <propertyName> [: PropertyType] [= <property_initializer>]      
            [<getter>]                                                      // 就是一个函数 get() {}
            [<setter>]                                                      // set(value){}
        val <propertyName> [: PropertyType] [= <property_initializer>]      // 只读属性, 没有setter
            [<getter>]
        
        }
      • 改变访问器的可见性(或添加注解): 默认情况下 属性访问器的可见性和属性时一致的, 但是也可以单独指定
        var setterVisibility: String = “abc”
        private set                                   // 可以只添加修饰符, 而不改变其默认实现
        
        var setterWithAnnotation: Any? = null
        @Inject set
        
      • 幕后字段: 如果属性至少有一个访问器使用默认实现, 或者通过 field 引用幕后字段,将会为该属性生成一个幕后字段
        var counter = 0
        set(value){
            field = value
        }
        
      • 编译期常量: 使用 const 修饰, 位于顶层或是 object 的成员, 使用 String或原生类型初始化, 没有自定义 getter
      • 属性延迟初始化: 一般情况下属性必须通过 构造函数 或 初始化器 或 getter 初始化, 使用以下方式可以延迟初始化时机
        class Test{
        lateinit var a: String       // 方式一: 必须是可变属性, 必须是非空类型, 不能自定义 getter 和 setter
        var/val b by lazy{}          // 方式二:
        
        }
        -
    • 创建类的实例: 调用构造方法, 没有 new 关键字
      val p = Person()

  2. 继承
    • Any 是所有类的基类
    • 继承的写法
      • 如果基类有主构造函数, 则必须使用该主构造函数就地初始化
        class Derived(p: Int) : Base(p)
      • 如果基类没有主构造函数, 那么每个次构造函数必须使用 super 关键字初始化基类, 或者委托另一构造函数做到这一点
        class MyView : View {
        constructor(ctx: Context) : super(ctx)
        
        }
    • super 关键字
      • 调用基类的函数和属性访问器
        open class F{
        open fun test() { print("aaaaa") }
        open val x: Int get() = 1
        
        }
        class S: F(){
        override test(){
            super.test()
            print("bbbbbb")
        }
        override val x: Int get() = super.x + 1
        
        }
        -
      • 在内部类中, 通过外部类类名限定的 super, 访问外部类的超类
        class S: F(){
        ...
        inner class SI{
            fun test2(){
                super@S.test()
                print(super@S.x)
            }
        }
        
        }
  3. 修饰符号
    • open/final:
      • 类默认为 final, 如果希望被继承, 需要加上 open 修饰
      • 函数默认为 final, 如果希望被重写, 需加上 open 修饰, 且子类重写时必须加上 override修饰, 如果子类不希望再被重写, 加上 final
      • 属性和函数类似, 此外: val 可以重写为 var, var不能重写为 val
    • abstract: 可用于修饰 类 或 函数 (抽象类或函数肯定可以被继承和重写, 不需要再添加 open 修饰)
    • 可见性修饰符 (作用目标: 类、对象、接口、构造函数、方法、属性及其setter, getter可见性总是和属性本身一致)
      private(自己可见)、 protected(不适用顶层声明)、 internal(相同模块内可见) 和 public(缺省默认)
  4. 接口
    • 使用 interface 关键字, 多继承
    • 可以有抽象方法 和 实现方法(java8)
    • 可以有属性, 但必须为 抽象的 或者 提供访问器实现
      interface A{
      val a: Int
      val b: String
          get() = "bbb"                  // 接口中不支持 幕后字段
      
      }
    • 多继承与 覆盖冲突
      interface A{
      fun a(){}
      
      }
      interface B{
      fun a(){}
      
      }
      class C: A, B{
      override fun a() {                // 虽然 a 方法在父类中是实现方法, 但这里必须要重写, 且可以通过 super<父类名>选择性调用父类实现
          super<A>.a()
      }
      
      }
  5. 扩展
    • 定义扩展函数
      fun 类型名称.方法名(){ // 扩展函数体可通过 this 访问调用对象 }
    • 扩展函数式静态解析的: 并没有在目标类中插入新的成员, 仅仅表示可以通过该类型变量使用点语法调用函数
    • 如果扩展函数和成员方法冲突了, 那么成员方法总是优先的
    • 可空接受者
      fun Any?.toString(): String? = if(this == null){ null } else { this.toString() }
    • 扩展属性: 扩展属性不能使用幕后字段, 不能有初始化器, 只能有 访问器
      val List.lastIndex: Int
      get() = size - 1
      
    • 为伴生对象 定义扩展
      class A{
      companion object{}
      
      }
      fun A.Companion.test(){ … }
      A.test(); // 可直接通过类名访问
    • 定义扩展的位置
      • 一般情况下定义在顶层
      • 也可以在一个类(扩展分发者) 内部 为 另一个类(扩展接受者) 定义扩展
        • 在此扩展方法中可以同时访问 扩展分发者 以及 扩展接受者 的成员
        • 如果 扩展分发者 和 扩展接受者的成员名称冲突时, 默认访问的是 扩展分发者的成员, 但是也可以通过 this 的限定语法指定访问
          class A{
          fun a1(){}
          fun a2(){}
          
          }
          class B{
          fun a2(){}
          fun b(){}
          fun A.aa(){
              b();            // 调用了 B 的方法
              a1();           // 调用了 A 的方法
              a2();           // 调用了 B 的方法
              this@A.a2();     // 通过 this 的限定语法, 调用了 A 的方法
          }
          
          }
    • 扩展的作用: 代替 工具类
  6. 数据类
    • 定义数据类
      data class Uer(val name: String, val age: Int)
    • 数据类的要求
      • 必须有主构造函数, 且至少有一个参数, 且参数必须声明为属性(使用 var 或 val 标记)
      • 如果希望使用无参构造函数, 可以给属性设置默认值
      • 数据类不能是抽象、开放、密封或者内部的
    • 编译器会通过主构造函数为数据类自动生成以下函数 (没有收到声明, 且父类没有设置为 final)
      • equals()、 hashCode()
      • toString() // 格式是 “User(name=John, age=42)” …
      • componentN() // 按声明顺序对应于所有属性
      • copy() // 用于复制一个对象, 并只改变部分属性
        val jack = User(name = “Jack”, age = 1)
        val olderJack = jack.copy(age = 2)
    • 在类体中声明属性: 编译器自动生成的函数只会使用主构造函数中的属性, 而不会使用类体中声明的属性
    • 数据类的解构
      val jane = User(“Jane”, 35)
      val (name, age) = jane
  7. 内部类
    • 在类的内部定义的类
      class Outer{
      private val bar: Int = 1
      class Inner1{                // 表示静态内部类                     ->  val i1 = Outer.Inner2().foo()
          fun foo() = 2
      }
      inner class Inner2{          // 使用inner关键字标记, 表示非静态内部类 ->  val i2 = Outer().Inner2().foo()
          fun foo() = bar          // 可访问外部类成员, 并且可通过 this@Outer 引用外部类实例
      }
      
      }
    • 匿名内部类: 使用 对象表达式. // 如果是函数式java接口的实例, 可使用lambda chipLayout.setOnClickListener { v -> print(v?.id) }
      view.setOnClickListener(object: View.OnClickListener{
      override fun onClick(v: View?) { }
      
      })
  8. 密封类
    • 定义一个密封类
      sealed class XXX
    • 密封类的限制:
      • 不允许有非 private 的构造函数(其构造函数默认是 private), 其所有直接子类必须和其写在同一个文件中
      • 在使用 when表达式时, 如果各个分支条件可以覆盖所有情况, 那么就不需要 else 分支. 密封类可以满足这一点
  9. 枚举类
    • 最基本的用法: (每一个枚举常量都是对象)
      enum class Direction {
      NORTH, SOUTH, WEST, EAST
      
      }
    • 每一个枚举常量都是枚举类的实例
      enum class Color(val rgb: Int) {
      RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF)
      
      }
    • 枚举常量也可以声明自己的匿名类
      enum class ProtocolState {
      WAITING {
          override fun signal() = TALKING
      },
      TALKING {
          override fun signal() = WAITING
      };
      abstract fun signal(): ProtocolState
      
      }

对象

  1. 对象表达式 (匿名对象)
    • 使用 object 关键字表示匿名对象
      view.setOnClickListener(object: View.OnClickListener{
      override fun onClick(v: View?) { }
      
      })
    • 可以有多个超类型(逗号分隔), 如果超类型存在构造函数, 则必须传递参数
      val ab: A = object : A(1), B {
      // 重写方法 或 属性
      
      }
    • 甚至可以不指定超类型
      val adHoc = object {
      var x: Int = 0
      var y: Int = 0
      
      }
    • 使用匿名对象作为 共有函数 的返回值 或 参数时, 其实际类型是匿名对象的超类型(如果为指明, 则是 Any 类型)
    • 使用匿名对象作为 私有函数 的返回值 或 参数时, 其实际类型是匿名对象类型
  2. 对象声明 (不是表达式, 不能用在赋值语句的右边)
    • 使用 object 关键字 声明对象 (等效于 java 中的 饿汉式单例)
      object AppScope{
      fun saveData(key: String, value: String){ ... }
      
      }
    • 如何访问对象成员
      • kotlin中: AppScope.saveData(…)
      • java中: AppScope.INSTANCE.saveData(…)
    • 对象可以有超类型
      object MyLisener: MessageListener(){
      override fun ...
      
      }
  3. 伴生对象
    • 定义伴生对象: 类内部的object 用 companion 关键字标记, 一个类只能有一个伴生对象,
      class MyClass {
      companion object Factory {
          @JvmStatic   
          fun create(): MyClass = MyClass()
          @JvmField
          val TAG: String = "tag"
      }
      
      }
    • 访问伴生对象的成员
      • kotlin中: val instance = MyClass.create()
      • java中:
        • 如果没有添加 @JvmStatic 、@JvmField 注解: val instance = MyClass.Factory.create()
        • 如果添加了 @JvmStatic 、@JvmField 注解: val instance = MyClass.create()
    • 伴生对象的名称可以省略, 此时将使用默认的名称 Companion
      class MyClass {
      companion object {
          fun create(): MyClass = MyClass()
      }
      
      }
      MyClass.Companion.create()
    • 虽然访问伴生对象的成员类似 java中的静态成员, 但实际在运行时他们任然是真实对象的实例成员
    • 在 jvm平台, 如果使用了 @JvmStatic 、@JvmField 注解, 可以将伴生对象的成员生成为真正的静态成员
    • kotlin中使用静态成员时, 应考虑是否有必要, 是否用 包级函数/变量 替代
  4. 单例模式
    • 饿汉式:
      objcet APIHelper{}
    • 懒汉式:
      class SingletonDemo private constructor() {
      companion object {
          private var instance: SingletonDemo? = null
              get() {
                  if (field == null) {
                      field = SingletonDemo()
                  }
                  return field
              }
          // @Synchronized   添加该注解表示 同步
          fun get(): SingletonDemo{
              return instance!!
          }
      }
      
      }
    • 双重校验锁式
      class SingletonDemo private constructor() {
      companion object {
          val instance: SingletonDemo by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { SingletonDemo() }
      }
      
      }
    • 静态内部类式
      class SingletonDemo private constructor() {
      companion object {
          val instance = SingletonHolder.holder
      }
      private object SingletonHolder {
          val holder = SingletonDemo()
      }
      
      }
      • 带参 ?
        class SToast private constructor(val context: Context) : Toast(context) {
        private object Builder{
            var instance: SToast? = null
        }
        companion object{
            fun with(context: Context): SToast{
                Builder.instance?:let{
                    Builder.instance = SToast(context)
                }
                return Builder.instance!!
            }
        }
        
        }

代理(委托)

  1. kotlin中可以简单的实现代理模式 (静态代理)
    interface Animal{
    fun bark()
    
    }
    class Dog :Animal {
    override fun bark() {
        println("Wang Wang")
    }
    
    }
    class Cat(animal: Animal) : Animal by animal // 将 Cat 的所有公有成员都委托给指定的对象
    Cat(Dog()).bark() // 用 Dog 作为 Cat 的代理
  2. 委托属性
    • 属性委托的语法: val/var <属性名>: <类型> by <表达式>
      class Example {
      var p: String by Delegate()
      
      }
      class Delegate {
      // 必须有 getValue方法, 对应 getter,  参数: 属性的拥有者, 对属性的描述
      operator fun getValue(thisRef: Any?, property: KProperty<* >): String {
          return "$thisRef, thank you for delegating '${property.name}' to me!"
      }
      // var属性还要有 setValue方法, 对应 setter,  参数: 属性的拥有者, 对属性的描述, 属性的值
      operator fun setValue(thisRef: Any?, property: KProperty<* >, value: String) {
          println("$value has been assigned to '${property.name} in $thisRef.'")
      }
      
      }
    • kotlin为委托提供的几个工厂方法
      • 延迟属性 Lazy: lazy()接受一个 lambda表达式 并返回一个 Lazy实例(在第一次访问属性的getter时执行 lambda并返回结果)
        val propLazy: Int by lazy{1} // 默认是所有线程同步的
        val v: Int by lazy(LazyThreadSafetyMode.PUBLICATION, {1}) // 指定线程不安全
        val v: Int by lazy(LazyThreadSafetyMode.NONE, {1}) // 不会有任何线程安全的保证和相关的开销
      • 可观察属性 Observable: (接受两个参数: 初始值, 处理函数)
        • 在赋值之后执行
          var name: String by Delegates.observable(“初始值”) {
          // 被赋值的属性、旧值和新值
          prop, old, new ->
          println("$old -> $new")
          
          }
        • 在赋值之前执行(可以拦截)
          var name: String by Delegates.vetoable(“初始值”) {
          // 被赋值的属性、旧值和新值
          prop, old, new ->
          println("$old -> $new")
          false
          
          }
      • 把属性储存在映射中
        class User(val map: Map) {
        val name: String by map
        val age: Int     by map
        
        }
        val user = User(mapOf(
        "name" to "John Doe",
        "age"  to 25
        
        ))
        // var 属性
        class MutableUser(val map: MutableMap) {
        var name: String by map
        var age: Int     by map
        
        }

函数

  1. 定义一个函数
    fun double(x: Int): Int { return 2 * x }
  2. 关于参数
    • 参数的默认值: (可以减少重载数量, 注意: 重写带默认值的函数时, 需要省略默认值))
      fun read(b: Array, off: Int = 0, len: Int = b.size()) {}
    • 命名参数: 调用函数时, 可以显示的指定名称传参
      fun foo(p1: Int = 0, p2: Int){ … }
      foo(p2 = 10)
    • 可变参: 使用关键字 vararg (可变参可以不是最后一个, 但是对于其后面的参数, 需要使用 命名参数 的语法传参)
      fun asList(vararg ts: T): List {
      val result = ArrayList<T>()
      for (t in ts) // ts is an Array
          result.add(t)
      return result
      
      }
      // 对于可变参可以一个一个的传, 但如果已经有了一个数组, 可以使用 伸展符号( )
      val arr = arrayOf(1, 2, 3)
      val list = asList(-1, 0,
      arr, 4)
    • 泛型参数
      fun singletonList(item: T): List {}
  3. 关于返回值
    • 无返回值: 如果一个函数没有返回值, 那么其返回类型应该是 Unit, 或者直接省略返回类型
    • 单表达式函数: 当函数返回单个表达式时, 可省略 {}, 并使用 = 表示返回值
      fun double(x: Int): Int = x 2
      fun double(x: Int) = x
      2 // 当返回类型可以推断时, 可省略返回类型
  4. 中缀语法:
    • 使用 infix 关键字标识函数
      infix fun Int.sh1(x: Int): Int{ …}
      1 sh1 2 // 使用中缀语法调用函数, 等效于 1.sh1(2)
    • 中缀函数必须满足一下条件
      • 必须是成员函数 或 扩展函数
      • 必须且只有一个参数
      • 参数不能是 可变参, 且不能有默认值
  5. 函数的作用域
    • 成员函数
    • 顶层函数: kotlin中的函数可以直接声明在 顶层
    • 局部函数: kotlin中的函数可以声明在另一个函数内部 (闭包: 内部函数可访问外部函数中的变量)
  6. 尾递归函数用: 使用 tailrec 关键字标记, 并满足要求(递归调用必须是最后一步,并且不能用在 try/catch/finally 块中), 只有后端支持
    tailrec fun findFixPoint(x: Double = 1.0): Double = if (x == Math.cos(x)) x else findFixPoint(Math.cos(x))

高阶函数 & Lambda

  1. 函数类型
    • 如何声明函数类型
      val click : (View) -> Unit = …
    • 函数类型的一些说明
      • 如果没有返回值, Unit 也不能省略
      • 如何表示可空类型: val a: ((Int) -> String)? = …
      • 可选择性的设置参数名(提高可读性): val click : (view: View) -> Unit = …
      • 函数类型的取值
        • 使用匿名函数:
          val click : (View) -> Unit = fun (v: View){ print(v.id) }
        • 使用Lambda表达式:
          val click : (View) -> Unit = { v -> print(v.id) }
        • 使用已有函数的引用
          fun test(view: View){ print(view.id) }
          val click : (View) -> Unit = ::test
        • 使用函数类型的实现类的实例
          class Test : (View) -> Unit{
          override fun invoke(view: View) {
              print(view.id)
          }
          
          }
          val click : (View) -> Unit = Test()
      • 如果信息足够推断函数类型时, 可省略类型
        val plus = {i : Int -> i + 1}
      • 调用函数类型的方式
        • 方式一: 直接调用
          var result = plus(3)
        • 方式二: 使用 invoke
          var result = plus.invoke(3)
      • 带接受者的函数类型
        • 定义带接受者的函数类型(类似 扩展函数)
          val myConcat: String.(String) -> String = { string -> this + string } // this表示该函数的调用者
          val result = “abc”.myConcat(“efg”) // 用 “abc” 调用该函数, 等价于 myConcat(“abc”, “efg”)
        • 函数类型的接受者, 可以相互转化为其参数列表的第一个参数
          fun a(str1: String, str2: String) = str1 + str2
          val myConcat: String.(String) -> String = ::a
  2. Lambda表达式
    • 例子
      val map = mapOf(“key1” to “value1”, “key2” to “values”)
      map.forEach({key: String, value: String ->
      print("$key & $value")
      
      })
    • Lambda表达式写在 {} 中, 参数列表 与 函数体 之间用 -> 分开
    • 参数类型可省略
      map.forEach({key, value ->
      print("$key & $value")
      
      })
    • 对于没有使用的参数, 可以用 表示
      map.forEach({
      , value ->
      print(value)
      
      })
    • 如果Lambda表达式的参数列表只有一个参数, 那么可以省略参数列表 和 ->, 在函数体中如果要使用该参数, 可以用 it代替
      val list = listOf(1,2,3)
      list.forEach({ print(it) })
    • 如果Lambda表达式需要有返回值, 那么函数体中的最后一个表达式会作为其返回值
      val newList = list.map({ 2 it }) // 不要使用 return, 除非带上限定符号, 比如 return@map
      // val newList = list.map({ return@map 2
      it })
    • 如果 Lambda 是另一个函数的最后一个参数, 那么可以写在 () 之外
      val newList = list.map() { 2 * it }
    • 如果 Lambda 是另一个函数的唯一个参数, 那么 () 可省略
      val newList = list.map { 2 * it }
  3. 高阶函数: 使用函数作为 参数 或 返回值
  4. 内联函数
    • 使用高阶函数会造成额外的消耗(函数也是对象, 需要单独开辟空间, 寻址..), 使用内联函数可以解决这一问题
      inline fun lock(lock: Lock, body: () -> T): T {}
    • inline会影响函数本身和作为参数的 lambda表达式. 内联会导致生成的代码量增加, 可以通过 noinline 标识哪些参数不进行内联
      inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {}
    • lambda表达式中不允许使用单独的 return; 但是在内联情况下, 可以直接 return, 标识从包裹该lambda表达式最近的函数返回
    • 内联属性: inline 也可以用来修饰 没有幕后字段 的属性
      • 作用于访问器
        val foo: Foo
        inline get() = Foo()
        
        var bar: Bar
        get() = ……
        inline set(v) { …… }
        
      • 直接作用于整个属性, 表示所有访问器都标记为内联
        inline var bar: Bar
        get() = ……
        set(v) { …… }
        

其它

  1. 类型别名
    typealias NodeSet = Set // 为泛型起别名
    typealias AInner = A.Inner // 为内部类起别名
    typealias MyHandler = (Int, String, Any) -> Unit // 为函数起别名 (可以简化函数类型的书写)
    typealias Predicate = (T) -> Boolean
  2. 集合
    • 不可变集合: List . Set . Map
      val list: List = listOf(1,2,3)
      val set = setOf(‘a’,’b’)
      val map = mapOf(“key1” to 1, “key2” to 2)
    • 可变集合: MutableList . MutableSet . MutableMap
      val list: MutableList = mutableListOf(1,2,3)
      val set = mutableSetOf(‘a’,’b’)
      val map = mutableMapOf(“key1” to 1, “key2” to 2)
    • 访问 map 集合
      println(map[“key”])
      map[“key”] = value
    • 集合的复制以及可变性变化
      toList() toMutableList() …
  3. 解构
    • 对象解构
      val (name, age) = person // 对于不需要用到的变量, 可以使用 代替 val (name, ) = person
    • 在 lambda 表达式中解构
      • 示例
        map.mapValues { entry -> “${entry.value}!” }
        map.mapValues { (key, value) -> “$value!” }
      • 声明 两个参数 和 声明一个 解构来取代单个参数之间的区别:
        { a -> …… } // 一个参数
        { a, b -> …… } // 两个参数
        { (a, b) -> …… } // 一个解构
        { (a, b), c -> …… } // 一个解构对以及其他参数
      • 解构中未被使用的变量可以使用 代替
        map.mapValues { (
        , value) -> “$value!” }
      • 可以指定整个解构的参数的类型 或者 分别指定特定组件的类型:
        map.mapValues { ( , value): Map.Entry -> “$value!” }
        map.mapValues { (
        , value: String) -> “$value!” }
  4. 利用 with 调用一个对象实例的多个方法
    class Turtle {
    fun penDown()
    fun penUp()
    fun turn(degrees: Double)
    fun forward(pixels: Double)
    
    }
    with(Turtle()) {
    penDown()
    for(i in 1..4) {
    forward(100.0)
        turn(90.0)
    }
    penUp()
    
    }
  5. 泛型
    • 类泛型
      class Box(t: T) {
      var value = t
      
      }
      val box: Box = Box(1) // 如果泛型可以推断, 那么可以省略 val box = Box(1)
    • 函数泛型
      fun singletonList(item: T): List {} // 普通函数泛型
      fun T.basicToString() : String {} // 扩展函数泛型
      val l = singletonList(1) // 调用泛型函数需要在 <> 中指明类型
    • 泛型变异
      • Java中是在使用处型变
        • 协变: <? extends T> 可以使用父类型的地方, 就可以接受子类型; 只能取值, 不能写入, 称为 生产者
          public class Utils{
          public static void test(List<? extends View> list){
              // 只能从 list 中取值, 而不能写入
              View view = list.get(0);                 // 可以通过编译
              // list.add(new TextView(context));      // 不能通过编译
          }
          
          }
          List imgs = new ArrayList<>();
          Utils.test(imgs);
        • 逆变 <? super T> 可以使用子类型的地方, 就可以接受父类型; 可以写入, 取值都是 Object, 称为 消费者
          public class Utils{
          public static void test(List<? super ImageView> list){
              // 可以往 list 中写入, 但取值都是 Object
              list.add(new ImageView(context));
              Object object = list.get(0);
          }
          
          }
          List views = new ArrayList<>();
          Utils.test(views);
      • Kotlin中是在声明处型变
        • out: 类似 协变, 表示泛型只能被生产, 不能被消费(就是只能作为返回值, 不能作为参数)
          class Test{
          fun test(): T { ... }
          
          }
          val t: Test = Test() // 这是合法的, 因为泛型只能被读取, 不能写入 (安全)
        • in: 类似 逆变, 表示泛型只能被消费, 不能被生产 (就是只能作为参数, 不能作为返回值)
          class Test{
          fun test(t: T) { ... }
          
          }
          val t: Test = Test() // 这是合法的, 因为泛型只能被写入, 不能读取 (安全)
    • 泛型约束
      fun test(t: T){ } // 泛型约束为 Number子类及其子类
      test(1) // 可以通过编译
      test(“a”) // 不能通过编译
      fun test(t: T) where T : A, T: B{ } // 泛型约束为: 必须是 A的子类 也是 B的子类
  6. 注解
    • 元注解
      @Target 指定注解可作用的地方(类, 函数, 属性, 表达式)
      @Retention 指定注解被保留的时间长短(AnnotationRetention.SOURCE, ..CLASS, ..RUNTIME)
      @Repeatable 允许 在单个元素上多次使用相同的该注解
      @MustBeDocumented 指定 该注解是公有 API 的一部分,并且应该包含在 生成的 API 文档中显示的类或方法的签名中。
    • 利用元注解 自定义注解
      @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
      AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
      
      @Retention(AnnotationRetention.SOURCE)
      @MustBeDocumented
      annotation class Fancy
    • 注解可以有接受参数的构造函数
      annotation class Special(val why: String)
    • 使用注解
      @Special(“example”) class Foo {}
  7. 反射
    • 类引用
      • 通过类名获取
        val c = MyClass::class // 该引用是KClass类型, 要获得 Java 类引用, 需要在 KClass 实例上使用 .java 属性。
      • 通过对象获取
        val widget: Widget = ……
        widget::class
    • 函数的引用 (用于高阶函数, lamada 表达式也可以实现类似功能)
      • 获取函数的引用
        fun isOdd(x: Int) = x % 2 != 0
        listOf(1, 2, 3).filter(::isOdd)
      • 函数的引用可以存储到变量中
        val predicate: (Int) -> Boolean = ::isOdd
      • 在外部引用 类的成员函数(或构造函数) 需要指定接受者
        val isListEmpty: List.() -> Boolean = List::isEmpty // 函数类型一定要指明接受者, 因为无法自动推断出来
        val list = listOf(“1”)
        print(“isEmpty: ${isListEmpty(list)}”) // 可将调用者作为第一个参数传入
        print(“isEmpty: ${list.isListEmpty()}”) // 和上面是等价的
      • 引用需要接受者的函数时, 使用 类名 和 类实例 都可以
        class Test{
        fun double (p: Int) = 2 * p
        
        }
        val test = Test()
        val d1: Test.(Int) -> Int = Test::double // 使用类名限定
        val d2: (Int) -> Int = test::double // 使用类实例限定
        val result1 = d1(test, 3) // 需要将调用者作为第一个参数, 或者直接 test.d1(3)
        val result2 = d2(3) // 无需再设置调用者
      • 引用特定对象的函数
        val numberRegex = “\d+”.toRegex()
        println(numberRegex.matches(“29”)) // 输出“true”
        val isNumber = numberRegex::matches
        println(isNumber(“29”)) // 输出“true”
      • 构造函数引用
        ::类名 // 表示 零参数构造函数 的引用
    • 属性引用
      • 获取属性的引用
        var x = 1
        ::x.name // 获取属性名
        ::x.get() // 获取值
        ::x.set(3) // 对于 var 属性, 还可以赋值
      • 在外部引用类的属性
        class A(var p: Int)
        val a = A(3)
        A::p.set(a, 5)
        val value = A::p.get(a)
      • 引用类的扩展属性
        val String.lastChar: Char
        get() = this[length - 1]
        
        val str = “abc”
        val result = String::lastChar.get(str)
      • 引用特定对象的属性
        val prop = “abc”::length
        val l = prop.get()

协程

1.

  1. 通过 launch、 runBlocking 开启协程
    • 一个关于协程的小案例
      override fun onCreate(savedInstanceState: Bundle?){
      setContentView(R.layout.activity_main)
      launch(CommonPool) {                    // 在后台启动一个协程
          delay(1000L)                        // 延迟1s,  delay是一个特殊的 suspend fun, 不会锁死线程, 只会挂起协程, 只能在协程内使用
          Log.e(">>>", "World: @ ${Thread.currentThread().name}")
      }
      Log.e(">>>", "Hello: @ ${Thread.currentThread().name}")
      runBlocking{                            // 在当前线程启动协程, 会阻塞当前线程, 直到协程内部的代码运行完毕
          delay(1000L)  
      }
      
      }
    • 直接用 runBlocking 包裹整个函数 (泛型是指当前函数的返回值类型, 如果需要返回值, runBlocking 代码块的最后一个表达式会作为返回值)
      override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      launch(CommonPool) {
          delay(2000L)
          Log.e(">>>", "World: @ ${Thread.currentThread().name}")
      }
      Log.e(">>>", "Hello: @ ${Thread.currentThread().name}")
      delay(2100L)
      Log.e(">>>", "over: @ ${Thread.currentThread().name}")
      
      }
    • 等待协程运行完毕
      override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      val job: Job = launch(CommonPool) {                                  // 启动新的协程, 并通过变量保留其引用
          delay(2000L)
          Log.e(">>>", "World: @ ${Thread.currentThread().name}")
      }
      Log.e(">>>", "Hello: @ ${Thread.currentThread().name}")
      job.join()                                                           // 等待子协程结束后才会执行后面的代码
      Log.e(">>>", "over: @ ${Thread.currentThread().name}")
      
      }
  2. 协程的取消
    • 通过 lauch返回的 job, 调用 cancel() 可以取消协程
      override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      val job: Job = launch(CommonPool) {
          repeat(100){ i ->
              Log.e(">>>", "$i @ ${Thread.currentThread().name}")
              delay(1000L)
          }
      }
      delay(3100L)
      job.cancel()                                // 取消协程
      job.join()
      Log.e(">>>", "over: @ ${Thread.currentThread().name}")
      
      }
    • 如果协程一直在运行, 那么它就不会去检查取消标志, 结构就无法取消成功. 解决方式是 在代码中主动去检查取消标志
      override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      var time = System.currentTimeMillis()
      val job: Job = launch(CommonPool) {
          var i = 0
          while (isActive && i < 1000){                  // isActive是协程内部的一个属性,可以检查该协程是否被取消
              if(System.currentTimeMillis() >= time){
                  Log.e(">>>", "${i++} @ ${Thread.currentThread().name}")
                  time += 1000
              }
          }
      }
      delay(3100L)
      job.cancel()
      job.join()
      Log.e(">>>", "over: @ ${Thread.currentThread().name}")
      
      }
    • 协程取消时时候回抛出 CancellationException, 可以通过 try {} finally {} 处理
      override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      val job: Job = launch(CommonPool) {
          try {
              repeat(1000){ i ->
                  Log.e(">>>", "$i @ ${Thread.currentThread().name}")
                  delay(500)
              }
          }catch (e: Exception){                        // catch不是必须的, 这里只是验证确实抛出了异常
              Log.e(">>>", e.toString())
          }fianlly{ ... }
      }
      delay(3100L)
      job.cancel()
      job.join()
      Log.e(">>>", "over: @ ${Thread.currentThread().name}")
      
      }
    • 超时自动取消 (withTimeout): 在当前线程启动协程, 如果协程内的代码在指定时间内未执行完毕, 就抛出异常 TimeoutException
      override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      val result = withTimeout(3000){                                             
          repeat(10){ i ->
              Log.e(">>>", "$i: @ ${Thread.currentThread().name}")
              delay(1000)
          }
          "最后一行表达式就是结果"
      }
      Log.e(">>>", "结果等于: $result")
      
      }
    • 安全的超时自动取消(withTimeoutOrNull): 超时后不会抛出异常, 只是返回结果是 null
      override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      val result = withTimeoutOrNull(3000){
          repeat(1){ i ->
              Log.e(">>>", "$i: @ ${Thread.currentThread().name}")
              delay(1000)
          }
          "最后一行表达式就是结果"
      }
      Log.e(">>>", "结果等于: $result")
      
      }
  3. suspend fun : 由 suspend 修饰的方法, 和普通方法结构完全一样
    • suspend fun 只能在另一个 suspend fun 中调用
      override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      val job: Job = launch(CommonPool) { test() }
      Log.e(">>>", "Hello: @ ${Thread.currentThread().name}")
      job.join()
      Log.e(">>>", "over: @ ${Thread.currentThread().name}")
      
      }
      suspend fun test(){
      delay(2000L)
      Log.e(">>>", "World: @ ${Thread.currentThread().name}")
      
      }
    • 多个 suspend fun 的执行顺序
      • 默认是串行执行
        suspend fun one(): Int{ // 延迟 1s 后返回 1
        delay(1000L)
        return 1
        
        }
        suspend fun two(): Int{ // 延迟 1s 后返回 2
        delay(1000L)
        return 2
        
        }
        override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val time = measureTimeMillis {                   // 在当前线程启动协程, 会阻塞当前线程, 最后会返回该协程消耗的时长
            val one = one()
            val two = two()
            Log.e(">>>", "计算结果: ${one + two}")
        }
        Log.e(">>>", "耗费时长: $time")                   // 时间差不多是 2s
        
        }
      • async & await
        • 说明:
          async 的作用是在后台启动一个协程, 并返回一个 Deferred 对象, 它是Job的子类, 表示一个承诺(promise) 会在稍后返回一个结果
          await 是 Deferred 上的一个方法, 作用是获取最终的结果 (会挂起当前协程, 直到获取到结果才会执行后面的代码)
        • 示例:
          override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
          super.onCreate(savedInstanceState)
          setContentView(R.layout.activity_main)
          val time = measureTimeMillis {
              val one = async(CommonPool){ one() }     // 在后台启动一个新的协程
              val two = async(CommonPool){ two() }
              Log.e(">>>", "计算结果: ${one.await() + two.await()}   @ ${Thread.currentThread().name}")
          }
          Log.e(">>>", "耗费时长: $time")               // 因为是 并行执行的, 时间差不多缩短一半
          
          }
        • async 的惰性求值: async函数可以传入一个参数 CoroutineStart.LAZY, 开启惰性求值, 表示只有调用 awaite 或 start 时开会开启协程
          override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
          super.onCreate(savedInstanceState)
          setContentView(R.layout.activity_main)
          val time = measureTimeMillis {
              val one = async(CommonPool, CoroutineStart.LAZY){ one() }     
              val two = async(CommonPool, CoroutineStart.LAZY){ two() }
              Log.e(">>>", "计算结果: ${one.await() + two.await()}   @ ${Thread.currentThread().name}")
          }
          Log.e(">>>", "耗费时长: $time")               // 时间 又 差不多是 2s
          
          }
        • async 风格的函数
          fun oneAsync() = async(CommonPool){ // oneAsync 就是一个普通函数, 不过调用它时会异步开启一个协程
          one()   
          
          }
          fun twoAsync() = async(CommonPool){
          two()
          
          }
          override fun onCreate(savedInstanceState: Bundle?) { // 注意这里没有了 runBlocking
          super.onCreate(savedInstanceState)
          setContentView(R.layout.activity_main)
          val one = oneAsync()                                       // 可以在协程之外调用
          val two = twoAsync()
          runBlocking {
              Log.e(">>>", "计算结果: ${one.await() + two.await()}")  // 但是 await 还是必须要在协程中调用
          }
          
          }
  4. 协程的上下文: 协程总是在 CoroutineContext 类型的上下文中执行
    • 协程调度器: 决定协程将要在 哪个(或哪些)线程中执行
      override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      val jobs = arrayListOf<Job>()
      jobs += launch(Unconfined){           // 无限制, 会在当前线程开启协程
          Log.e(">>>", "Unconfined @ ${Thread.currentThread().name}")
      }
      jobs += launch(coroutineContext){     // 限制使用 父级上下文, 这里是 runBlocking的上下文(任何协程内都能获取 coroutineContext)
          Log.e(">>>", "coroutineContext @ ${Thread.currentThread().name}")
      }
      jobs += launch(CommonPool){           // 一个线程池 ForkJoinPool.commonPool
          Log.e(">>>", "CommonPool @ ${Thread.currentThread().name}")
      }
      jobs += launch(newSingleThreadContext("MyNewThread")){ // 开启新的线程, 实际项目中如果不再使用它, 需要通过 close 方法释放
          Log.e(">>>", "newSingleThreadContext @ ${Thread.currentThread().name}")
      }
      jobs.forEach { it.join() }
      
      }
    • 父子协程: 如果 协程B 使用了 协程A 的上下文, 那么 B 就是 A 的子协程
      • 父协程会等其所有子协程执行完毕后才会结束
      • 取消父协程, 它所有的子协程也会取消
  5. Channel: 用于在 协程间 发送 和 接收 数据
    • send & receive : 发送数据与接受 (都会将协挂起)
      override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      val channel = Channel<Int>()
      val job = launch(CommonPool){
          for (i in 1 .. 5)
              channel.send( i * i)           // 发送数据
          channel.close()                    // 发送结束
      }
      for (value in channel)                 // 迭代获取数据 (底层通过 receive) , 直到 channel 关闭
          Log.e(">>>", "$value")
      job.join()
      Log.e(">>>", "Over")
      
      }
    • 使用 produce & consumeEach 简化 channel 的生产和消费
      override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      val product = product()
      product.consumeEach { Log.e(">>>", "$it") }
      Log.e(">>>", "Over")
      
      }
      fun product() = produce(CommonPool){
      for (i in 1 .. 5) send( i * i)
      
      }
    • Channel缓冲区:
      • Channel 默认不开启缓冲, 它会等 send 和 receive都准备好之后再进行数据传输. 如果 send先调用, 也会挂起直到 receive被调用
      • 构建 Channel 是可以指定缓冲区大小. 设置缓冲的情况下, 允许在没有 receive的时候先发送几条数据, 直到填满缓冲区
        val channel = Channel(5)
  6. 多个协程更改共享数据
    • 数据安全问题: 多个协程同时对数据进行修改, 导致结果的未知性
      override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
      super.onCreate(savedInstanceState)
      var count = 0
      val jobs = List(1000){
          launch(CommonPool){
              count++
          }
      }
      jobs.forEach { it.join() }
      Log.e(">>>", "$count")                // 结果不可预知
      
      }
    • 解决方式
      • 方式一: 粗粒度的控制, 将所有协程 限制在 一个线程上
        override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
        super.onCreate(savedInstanceState)
        var count = 0
        val ctx = newSingleThreadContext("ctx1")
        val jobs = List(1000){
            launch(ctx){
                count++
            }
        }
        jobs.forEach { it.join() }
        Log.e(">>>", "$count")           // 结果是 1000
        
        }
      • 方式二: 细粒度的控制, 使用互斥锁 Mutex
        override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
        super.onCreate(savedInstanceState)
        var count = 0
        val mutex = Mutex()
        val jobs = List(1000){
            launch(CommonPool){
                mutex.withLock { count++ }        // 等价于  mutex.lock(); try{ count++ } finally{ mutex.unlock() }
            }
        }
        jobs.forEach { it.join() }
        Log.e(">>>", "$count")                    // 结果是 1000
        
        }
      • 方式三: 使用 Actor (就是一个协程, 其中可以保存数据, 并带有一个和其它协程通信的 channel )
        sealed class CounterMsg
        object IncCounter: CounterMsg()
        class GetCounter(val response: CompletableDeferred): CounterMsg()
        fun counterActor() = actor(CommonPool){
        var counter = 0
        for (msg in channel){                                      // 迭代收到的数据
            when(msg){
                is IncCounter -> counter++                         // 修改数据
                is GetCounter -> msg.response.complete(counter)    // 将结果返回
            }
        }
        
        }
        override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
        super.onCreate(savedInstanceState)
        val counter = counterActor()                          // 创建 Actor
        val jobs = List(1000){
            launch(CommonPool){
                counter.send(IncCounter)                      // 发送消息 通知修改数据
            }
        }
        jobs.forEach { it.join() }
        val res = CompletableDeferred<Int>()                  // 用于获取数据
        counter.send(GetCounter(res))                         // 发送消息 通知需要获取结果
        Log.e(">>>", "${res.await()}")                        // 结果是 1000
        counter.close()
        
        }
  7. 在 Android中使用协程
    • 添加依赖: implementation ‘org.jetbrains.kotlinx:kotlinx-coroutines-android:0.18’
    • 一个简单的案例
      override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      val job = launch(UI){                      // 使用 UI 上下文 开启协程
          for (i in 10 downTo 1){
              tv.text = "CountDown $i"           // 修改 TextView 的 文本
              delay(500L)                        // 延迟 500  (不会阻塞线程, 只会挂起协程)
          }
          tv.text = "Done"
      }
      bt.setOnClickListener { job.cancel() }     // 点击按钮 取消协程
      
      }
    • 通过UI上下文使用 actor
      override fun onCreate(savedInstanceState: Bundle?) = runBlocking{
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      bt.onClick {
          for (i in 10 downTo 1){
              tv.text = "Hellow: $i"
              delay(500)
          }
          tv.text = "Done!!!"
      }
      
      }
      fun View.onClick(action: suspend () -> Unit){
      val eventActor = actor<Unit>(UI){
          for (event in channel) action()
      }
      setOnClickListener{
          // offer方法会尝试给 actor传递数据, 如果此时actor正在处理其它数据, 则会根据情况做不同处理
              // 通过 actor<Unit>(UI) 创建的 actor                   ->  直接将新的数据丢弃
              // 通过 actor<Unit>(UI, Channel.CONFLATED) 创建的数据   ->  保留最新的那条数据, 等 actor空闲后再传入
              // 通过 actor<Unit>(UI, Channel.UNLIMITED) 创建的数据   ->  将所有新数据依次保存, 等 actor空闲后再依次传入
          eventActor.offer(Unit)
      }
      
      }
    • 使用协程的一些建议
      • 为了便于在页面销毁时统一释放资源, 建议将页面中的所有协程用一个 Job管理
        interface JobHolder{
        val job: Job
        
        }
        class MainActivity : AppCompatActivity(), JobHolder {
        override val job: Job = Job()                           // 每个页面都持有一个顶层 job
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            launch(job + UI){ ... }                             // 开启协程时, 这样传递上下文
        }
        override fun onDestroy() {
            super.onDestroy()
            job.cancel()                                         // 页面销毁时取消
        }
        
        }
      • 假如需要在自定义View中获取 Job, 可以通过扩展属性实现
        val View.contextJob: Job
        get() = (context as? JobHolder)?.job ?: NonCancellable
        

Eventbus使用及源码分析

发表于 2019-02-12 | 分类于 java

EventBus基础

何为EventBus

EventBus是Android和Java的publish/subscribe事件总线。

EventBus框架

对于EventBus除了greenrobot的EventBus,实际上还有Google出品的Guava以及square的otto(修改自Guava),
Guava是一个庞大的库,EventBus只是它附带的一个小功能,实际项目使用较少。用的最多的是greenrobot
的EventBus,该库优点是接口简洁、集成方便。

EventBus基本使用

本文主要讨论greenrobot的EventBus(3.1.1)库。

EventBus中的三个主要角色

  1. Event:事件,它可以是任意类型,EventBus会根据事件类型进行全局的通知;
  2. Subscribe:事件订阅者,而在3.0之后事件处理的方法名可以随意取,不过需要加上注解@subscribe,
    并且指定线程模型,默认是POSTING。
  3. Publisher:事件的发布者,可以在任意线程里发布事件。一般情况下,使用 EventBus.getDefault()
    就可以得到一个EventBus对象,然后再调用 post(Object) 方法发布事件即可。
    各角色协作流程如下:
    角色协作

    EventBus的5种线程模型,分别是:

  4. POSTING :订阅者将直接在同一个线程中调用,该线程将发布该事件,这是默认值。事件传递意
    味着开销最小,因为它完全避免了线程切换。 因此,这是已知在很短的时间内完成而不需要主线程的简单任
    务的推荐模式。 使用此模式的事件处理程序必须快速返回以避免阻止发布线程,这可能是主线程;
  5. MAIN:在Android上,将在Android的主线程(UI线程)中调用订阅者。 如果发布线程是主线程,
    订阅者方法将直接调用,阻止发布线程。 否则事件排队等待交付(非阻塞)。 使用此模式的订户必须快速返
    回以避免阻止主线程。如果不在Android上,则行为与{@link #POSTING}相同;
  6. MAIN_ORDERED:在Android上,将在Android的主线程(UI线程)中调用订阅者。 与{@link
    #MAIN}不同,该事件将始终排队等待传递。 这可确保后置调用是非阻塞的;
  7. BACKGROUND:在Android上,将在后台线程中调用订阅者。 如果发布线程不是主线程,则将在发
    布线程中直接调用订阅者方法。 如果发布线程是主线程,则EventBus使用单个后台线程,该线程将按顺序传
    递其所有事件。 使用此模式的订阅者应尝试快速返回以避免阻止后台线程。 如果不在Android上,则始终使
    用后台线程。
  8. ASYNC: 订阅者将在单独的线程中调用。 这始终独立于发布线程和主线程。 发布事件永远不会等
    待使用此模式的订阅者方法。 订阅者方法如果执行可能需要一些时间,则应使用此模式,例如 用于网络访问。
    避免同时触发大量长时间运行的异步订阅者方法以限制并发线程数。 EventBus使用线程池从已完成的异步订
    阅者通知中有效地重用线程;

具体使用

  1. 在项目中引入依赖

    1
    implementation 'org.greenrobot:eventbus:3.1.1'
  2. 订阅/解除订阅

    • 订阅

      1
      EventBus.getDefault().register(this);//订阅
    • 解除订阅

      1
      EventBus.getDefault().unregister(this);//解除订阅
  3. 发布事件

1
2
//事件是POJO(普通旧Java对象),没有任何特定要求。如此处的BeanObject。
EventBus.getDefault().post(new BeanObject());
  1. 订阅事件处理

    1
    2
    3
    4
    5
    //BeanObject为发布的对象,这个要对应发布的对象(或称事件)
    @Subscribe(threadMode = ThreadMode.MAIN) //在ui线程执行
    public void onDataSynEvent(BeanObject event) {
    Log.e(TAG, "event---->");
    }
  2. 订阅事件的优先级
    事件的优先级类似广播的优先级,优先级越高优先获得消息

    1
    2
    3
    4
    @Subscribe(threadMode = ThreadMode.MAIN,priority = 100) //在ui线程执行 优先级100
    public void onDataSynEvent(BeanObject event) {
    Log.e(TAG, "event---->");
    }
  3. 中止事件往下传递
    发送有序广播可以终止广播的继续往下传递,EventBus也实现了此功能

    1
    EventBus.getDefault().cancelEventDelivery(event) ;//优先级高的订阅者可以终止事件往下传递
  4. 代码混淆

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    -keepattributes *Annotation*
    -keepclassmembers class ** {
    @org.greenrobot.eventbus.Subscribe <methods>;
    }
    -keep enum org.greenrobot.eventbus.ThreadMode { *; }

    # Only required if you use AsyncExecutor
    -keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
    <init>(java.lang.Throwable);
    }

EventBus黏性事件

EventBus除了普通事件也支持粘性事件,这个有点类似广播分类中的粘性广播。本身粘性广播用的就比较少,为了方便理解成订阅在发布事件之后,但同样可以收到事件。订阅/解除订阅和普通事件一样,但是处理订阅函数有所不同,需要注解中添加sticky = true

1
2
3
4
@Subscribe(threadMode = ThreadMode.MAIN,sticky = true) //在ui线程执行
public void onDataSynEvent(BeanObject event) {
Log.e(TAG, "event---->");
}

  • 发送粘性事件

    1
    EventBus.getDefault().postSticky(new BeanObject());
  • 取消粘性事件
    对于 粘性广播其属于常驻广播(事件一直会保留,除非用户主动移除,且只要有新的订阅者订阅就会把这个事件发送给新的订阅者) ,对于EventBus粘性事件也类似,因此如果不再需要该粘性事件时可以移除

    1
    2
    //⚠️一般在订阅者消费过该事件后调用,其还有一个参数为class的方法,取消class这类事件
    EventBus.getDefault().removeStickyEvent(new DataSynEvent());

或者移除所有粘性事件

1
EventBus.getDefault().removeAllStickyEvents();

EventBus processor使用

EventBus提供了一个EventBusAnnotationProcessor注解处理器来在编译期通过读取@Subscribe()注解并解析,
处理其中所包含的信息,然后生成java类来保存所有订阅者关于订阅的信息,这样就 比在运行时使用反射来获得这些订阅者的
信息速度要快
.

  1. 具体使用:在模块下的build.gradle文件中加入(以下针对Android Gradle Plugin version 2.2.0 及更高版本):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    android {
    defaultConfig {
    javaCompileOptions {
    annotationProcessorOptions {
    arguments = [ eventBusIndex : '项目包名.MyEventBusIndex' ] //参数设置
    }
    }
    }
    }

    dependencies {
    implementation 'org.greenrobot:eventbus:3.1.1'
    annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.1.1' //注解
    }
  2. 使用索引

此时编译一次,自动生成生成索引类。在\build\generated\source\apt\debug(或release)\PakageName\下看到通过注解分析生成的索引类,这样便可以在初始化EventBus时应用已经生成的索引了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/** This class is generated by EventBus, do not edit. */
public class MyEventBusIndex implements SubscriberInfoIndex {
private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;

static {
SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();

putIndex(new SimpleSubscriberInfo(com.mugwort.lockscreen.ui.BaseActivity.class, true,
new SubscriberMethodInfo[] {
new SubscriberMethodInfo("doEventMessage", com.mugwort.lockscreen.entities.EventMessage.class,
ThreadMode.MAIN),
}));

}

private static void putIndex(SubscriberInfo info) {
SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);
}

@Override
public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {
SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
if (info != null) {
return info;
} else {
return null;
}
}
}

添加索引到EventBus默认的单例中

1
EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();

  1. 添加后注册效率会得到很大提升。

EventBus优缺点

  • 优点:简化组件之间的通信方式,实现解耦让业务代码更加简洁,可以动态设置事件处理线程以及优先级
  • 缺点:目前发现唯一的缺点就是类似之前策略模式一样的诟病,每个事件都必须自定义一个事件类,造成事件类太多,无形中加大了维护成本

参考:
[0]. 官网文档 http://greenrobot.org/eventbus/documentation/
[1]. https://www.cnblogs.com/whoislcj/p/5595714.html
[2]. https://juejin.im/post/5b6859706fb9a04fbc2218d2

EventBus源码解析

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private static final EventBusBuilder DEFAULT_BUILDER = new EventBusBuilder();

/** Convenience singleton for apps using a process-wide EventBus instance. */
/** 通过使用一个进程范围的实例生成一个便捷单例*/
public static EventBus getDefault() {
if (defaultInstance == null) {
synchronized (EventBus.class) {
if (defaultInstance == null) {
defaultInstance = new EventBus();
}
}
}
return defaultInstance;
}
/**
* Creates a new EventBus instance; each instance is a separate scope in which events are delivered. To use a
* central bus, consider {@link #getDefault()}.
*/
public EventBus() {
this(DEFAULT_BUILDER);
}

EventBus(EventBusBuilder builder) {
logger = builder.getLogger();
subscriptionsByEventType = new HashMap<>();
typesBySubscriber = new HashMap<>();
stickyEvents = new ConcurrentHashMap<>();
mainThreadSupport = builder.getMainThreadSupport();
mainThreadPoster = mainThreadSupport != null ? mainThreadSupport.createPoster(this) : null;
backgroundPoster = new BackgroundPoster(this);
asyncPoster = new AsyncPoster(this);
indexCount = builder.subscriberInfoIndexes != null ? builder.subscriberInfoIndexes.size() : 0;
subscriberMethodFinder = new SubscriberMethodFinder(builder.subscriberInfoIndexes,
builder.strictMethodVerification, builder.ignoreGeneratedIndex);
logSubscriberExceptions = builder.logSubscriberExceptions;
logNoSubscriberMessages = builder.logNoSubscriberMessages;
sendSubscriberExceptionEvent = builder.sendSubscriberExceptionEvent;
sendNoSubscriberEvent = builder.sendNoSubscriberEvent;
throwSubscriberException = builder.throwSubscriberException;
eventInheritance = builder.eventInheritance;
executorService = builder.executorService;
}

EventBus 初始化的三个步骤,直观上看用到 单例模式和Builder模式 ,将构造参数给分离了出来,实际上还用到了 策略模式 ,其中 Builder 中有些参数用于代码执行的策略,即传的参数不一样,执行的方式也就不一样,像 ignoreGeneratedIndex 作用就是让 EventBus 如何查找出订阅方法的策略。这些布尔类型的参数,在分析代码中可以逐步的了解到,先了解一些缓存对象,以更容易的了解源码:

  • subscriptionsByEventType: 内部是一个Map集合,可以根据 EventType 查找订阅事件。
  • typesBySubscriber: 根据订阅对象找到 EventType。
  • stickyEvents: 粘性事件的并发缓存。
  • 事件投递者 : mainThreadPoster, backgroundPoster, asyncPoster 根据订阅注解 ThreadMode 去选择不同的投递者,不同投递者投递事件,接收函数会执行在不同的线程中。
  • subscriberMethodFinder:查找方法用的,内部维护了一个订阅方法的集合。

注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Registers the given subscriber to receive events. Subscribers must call {@link #unregister(Object)} once they
* are no longer interested in receiving events.
* <p/>
* Subscribers have event handling methods that must be annotated by {@link Subscribe}.
* The {@link Subscribe} annotation also allows configuration like {@link
* ThreadMode} and priority.
*/
public void register(Object subscriber) {
Class<?> subscriberClass = subscriber.getClass();
List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}

调用 register(this) 的时候就把订阅者给传了进来,代码量很少,主要就两个步骤,第一个 findSubscriberMethods 找出一个 SubscriberMethod 的集合,然后就遍历 SubscriberMethod 去订阅事件,先看看 findSubscriberMethods() 里面到底做了什么,返回的是什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
if (subscriberMethods != null) {
return subscriberMethods;
}

if (ignoreGeneratedIndex) {
subscriberMethods = findUsingReflection(subscriberClass);
} else {
subscriberMethods = findUsingInfo(subscriberClass);
}
if (subscriberMethods.isEmpty()) {
throw new EventBusException("Subscriber " + subscriberClass
+ " and its super classes have no public methods with the @Subscribe annotation");
} else {
METHOD_CACHE.put(subscriberClass, subscriberMethods);
return subscriberMethods;
}
}

这里是通过 订阅者的类文件 查找里面对事件处理的订阅方法即添加了注解 @Subscribe 的方法。首先从缓存中查找(缓存的key是类文件class),如果找到了就立马返回。如果缓存中没有的话,则根据 ignoreGeneratedIndex 选择如何查找订阅方法,最后,找到订阅方法后,放入缓存,以免下次继续查找。ignoreGeneratedIndex 默认就是 false ,执行 findUsingInfo() 方法,但是这里先分析 findUsingReflection() ,因为默认配置的情况下还是会执行上面的 findUsingReflection(),就是通过反射来解析注解。

1
2
3
4
5
6
7
8
9
private List<SubscriberMethod> findUsingReflection(Class<?> subscriberClass) {
FindState findState = prepareFindState(); // 1
findState.initForSubscriber(subscriberClass);
while (findState.clazz != null) {
findUsingReflectionInSingleClass(findState); // 2
findState.moveToSuperclass();
}
return getMethodsAndRelease(findState);
}

在真正执行反射查找方法时,会传递一个 FindState,跟进 prepareFindState() 看下:

1
2
3
4
5
6
7
8
9
10
11
12
private FindState prepareFindState() {
synchronized (FIND_STATE_POOL) {
for (int i = 0; i < POOL_SIZE; i++) {
FindState state = FIND_STATE_POOL[i];
if (state != null) {
FIND_STATE_POOL[i] = null;
return state;
}
}
}
return new FindState();
}

这里第一步从池中拿出一个 FindState 对象,FindState 中维护的是对订阅方法查找结果的封装。其实,往后面会发现作者这里设计的非常精妙。第二步,initForSubscriber() 就是将订阅者传给 FindState 对象。第三步做的就是不断从订阅者和订阅者的父类去查找订阅方法,一起看 findUsingReflectionInSingleClass():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void findUsingReflectionInSingleClass(FindState findState) {
Method[] methods;
try {
// This is faster than getMethods, especially when subscribers are fat classes like Activities
methods = findState.clazz.getDeclaredMethods();
} catch (Throwable th) {
// Workaround for java.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149
methods = findState.clazz.getMethods();
findState.skipSuperClasses = true;
}
for (Method method : methods) {
int modifiers = method.getModifiers();
if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 1) {
Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
if (subscribeAnnotation != null) {
Class<?> eventType = parameterTypes[0];
if (findState.checkAdd(method, eventType)) {
ThreadMode threadMode = subscribeAnnotation.threadMode();
findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
}
}
} else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
String methodName = method.getDeclaringClass().getName() + "." + method.getName();
throw new EventBusException("@Subscribe method " + methodName +
"must have exactly 1 parameter but has " + parameterTypes.length);
}
} else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
String methodName = method.getDeclaringClass().getName() + "." + method.getName();
throw new EventBusException(methodName +
" is a illegal @Subscribe method: must be public, non-static, and non-abstract");
}
}
}

这里才是真正的查找订阅者下的订阅方法。通过对订阅者方法的遍历,看有没有注解,有的话就解析注解,然后将找到的订阅方法的集合封装到 FindState 对象中的 subscriberMethods 集合中。解析完了之后,在看 findUsingReflection() 方法的最后,返回了 getMethodsAndRelease(FindState),将 FindState 传给了 getMethodsAndRelease(FindState) 方法,跟进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
private List<SubscriberMethod> getMethodsAndRelease(FindState findState) {
List<SubscriberMethod> subscriberMethods = new ArrayList<>(findState.subscriberMethods);
findState.recycle();
synchronized (FIND_STATE_POOL) {
for (int i = 0; i < POOL_SIZE; i++) {
if (FIND_STATE_POOL[i] == null) {
FIND_STATE_POOL[i] = findState;
break;
}
}
}
return subscriberMethods;
}

从这里,可以知道作者设计 FindState池 的初心了,解析完了之后,将订阅方法赋给List集合,再回收 FindState ,继续接收解析,内存没有半点浪费。最后返回的是一个订阅方法的集合。这样,通过反射解析注解,找到订阅方法的方式已经分析完了。再看看通过apt处理器来找,apt处理是针对源码的处理,是执行在编译过程中的。所以性能要比反射好的多,也是推荐大家使用的方式。回到 findUsingInfo(),方法在没有配置时还是使用反射呢,一起看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
FindState findState = prepareFindState();
//将订阅类交给findState
findState.initForSubscriber(subscriberClass);
//扫描完当前类和其父类的订阅方法
while (findState.clazz != null) {
findState.subscriberInfo = getSubscriberInfo(findState);
if (findState.subscriberInfo != null) {
SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
for (SubscriberMethod subscriberMethod : array) {
//根据eventType对SubscriberMethod检查
//如果有这个类型的方法,或者有这个方法类型的子类就返回false
if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
findState.subscriberMethods.add(subscriberMethod);
}
}
} else {
//没有EventBusIndex的信息时使用反射
findUsingReflectionInSingleClass(findState);
}
//上移到父类
findState.moveToSuperclass();
}
//置空FindState池
return getMethodsAndRelease(findState);
}

上面查找订阅方法,和通过反射查找基本一致,主要看看 getSubscriberInfo(findstate) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private SubscriberInfo getSubscriberInfo(FindState findState) {
//查找是否有订阅信息,无则直接放回
if (findState.subscriberInfo != null && findState.subscriberInfo.getSuperSubscriberInfo() != null) {
SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo();
if (findState.clazz == superclassInfo.getSubscriberClass()) {
return superclassInfo;
}
}
if (subscriberInfoIndexes != null) {
for (SubscriberInfoIndex index : subscriberInfoIndexes) {
SubscriberInfo info = index.getSubscriberInfo(findState.clazz);
if (info != null) {
return info;
}
}
}
return null;
}

上述代码主要针对缓存,从缓存中获取订阅信息, 当使用apt处理时从 subscriberInfoIndexes 中
查找订阅信息的。而subscriberInfoIndexes是从 EventBus Builder 对象中获取:

1
2
3
4
5
6
SubscriberMethodFinder(List<SubscriberInfoIndex> subscriberInfoIndexes, boolean strictMethodVerification,
boolean ignoreGeneratedIndex) {
this.subscriberInfoIndexes = subscriberInfoIndexes;
this.strictMethodVerification = strictMethodVerification;
this.ignoreGeneratedIndex = ignoreGeneratedIndex;
}

其中 subscriberInfoIndexes 默认为空,结合之前的代码,还是执行了 findUsingReflection() 方法,
那么其何时才得到赋值。要使 subscriberInfoIndexes 得到赋值需要引入 EventBusAnnotationProcessor 库。这个在 EventBus processor使用 片段已经做了介绍。

继续分析 findUsingInfo() 方法,需要注意:

1
2
3
4
5
6
7
for (SubscriberMethod subscriberMethod : array) {
//根据eventType对SubscriberMethod检查
//如果有这个类型的方法,或者有这个方法类型的子类就返回false
if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
findState.subscriberMethods.add(subscriberMethod);
}
}

这里添加订阅方法的时候做了各检查,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
boolean checkAdd(Method method, Class<?> eventType) {
// 2 level check: 1st level with event type only (fast), 2nd level with complete signature when required.
// Usually a subscriber doesn't have methods listening to the same event type.
Object existing = anyMethodByEventType.put(eventType, method);
if (existing == null) {
return true;
} else {
if (existing instanceof Method) {
//只有子类中没有发现这种类型的方法才返回true
if (!checkAddWithMethodSignature((Method) existing, eventType)) {
// Paranoia check
throw new IllegalStateException();
}
// Put any non-Method object to "consume" the existing Method
anyMethodByEventType.put(eventType, this);
}
return checkAddWithMethodSignature(method, eventType);
}
}

这里做了两步检查,第一步类型检查,第二步签名检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private boolean checkAddWithMethodSignature(Method method, Class<?> eventType) {
methodKeyBuilder.setLength(0);
methodKeyBuilder.append(method.getName());
methodKeyBuilder.append('>').append(eventType.getName());
//使用方法名称和事件类型做key,保持方法
String methodKey = methodKeyBuilder.toString();
Class<?> methodClass = method.getDeclaringClass();
Class<?> methodClassOld = subscriberClassByMethodKey.put(methodKey, methodClass);
//如果传递过来的methodclass为父类,则直接返回
if (methodClassOld == null || methodClassOld.isAssignableFrom(methodClass)) {
// Only add if not already found in a sub class
return true;
} else {
// Revert the put, old class is further down the class hierarchy
subscriberClassByMethodKey.put(methodKey, methodClassOld);
return false;
}
}

其实作者,这里又做了一个优化,将方法名和事件类型当作key,来保存方法,将传来的方法类型和我们签名的保存的比较,如果保存的是父类,就返回 true,如果是子类,就将传来的方法保存起来,返回 false。这样做的意图是,如果有父类的方法了,就没有必要添加子类的方法了,因为继承会执行到的。至此查找订阅方法的过程已经完全分析完了。看懂了之后,非常的过瘾。无论哪种方式查找,都返回了 SubscriberMethod 对象,我们看看它维护了什么属性:

1
2
3
4
5
6
7
8
9
/** Used internally by EventBus and generated subscriber indexes. */
public class SubscriberMethod {
final Method method;
final ThreadMode threadMode;
final Class<?> eventType;
final int priority;
final boolean sticky;
/** Used for efficient comparison */
String methodString;

订阅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Must be called in synchronized block
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
Class<?> eventType = subscriberMethod.eventType;
//根据订阅者和订阅方法构造一个订阅事件
Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
//根据eventType查找Subscription集合
CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
//查看是否缓存过该订阅事件,没有则放进缓存
if (subscriptions == null) {
subscriptions = new CopyOnWriteArrayList<>();
subscriptionsByEventType.put(eventType, subscriptions);
} else {
if (subscriptions.contains(newSubscription)) {
throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
+ eventType);
}
}
//遍历订阅事件,找到比subsecriptions中订阅事件的位置,然后根据priority插进队列
int size = subscriptions.size();
for (int i = 0; i <= size; i++) {
if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
subscriptions.add(i, newSubscription);
break;
}
}
//根据订阅者查找Eventype的缓存
List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
if (subscribedEvents == null) {
subscribedEvents = new ArrayList<>();
typesBySubscriber.put(subscriber, subscribedEvents);
}
subscribedEvents.add(eventType);
//如果是粘性事件。立马处理
if (subscriberMethod.sticky) {
if (eventInheritance) {
// Existing sticky events of all subclasses of eventType have to be considered.
// Note: Iterating over all events may be inefficient with lots of sticky events,
// thus data structure should be changed to allow a more efficient lookup
// (e.g. an additional map storing sub classes of super classes: Class -> List<Class>).
Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
for (Map.Entry<Class<?>, Object> entry : entries) {
Class<?> candidateEventType = entry.getKey();
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = entry.getValue();
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object stickyEvent = stickyEvents.get(eventType);
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
}

其实里面就做了两件事,将订阅方法和订阅者,封装到 subscriptionsByEventType 和 ypesBySubscriber ,至于这两个对象是干什么的呢?第一个是投递订阅事件的时候,就是根据 EventType 找到订阅事件,从而去分发事件,处理事件的;第二个是在调用 unregister(this) 的时候,根据订阅者找到 EventType,又根据 EventType 找到订阅事件,从而解绑用的。第二件事,就是如果是粘性事件的话,就立马投递、执行。

发布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** Posts the given event to the event bus. */
public void post(Object event) {
//每个线程维护一个投递状态
PostingThreadState postingState = currentPostingThreadState.get();
List<Object> eventQueue = postingState.eventQueue;
eventQueue.add(event);

if (!postingState.isPosting) {
postingState.isMainThread = isMainThread();
postingState.isPosting = true;
if (postingState.canceled) {
throw new EventBusException("Internal error. Abort state was not reset");
}
try {
while (!eventQueue.isEmpty()) {
postSingleEvent(eventQueue.remove(0), postingState);
}
} finally {
postingState.isPosting = false;
postingState.isMainThread = false;
}
}
}

post(Object object) 的方法的时候就执行了上面的代码,PostingThreadState 是维护了投递的状态,最后循环投递,直到 PostingThreadState 中的 EventQueue 为空。那么代码最终执行到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
switch (subscription.subscriberMethod.threadMode) {
case POSTING:
invokeSubscriber(subscription, event);
break;
case MAIN:
if (isMainThread) {
invokeSubscriber(subscription, event);
} else {
mainThreadPoster.enqueue(subscription, event);
}
break;
case MAIN_ORDERED:
if (mainThreadPoster != null) {
mainThreadPoster.enqueue(subscription, event);
} else {
// temporary: technically not correct as poster not decoupled from subscriber
invokeSubscriber(subscription, event);
}
break;
case BACKGROUND:
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC:
asyncPoster.enqueue(subscription, event);
break;
default:
throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
}
}

这里根据 ThreadMode 去处理事件了。由于篇幅的问题,就分析一种了,当线程模式是主线程的时候,意味着,需要执行的代码在主线程中操作。如果是主线程,就是通过 反射,直接运行订阅的方法,如果不是主线程,需要 mainThreadPoster 将订阅事件入队列,一起看看 mainThreadPoster 的工作原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class HandlerPoster extends Handler implements Poster {

private final PendingPostQueue queue;
private final int maxMillisInsideHandleMessage;
private final EventBus eventBus;
private boolean handlerActive;

protected HandlerPoster(EventBus eventBus, Looper looper, int maxMillisInsideHandleMessage) {
super(looper);
this.eventBus = eventBus;
this.maxMillisInsideHandleMessage = maxMillisInsideHandleMessage;
queue = new PendingPostQueue();
}

public void enqueue(Subscription subscription, Object event) {
PendingPost pendingPost = PendingPost.obtainPendingPost(subscription, event);
synchronized (this) {
queue.enqueue(pendingPost);
if (!handlerActive) {
handlerActive = true;
if (!sendMessage(obtainMessage())) {
throw new EventBusException("Could not send handler message");
}
}
}
}

@Override
public void handleMessage(Message msg) {
boolean rescheduled = false;
try {
long started = SystemClock.uptimeMillis();
while (true) {
PendingPost pendingPost = queue.poll();
if (pendingPost == null) {
synchronized (this) {
// Check again, this time in synchronized
pendingPost = queue.poll();
if (pendingPost == null) {
handlerActive = false;
return;
}
}
}
eventBus.invokeSubscriber(pendingPost);
long timeInMethod = SystemClock.uptimeMillis() - started;
if (timeInMethod >= maxMillisInsideHandleMessage) {
if (!sendMessage(obtainMessage())) {
throw new EventBusException("Could not send handler message");
}
rescheduled = true;
return;
}
}
} finally {
handlerActive = rescheduled;
}
}
}

其实,在 EventBus 初始化的时候,mainThreadPoster 就已经获取主线程的Looper了,
就是用到了我们Android的消息处理机制:Looper,Handler 。至于消息队列是自己维护的一个
单向的链表。每次向Andorid的主线程Looper投递一个空消息,然后在 HandlerMessage() 方法里
面从自己维护的队列中取出 PendingPost 进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
final class PendingPost {
private final static List<PendingPost> pendingPostPool = new ArrayList<PendingPost>();

Object event;
Subscription subscription;
PendingPost next;
}
```
而 PendingPost 中维护的是订阅事件,EventType 和下一个 PendingPost 的地址。

### 反注册

```java
/** Unregisters the given subscriber from all event classes. */
public synchronized void unregister(Object subscriber) {
List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
if (subscribedTypes != null) {
for (Class<?> eventType : subscribedTypes) {
unsubscribeByEventType(subscriber, eventType);
}
typesBySubscriber.remove(subscriber);
} else {
logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());
}
}

反注册就是通过EventBus中 typesBySubscriber 这个属性,通过订阅者去查找订阅事件,然后去一一解绑的。当然,反注册主要是为了提高效率的,不然订阅的事件太多,非常影响性能。

新特性-粘性事件

在订阅方法后半部分,关于粘性事件的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (subscriberMethod.sticky) {
if (eventInheritance) {
// Existing sticky events of all subclasses of eventType have to be considered.
// Note: Iterating over all events may be inefficient with lots of sticky events,
// thus data structure should be changed to allow a more efficient lookup
// (e.g. an additional map storing sub classes of super classes: Class -> List<Class>).
Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
for (Map.Entry<Class<?>, Object> entry : entries) {
Class<?> candidateEventType = entry.getKey();
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = entry.getValue();
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object stickyEvent = stickyEvents.get(eventType);
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}

这里看下 checkPostStickyEventToSubscription:

1
2
3
4
5
6
7
private void checkPostStickyEventToSubscription(Subscription newSubscription, Object stickyEvent) {
if (stickyEvent != null) {
// If the subscriber is trying to abort the event, it will fail (event is not tracked in posting state)
// --> Strange corner case, which we don't take care of here.
postToSubscription(newSubscription, stickyEvent, isMainThread());
}
}

如果在注册的时候,指定要发布粘性事件,那么在订阅的时候,就立马调用 postToSubscription ,去发布了,至于它从缓存中 stickyEvents 中获取订阅事件,可能有疑问,什么时候把 EventType 放进去的呢?

1
2
3
4
5
6
7
8
9
10
11
/**
* Posts the given event to the event bus and holds on to the event (because it is sticky). The most recent sticky
* event of an event's type is kept in memory for future access by subscribers using {@link Subscribe#sticky()}.
*/
public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}

粘性事件在调用 postSticky() 方法的时候就已经放入缓存。自此关于EventBus的源码也分析完了。

参考:
[0]. 官网git https://github.com/greenrobot/EventBus
[1]. http://www.10tiao.com/html/227/201607/2650236358/1.html

Android之Boradcast

发表于 2019-01-24 | 分类于 Android

Android应用可以从 Android系统 和 其他Android应用 发送或接收广播消息,类似于 发
布-订阅
设计模式。当感兴趣的事件发生时,发送这些广播。例如,Android系统在发生各种系统事件时
发送广播,例如系统启动或设备开始充电时。例如,应用程序还可以发送自定义广播,以通知其他应用程序他
们可能感兴趣的内容(例如,已下载了一些新数据)。

应用可以注册以接收特定广播。当发送广播时,系统自动将广播路由到已订阅接收该特定类型广播的应用。

一般而言,广播可以用作跨应用程序和普通用户流程之外的消息传递系统。但是,您必须小心,不要滥用机会
响应广播并在后台运行可能导致系统性能降低的作业。

通过以上介绍可以知道:在Android系统中,广播(Broadcast)是在组件之间传播数据的一种机制,这些组件
可以位于不同的进程中,起到进程间通信的作用。

关于系统广播

当系统发生各种系统事件时,系统会自动发送广播,例如当系统进出飞行模式时。系统广播将发送到订阅接收
事件的所有应用程序。

广播消息本身包装在一个 Intent 对象中,该对象的动作字符串标识发生的事件(例如 android.intent.action.AIRPLANE_MODE)。意图还可以包括捆绑到其额外字段中的附加信息。例如,
飞行模式意图包括一个布尔额外值,用于指示飞行模式是否打开。

有关如何读取意图并从意图获取操作字符串的更多信息,请参阅意图和意图过滤器。

有关系统广播操作的完整列表,请参阅Android SDK中的 BROADCAST_ACTIONS.TXT 文件。每个广播动作都有一个与之相关的常量字段。例如,常量的值 ACTION_AIRPLANE_MODE_CHANGED是 android.intent.action.AIRPLANE_MODE。每个广播操作的文档都在其关联的常量字段中提供。

系统广播的变化

随着Android平台的发展,它会定期更改系统广播的行为方式。如果您的应用针对Android 7.0(API级别24)或更高版本,或者如果它安装在运行Android 7.0或更高版本的设备上,请记住以下更改。

Android 9

从Android 9(API级别28)开始, NETWORK_STATE_CHANGED_ACTION 广播不会收到有关 用户位置或个人身份数据的信息 。

此外,如果您的应用安装在运行Android 9或更高版本的设备上,则来自Wi-Fi的系统广播不包含SSID,BSSID,连接信息或扫描结果。要获取此信息,请调用getConnectionInfo() 。

Android 8.0

从Android 8.0(API级别26)开始,系统对清单声明的接收器施加了额外的限制。

如果您的应用面向Android 8.0或更高版本,则无法使用清单为大多数隐式广播声明接收方(广告不会专门针对您的应用)。当用户主动使用您的应用时,您仍然可以使用 上下文注册的接收器。

Android 7.0

Android 7.0(API级别24)及更高版本不发送以下系统广播:

  • ACTION_NEW_PICTURE
  • ACTION_NEW_VIDEO
    此外,针对Android 7.0及更高版本的应用必须CONNECTIVITY_ACTION使用注册广播registerReceiver(BroadcastReceiver, IntentFilter)。在清单中声明接收器不起作用。

接收广播

应用程序可以通过两种方式接收广播:通过 清单声明的接收器 和 上下文注册的接收器。

清单声明的接收器

如果您在清单中声明了广播接收器,系统会在发送广播时启动您的应用(如果应用尚未运行)。

⚠️注意:如果您的应用程序的目标是API级别26或更高级别,则不能使用清单来声明隐式广播的接收者(特定于您的应用程序的广播),除了一些免于该限制的隐式广播。在大多数情况下,您可以使用预定作业。
要在清单中声明广播接收器,请执行以下步骤:

  1. 在应用清单中指定元素。
    1
    2
    3
    4
    5
    6
    <receiver android:name=".MyBroadcastReceiver"  android:exported="true">
    <intent-filter>
    <action android:name="android.intent.action.BOOT_COMPLETED"/>
    <action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
    </intent-filter>
    </receiver>

intent过滤器指定接收者订阅的广播操作。

  1. 子类BroadcastReceiver并实现onReceive(Context, Intent)。以下示例中的广播接收器记录并显示广播的内容:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    private const val TAG = "MyBroadcastReceiver"

    class MyBroadcastReceiver : BroadcastReceiver() {

    override fun onReceive(context: Context, intent: Intent) {
    StringBuilder().apply {
    append("Action: ${intent.action}\n")
    append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\n")
    toString().also { log ->
    Log.d(TAG, log)
    Toast.makeText(context, log, Toast.LENGTH_LONG).show()
    }
    }
    }
    }

系统软件包管理器在安装应用程序时注册接收器。然后,接收器成为应用程序的单独入口点,这意味着如果应用程序当前未运行,系统可以启动应用程序并发送广播。

系统创建一个新的BroadcastReceiver组件对象来处理它接收的每个广播。 此对象仅在调用onReceive(Context,Intent)期间有效。 一旦您的代码从此方法返回,系统会认为该组件不再处于活动状态。

上下文注册的接收器

要使用上下文注册接收器,请执行以下步骤:

  1. 创建一个实例BroadcastReceiver。

    1
    val br: BroadcastReceiver = MyBroadcastReceiver()
  2. IntentFilter通过调用registerReceiver(BroadcastReceiver, IntentFilter)以下命令创建并注册接收器:

    1
    2
    3
    4
    val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION).apply {
    addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED)
    }
    registerReceiver(br, filter)

⚠️注意:要注册本地广播,请调用LocalBroadcastManager.registerReceiver(BroadcastReceiver, IntentFilter)。
只要注册上下文有效,上下文注册的接收器就会接收广播。例如,如果您在Activity上下文中注册 ,只要Activity未被销毁,您就会收到广播。如果您在应用程序上下文中注册,则只要应用程序正在运行,您就会收到广播。

  1. 要停止接收广播,请调用unregisterReceiver(android.content.BroadcastReceiver)。当您不再需要接收器或上下文不再有效时,请务必取消注册接收器。

请注意注册和取消注册接收器的位置,例如,如果使用活动的上下文在onCreate(Bundle)中注册接收器,则应在onDestroy()中取消注册,以防止接收器泄漏到活动上下文之外。 如果在onResume()中注册接收器,则应在onPause()中注销它以防止多次注册(如果您不希望在暂停时接收广播,这可以减少不必要的系统开销)。 不要在onSaveInstanceState(Bundle)中取消注册,因为如果用户在历史堆栈中向后移动,则不会调用此方法。

对过程状态的影响

BroadcastReceiver的状态(无论是否正在运行)会影响其包含进程的状态,从而影响其被系统杀死的可能性。例如,当进程执行接收器(即,当前在其onReceive()方法中运行代码)时,它被认为是前台进程。除极端内存压力外,系统保持运行。

但是,一旦您的代码从onReceive()返回,BroadcastReceiver就不再处于活动状态。接收方的主机进程与其中运行的其他应用程序组件一样重要。如果该进程仅承载清单声明的接收者(用户从未或最近未与之交互的应用程序的常见情况),则从onReceive()返回时,系统将其进程视为低优先级进程并且可能杀死它以使资源可用于其他更重要的过程。

因此,您不应该从广播接收器开始长时间运行后台线程。在onReceive()之后,系统可以随时终止进程以回收内存,并且这样做会终止在进程中运行的生成线程。要避免这种情况,您应该调用goAsync()(如果您希望在后台线程中处理广播更多时间)或使用JobScheduler从接收器调度JobService,以便系统知道该进程继续执行活动工作。有关更多信息,请参阅进程和应用程序生命周期。

以下代码段显示了一个BroadcastReceiver,它使用goAsync()标记在onReceive()完成后需要更多时间才能完成。 如果要在onReceive()中完成的工作足够长,导致UI线程错过一个帧(> 16ms),使其更适合后台线程,则此功能尤其有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private const val TAG = "MyBroadcastReceiver"

class MyBroadcastReceiver : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) {
val pendingResult: PendingResult = goAsync()
val asyncTask = Task(pendingResult, intent)
asyncTask.execute()
}

private class Task(
private val pendingResult: PendingResult,
private val intent: Intent
) : AsyncTask() {

override fun doInBackground(vararg params: String?): String {
val sb = StringBuilder()
sb.append("Action: ${intent.action}\n")
sb.append("URI: ${intent.toUri(Intent.URI_INTENT_SCHEME)}\n")
return toString().also { log ->
Log.d(TAG, log)
}
}

override fun onPostExecute(result: String?) {
super.onPostExecute(result)
// Must call finish() so the BroadcastReceiver can be recycled.
pendingResult.finish()
}
}
}

发送广播

Android为应用发送广播提供了三种方式:

  • sendOrderedBroadcast(Intent,String)方法一次向一个接收器发送广播。 当每个接收器依次执行时,它可以将结果传播到下一个接收器,或者它可以完全中止广播,以便它不会传递给其他接收器。 可以使用android:priority属性来控制匹配intent-filter顺序接收器; 具有相同优先级的接收器将以任意顺序运行。
  • sendBroadcast(Intent)方法以未定义的顺序向所有接收器发送广播。这称为正常广播。这更有效,但意味着接收器无法从其他接收器读取结果,传播从广播接收的数据或中止广播。
  • LocalBroadcastManager.sendBroadcast方法将广播发送到与发送者位于同一应用程序中的接收者。如果您不需要跨应用程序发送广播,请使用本地广播。实现效率更高(无需进程间通信),您无需担心与其他应用程序能够接收或发送广播相关的任何安全问题。

以下代码段演示了如何通过创建Intent和调用来发送广播sendBroadcast(Intent)。

1
2
3
4
5
Intent().also { intent ->
intent.setAction("com.example.broadcast.MY_NOTIFICATION")
intent.putExtra("data", "Notice me senpai!")
sendBroadcast(intent)
}

广播消息包含在Intent对象中。 intent的操作字符串必须提供应用程序的Java包名称语法,并唯一标识广播事件。 您可以使用putExtra(String,Bundle)将其他信息附加到intent。 您还可以通过调用intent上的setPackage(String)将广播限制为同一组织中的一组应用程序。

⚠️注意:尽管意图用于发送广播和使用startActivity(Intent)启动活动,但这些操作完全不相关。 广播接收器无法查看或捕获用于启动活动的意图; 同样,当您广播意图时,您无法找到或开始活动。

限制具有权限的广播

权限允许您将广播限制为具有特定权限的应用程序集。您可以对广播的发送者或接收者实施限制。

发送权限

当您调用sendBroadcast(Intent,String)或sendOrderedBroadcast(Intent,String,BroadcastReceiver,Handler,int,String,Bundle)时,您可以指定权限参数。 只有那些已经在其清单中请求带有标签的许可的接收者(并且如果它是危险的,则随后被授予许可)可以接收广播。 例如,以下代码发送广播:

1
sendBroadcast(Intent("com.example.NOTIFY"), Manifest.permission.SEND_SMS)

要接收广播,接收应用必须请求权限,如下所示:

1
<uses-permission android:name="android.permission.SEND_SMS"/>

您可以指定现有的系统权限SEND_SMS也可以使用该元素定义自定义权限 。有关一般权限和安全性的信息,请参阅系统权限。

⚠️注意:安装应用程序时会注册自定义权限。 必须在使用该应用程序的应用程序之前安装定义自定义权限的应用程序。

接收权限

如果您在注册广播接收器时指定了权限参数(在清单中带有registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)或 带有标签),那么只有已经在其清单中请求带有标签的权限的广播公司 (如果它是危险的,则随后被授予权限)可以发送对接收者的意图。

例如,假设您的接收应用程序具有清单声明的接收器,如下所示:

1
2
3
4
5
6
<receiver android:name=".MyBroadcastReceiver"
android:permission="android.permission.SEND_SMS">
<intent-filter>
<action android:name="android.intent.action.AIRPLANE_MODE"/>
</intent-filter>
</receiver>

或者您的接收应用程序有一个上下文注册的接收器,如下所示:

1
2
var filter = IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED)
registerReceiver(receiver, filter, Manifest.permission.SEND_SMS, null )

然后,为了能够向这些接收者发送广播,发送应用必须请求许可,如下所示:

1
<uses-permission android:name="android.permission.SEND_SMS"/>

安全考虑因素和最佳实践

以下是发送和接收广播的一些安全注意事项和最佳做法:

  • 如果您不需要向应用程序外部的组件发送广播,则发送和接收支持库中LocalBroadcastManager提供的 本地广播。的效率要高得多(不需要进程间通信),并可以让你避免考虑与其他应用程序能够接收或发送你的广播任何安全问题。本地广播可以在您的应用程序中用作通用发布/子事件总线,而无需系统范围广播的任何开销。LocalBroadcastManager

  • 如果许多应用已注册在其清单中接收相同的广播,则可能导致系统启动大量应用,从而对设备性能和用户体验产生重大影响。为避免这种情况,请优先使用上下文注册而不是清单声 有时,Android系统本身会强制使用上下文注册的接收器。例如,CONNECTIVITY_ACTION广播仅被传送到上下文注册的接收器。

  • 不要使用隐式意图广播敏感信息。任何注册接收广播的应用都可以读取该信息。有三种方法可以控制谁可以接收您的广播:

    • 您可以在发送广播时指定权限。
    • 在Android 4.0及更高版本,可以指定一个 包与 setPackage(String)发送广播时。系统将广播限制为与包匹配的应用程序集。
    • 您可以发送本地广播LocalBroadcastManager。
  • 当您注册接收器时,任何应用都可以向您的应用接收器发送潜在的恶意广播。有三种方法可以限制应用收到的广播:

    • 您可以在注册广播接收器时指定权限。
    • 对于清单声明的接收器,您可以在清单中将 android:exported 属性设置为“false”。接收方不接收来自应用程序之外的来源的广播。
    • 您可以将自己限制为仅限本地广播LocalBroadcastManager。
  • 广播操作的命名空间是全局的。确保操作名称和其他字符串都写在您拥有的命名空间中,否则您可能会无意中与其他应用程序发生冲突。

  • 因为接收者的onReceive(Context, Intent)方法在主线程上运行,所以它应该执行并快速返回。如果需要执行长时间运行的工作,请注意生成线程或启动后台服务,因为系统可能会在onReceive()返回后终止整个进程 。有关更多信息,请参阅对进程状态的影响要执行长时间运行的工作,我们建议:

    • 调用goAsync()接收者的onReceive()方法并将其传递BroadcastReceiver.PendingResult给后台线程。这使得广播在返回后保持活动状态onReceive()。但是,即使采用这种方法,系统也希望您能够非常快速地完成广播(10秒以内)。它允许您将工作移动到另一个线程,以避免故障主线程。
    • 使用计划安排工作JobScheduler。有关更多信息,请参阅智能作业计划。
  • 不要从广播接收器开始活动,因为用户体验很不稳定; 特别是如果有多个接收器。相反,请考虑显示通知。

原文:
[1]. https://developer.android.com/guide/components/broadcasts
参考:
[1]. https://www.jianshu.com/p/f348f6d7fe59

Navigation官方译文

发表于 2019-01-16 | 分类于 Android , androidx , Navigation

概念

导航是应用程序设计的重要组成部分。通过导航,您可以设计允许用户在应用内的不同内容区域移动,进出和退出的交互。

导航架构组件

导航架构组件可帮助您在应用程序中实现常见但复杂的导航要求,使您能够更轻松地为用户提供一致且可预测的体验。

导航可处理在应用程序的目的地之间导航- 也就是说,在应用程序中用户可以导航到任何位置。虽然目标通常通过Fragments代表特定的屏幕,但导航架构组件支持下面列出的其他目标类型:

  • Activities
  • 导航图和子图 - 当目标是图形或子图时,用户导航到该图或子图的起始目标
  • 自订目标类型

这些目的地通过操作连接,一系列的目的地和连接操作组成应用程序的导航图。

导航架构组件提供了许多其他好处,包括:

  • 处理Fragment事务
  • 默认情况下正确处理向上和向后操作
  • 为动画和过渡提供标准化资源
  • 将深层链接视为一级操作
  • 包括导航UI模式,例如导航抽屉和底部导航,只需最少的额外工作
  • 在导航传递参数时提供类型安全性
  • 使用Android Studio的导航编辑器可视化和编辑导航图

⚠️:如果要将导航架构组件与Android Studio一起使用,则必须使用Android Studio 3.2 Canary 14或更高版本。

导航原理

导航架构组件基于以下设计原则:

固定起始目的地

应用应具有固定的起始目的地,即用户从启动器启动应用时看到的屏幕,以及用户在按下后退按钮返回启动器之前看到的最后一个屏幕。

⚠️:应用可能具有一次性设置或一系列登录屏幕。这些条件屏幕不应被视为您应用的起始目的地。

导航状态应该通过堆栈目的地来表示

一个导航堆栈应该有应用程序的起始目标在堆栈的底部,并在堆栈顶部的当前目标。

更改导航堆栈的操作应始终在导航堆栈的顶部操作,方法是将新目标推送到堆栈顶部或从堆栈顶部弹出最顶层目标。

“向上”按钮永远不会退出您的应用

如果用户位于起始目的地,则不应显示“向上”按钮。当您的应用程序使用其他应用程序任务的深层链接启动时,Up应该将用户带到分层父目标,而不是返回到其他应用程序。

在应用程序的任务中,向上和向后相同

当系统“后退”按钮不会退出您的应用程序时,例如当您执行自己的任务而不是启动目标时,“向上”按钮的功能应与系统“后退”按钮完全相同。

深度链接和导航到目标应该产生相同的堆栈

用户应该能够使用“后退”或“上移”按钮,无论他们如何到达目的地,都可以通过目的地返回到起始目的地。
深度链接时,将删除任何现有的导航堆栈,并替换为深层链接的导航堆栈。

使用

通过导航组件实现导航,Jetpack的架构组件通过提供一组处理大部分细节的导航组件,可以轻松实现应用内导航。

使用Navigation,您可以创建导航图,这是一种XML资源,表示应用程序中的各个目标节点以及连接节点的操作。

下图显示了一个示例应用程序的导航图的直观表示,该应用程序包含通过5个操作连接的6个目标。
导航图

一个目标就是你可以在你的应用程序导航到任何地方。虽然目标通常是表示特定屏幕的Fragments,但导航支持其他目标类型:

  • Activities
  • 导航图和子图-当目标是图形或子图时,导航到该图或子图的起始目标
  • 自订目标类型
    ⚠️:导航组件专为具有一个具有多个Fragment目标的主要activity的应用程序而设计。主activity托管导航图,并负责根据需要交换目标。在具有多个activity目标的应用中,每个附加activity都会托管自己的导航图。有关更多信息,请参阅修改活动以主持导航 。

为项目引入导航组件

⚠️:如果您要使用Android Studio导航,则必须使用 Android Studio 3.2 Canary 14或更高版本。

要在项目中包含导航支持,请将以下内容添加到应用程序的 build.gradle文件中。

1
2
3
4
5
6
dependencies {
def nav_version = "1.0.0-alpha09"

implementation "android.arch.navigation:navigation-fragment:$nav_version" // use -ktx for Kotlin
implementation "android.arch.navigation:navigation-ui:$nav_version" // use -ktx for Kotlin
}

有关向项目添加其他体系结构组件的信息,请参阅向项目 添加组件。

创建导航图

要向项目添加导航图,请执行以下操作:

  1. 在“项目”窗口中,右键单击该res目录,然后选择 New > Android Resource File。出现 New Resource File 对话框。
  2. 在 File name 字段中键入名称,例如“nav_graph”。
  3. 从 Resource type 下拉列表中选择 Navigation。
  4. 单击 OK 。发生以下情况:

    • 在navigation目录中创建资源res目录。
    • nav_graph.xml在导航目录中创建一个文件。
    • 该 nav_graph.xml文件将在导航编辑器中打开。此XML文件包含导航图。
  5. 单击 Text 选项卡以切换到XML文本视图。您应该看到一个空的导航图,如以下示例所示:

1
2
3
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android">
</navigation>
  1. 单击 Design 返回 Navigation Editor。

浏览导航编辑器

在Navigation Editor中,您可以直观地编辑导航图,而不是直接编辑基础XML。
导航编辑器

  • 目的地:列出导航主机和当前位于“ 曲线图编辑器”中的所有目的地。
  • 图表编辑器:包含导航图的可视化表示。
  • 属性:显示导航图中当前所选项的属性。

创建目的地

创建导航图的第一步是识别和创建应用的各个目标。您可以在现有项目中的Fragments和Activities中创建目标,也可以创建占位符目标,以后可以使用Fragments或Activities替换它们。

如果您要将现有片段或活动添加到导航图中,或者如果要添加占位符,请单击 New Destination ➕,然后在下拉列表中单击相应的片段,活动或占位符目标。出现。现在,您可以在 Design 视图中查看目标的预览以及导航图的“ 文本”视图中的相应XML 。

要创建新的目标类型,请执行以下操作:

  1. 在导航编辑器中,单击 New Destination ➕,然后单击 Create blank destination 。
  2. 在出现的 New Android Component 对话框中,输入 Fragment Name 。这是Fragment类的名称。
  3. 要让Android Studio为Fragment创建相应的布局资源文件,请选中 Create layout XML 旁边的框,然后在 Fragment Layout Name 字段中输入资源名称。
  4. 在 Source Language 下拉列表中,为类源文件选择Kotlin或Java。
  5. 单击完成。
    新目标类型显示在导航编辑器中。Android Studio还以指定的源语言创建相应的类文件,如果指定,还会为新目标类型创建布局资源文件。

图显示了目标和占位符的示例。
目标和占位符

可以单击任何目的地以选择它。选择目标时,Attributes 面板中将显示以下属性:

  • Type 字段指示目标是否在源代码中实现为片段或活动。
  • Label 字段包含目标的XML布局文件的名称。
  • ID 字段包含被用于指在代码中的目的地的目的地的ID。
  • Class 下拉列表显示与目标关联的类的名称。您可以单击此下拉列表将关联的类更改为其他目标类型。

⚠️:占位符与类无关。请务必在运行应用程序之前更改类值。

单击 Text 选项卡以显示导航图的XML视图。XML包含相同的id,name,label,和layout属性的目的地,如下所示:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
app:startDestination="@id/blankFragment">
<fragment
android:id="@+id/blankFragment"
android:name="com.example.cashdog.cashdog.BlankFragment"
android:label="Blank"
tools:layout="@layout/fragment_blank" />
</navigation>

连接目的地

您必须有多个目标才能连接目标。以下是包含两个空白目标的导航图的XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
app:startDestination="@id/blankFragment">
<fragment
android:id="@+id/blankFragment"
android:name="com.example.cashdog.cashdog.BlankFragment"
android:label="fragment_blank"
tools:layout="@layout/fragment_blank" />
<fragment
android:id="@+id/blankFragment2"
android:name="com.example.cashdog.cashdog.BlankFragment2"
android:label="Blank2"
tools:layout="@layout/fragment_blank_fragment2" />
</navigation>

目的地使用动作连接。要连接两个目标,请执行以下操作:

  1. 在 Design 选项卡中,将鼠标悬停在您希望用户导航的目标的右侧。目的地上会出现一个圆圈。
    视图设计
    图4.动作连接圈
  2. 单击并按住,将光标拖到您希望用户导航到的目标上,然后释放。绘制一条线以指示两个目的地之间的导航。
    连接目的地
    图5.连接目的地
    单击箭头以突出显示该操作。Attributes 面板中显示以下属性:

    • Type 字段包含 Action。
    • 该ID字段包含动作ID。
    • Destination 字段包含所述目的地片Fragment或Activity的ID。
      单击 Text 选项卡以切换到XML视图。现在,操作元素已添加到源目标。该操作具有ID和目标属性,其中包含下一个目标的ID,如下例所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
app:startDestination="@id/blankFragment">
<fragment
android:id="@+id/blankFragment"
android:name="com.example.cashdog.cashdog.BlankFragment"
android:label="fragment_blank"
tools:layout="@layout/fragment_blank" >
<action
android:id="@+id/action_blankFragment_to_blankFragment2"
app:destination="@id/blankFragment2" />
</fragment>
<fragment
android:id="@+id/blankFragment2"
android:name="com.example.cashdog.cashdog.BlankFragment2"
android:label="fragment_blank_fragment2"
tools:layout="@layout/fragment_blank_fragment2" />
</navigation>

将屏幕指定为起始目的地

导航编辑器使用🏠来指示用户在打开应用程序时看到的第一个屏幕,也称为 起始目的地。

要选择其他起始目的地,请执行以下操作:

  1. 在 Design 选项卡中,单击新的起始目标以突出显示它。

  2. 单击 Assign start destination 按钮🏠。或者,您可以右键单击目标,然后单击 Set as Start Destination。

修改活动以主持导航

Activity为NavHost中的应用程序提供导航。 NavHost是一个空容器,当用户浏览您的应用程序时,目的地会被换入和换出。
Navigation组件中默认NavHost的实现是NavHostFragment。

使用布局编辑器添加NavHostFragment

您可以使用布局编辑器通过以下步骤将NavHostFragment添加到Activity:

  1. 如果您还没有导航图资源,请创建导航图资源。
  2. 在项目文件列表中,双击活动的布局XML文件,在布局编辑器中将其打开。
  3. 在Palette窗格中,选择Containers类别,或者搜索“NavHostFragment”。
  4. 将NavHostFragment视图拖到您的活动上。
  5. 接下来,在出现的 Navigation Graphs 对话框中,选择要与其关联的相应导航图NavHostFragment,然后单击 OK 。

回到 Text 视图,请注意Android Studio添加了类似于以下内容的XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"

app:defaultNavHost="true"
app:navGraph="@navigation/mobile_navigation" />

</android.support.constraint.ConstraintLayout>

该app:defaultNavHost=”true”属性确保您NavHostFragment 拦截系统后退按钮。您还可以通过覆盖AppCompatActivity.onSupportNavigateUp() 和调用来实现此行为NavController.navigateUp,如下例所示:

1
2
3
4
5
/*  override fun onSupportNavigateUp(): Boolean {
return Navigation.findNavController(this, R.id.fragment_nav).navigateUp()
}*/

override fun onSupportNavigateUp() = Navigation.findNavController(this, R.id.fragment_nav).navigateUp()

以编程方式创建NavHostFragment

您还可以使用NavHostFragment.create() 以编程方式创建NavHostFragment具有特定图形资源的资源,如以下示例所示:

1
2
3
4
5
val finalHost = NavHostFragment.create(R.navigation.example_graph)
supportFragmentManager.beginTransaction()
.replace(R.id.nav_host, finalHost)
.setPrimaryNavigationFragment(finalHost) // this is the equivalent to app:defaultNavHost="true"
.commit()

将目标绑定到ui小部件

使用NavController类导航到目标。 可以使用以下静态方法之一检索NavController::

  • NavHostFragment.findNavController(Fragment)
  • Navigation.findNavController(Activity, @IdRes int viewId)
  • Navigation.findNavController(View)
    检索后 NavController,使用其 navigate() 方法导航到目标。该 navigate() 方法接受资源ID。ID可以是导航图或操作中特定目标的ID。使用操作的ID而不是目标的资源ID具有优势,例如将过渡与导航相关联。有关转换的更多信息,请参阅创建目标之间的转换。

以下代码段显示了如何导航到 ViewTransactionsFragment:

1
2
3
viewTransactionsButton.setOnClickListener { view ->
view.findNavController().navigate(R.id.viewTransactionsAction)
}

Android系统维护一个包含最后访问目的地的 back stack 。当用户打开应用程序时,应用程序的第一个目标位于堆栈中。每次调用该 navigate() 方法都会将另一个目标放在堆栈顶部。相反,按“向上”或“返回”按钮分别调用 NavController.navigateUp() 和 NavController.popBackStack() 方法,将顶部目标弹出堆栈。

对于按钮,您还可以使用 Navigation类的 createNavigateOnClickListener() 便捷方法导航到目标:

1
button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.next_fragment, null))

要处理其他常见UI组件,例如顶部应用栏和底部导航,请参阅使用NavigationUI更新UI组件。

为目标创建深层链接

在Android中,深层链接是指向应用中特定目标的URI。当您想要将用户发送到特定目的地以在您的应用中执行某项任务时,这些URI很有用,例如发送资金流,允许用户快速汇款给某人。

为目标分配深层链接

要在导航图中为目标指定深层链接,请执行以下操作:

  • 在 Design 视图中,选择深层链接的目标。
  • 单击 Add deep link 按钮🔗(或单击 Attributes 面板的 Deep Links 部分中的➕)。出现 Add Deep Link 对话框。
  • 在URI字段中键入URI,例如“www.cashdog.com/sendmoney”,它表示应用中发送货币嵌套图的起始目的地。

请注意以下事项:

  • 未指定协议的URI被假定为http或https。例如, www.cashdog.com匹配http://www.cashdog.com 或https://www.cashdog.com。
  • 占位符的{placeholder_name}匹配形式为1个或多个字符。String占位符的值在Bundle带有相同名称的键的参数中可用。例如,http://www.example.com/users/{id}匹配http://www.example.com/users/4.
  • 。*通配符可用于匹配0个或多个字符。
    • (可选)选中 Auto Verify 以要求Google验证您是URI的所有者。有关更多信息,请参阅验证Android应用程序链接。
    • 单击 Add。 所选目标上方会显示一个链接图标,表示该目标具有深层链接。
    • 单击 Text 选项卡以切换到XML视图。已将嵌套的深层链接元素添加到目标:
      1
      <deepLink app:uri="https://cashdog.com/sendmoney"/>

当用户从深层链接目标按下“后退”按钮时,他们会导航回导航堆栈,就像他们从应用程序的入口点进入您的应用程序一样。

为深层链接添加意图过滤器

您必须添加manifest.xml文件以在您的应用中启用深层链接:

对于Android Studio 3.0和3.1,您必须手动添加intent-filter 元素。有关更多信息,请参阅 创建应用程序内容的深层链接。
对于Android Studio 3.2+,您可以nav-graph向活动元素添加元素:

1
2
3
<activity name=".MainActivity">
<nav-graph android:value="@navigation/main_nav" />
</activity>

作为清单合并构建步骤的一部分,此元素将替换为匹配导航图中所有深层链接所需的生成元素。

以编程方式使用NavDeepLinkBuilder创建深层链接

您可以使用NavDeepLinkBuilder该类构建一个PendingIntent 将用户带到特定目标的类。

触发此深层链接时,将清除任务后台堆栈并替换为深层链接目标。当嵌套图表,从每一级的开始目的地嵌套的,也就是说,从每个目的地开始 在元件层次结构也被添加到堆栈中。

您可以PendingIntent直接 构造一个NavDeepLinkBuilder(Context),如下面的示例所示。请注意,如果提供的上下文不是a Activity,则构造函数将使用 PackageManager.getLaunchIntentForPackage() 作为默认活动来启动(如果可用)。

1
2
3
4
5
val pendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.mobile_navigation)
.setDestination(R.id.android)
.setArguments(args)
.createPendingIntent()

有关包含此示例的完整导航项目,请参阅 Navigation Codelab。

⚠️:如果您有现有的NavController,您还可以通过创建深层链接NavController.createDeepLink()。

在目的地之间创建过渡

导航组件提供了在目的地之间轻松添加转换(例如淡入和淡出)的功能。要添加转换,请执行以下操作:

  1. 创建动画资源文件。导航支持属性和视图动画。有关详细信息,请参阅 Animation resources。
  2. 在导航编辑器的 https://developer.android.com/guide/topics/resources/animation-resource 选项卡中,单击要进行转换的操作。
  3. 在 Attributes 面板的 Transitions 部分中,单击 Enter 旁边的向下箭头以显示项目中可用过渡的列表。
  4. 选择用户进入目的地时发生的转换。
  5. 回到 Transitions 部分,单击 Exit 旁边的向下箭头,然后选择当用户退出目标时发生的转换。
  6. 单击 Text 选项卡以切换到XML文本视图。转换的XML出现在指定操作的元素中。该操作嵌入在转换发生之前处于活动状态的目标的XML中。在以下示例中, specifyAmountFragment是活动目标,因此它包含具有过渡动画的操作:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <fragment
    android:id="@+id/specifyAmountFragment"
    android:name="com.example.buybuddy.buybuddy.SpecifyAmountFragment"
    android:label="fragment_specify_amount"
    tools:layout="@layout/fragment_specify_amount">
    <action
    android:id="@+id/confirmationAction"
    app:destination="@id/confirmationFragment"
    app:enterAnim="@anim/slide_in_right"
    app:exitAnim="@anim/slide_out_left"
    app:popEnterAnim="@anim/slide_in_left"
    app:popExitAnim="@anim/slide_out_right" />
    </fragment>

在此示例中,我们在移动到目的地时(enterAnim以及exitAnim退出该目标(popEnterAnim 和popExitAnim)时发生转换)。

在Fragment目标之间添加共享元素转换

除了过渡动画之外,Navigation还支持在目标之间添加共享元素过渡。

与动画不同,共享元素转换是以编程方式提供的,而不是通过导航XML文件提供的,因为它们需要引用View您希望包含在共享元素转换中的 实例。

每种类型的目标都通过Navigator.Extras 接口的子类实现此编程API 。将Extras被传递到一个呼叫navigate()。

Fragment目标共享元素转换
在FragmentNavigator.Extras 类允许您附加共享元素到navigate()呼叫目的地的片段,如图下面的例子:

1
2
3
4
5
6
7
val extras = FragmentNavigatorExtras(
imageView to "header_image",
titleView to "header_title")
view.findNavController().navigate(R.id.confirmationAction,
null, // Bundle of args
null, // NavOptions
extras)

⚠️:这里的header_image和header_title要确保在当前布局的android:transitionName和需要调整的布局的android:transitionName一样

Activity目标共享元素转换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Rename the Pair class from the Android framework to avoid a name clash
import android.util.Pair as UtilPair
///...
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity,
UtilPair.create(imageView, "header_image"),
UtilPair.create(titleView, "header_title"))
val extras = ActivityNavigator.Extras(options)
view.findNavController().navigate(R.id.details,
null, // Bundle of args
null, // NavOptions
extras)

```



### 使用NavigationUI更新UI组件

导航架构组件包括NavigationUI类。 此类包含使用顶部应用栏,导航抽屉和底部导航管理导航的静态方法。

#### 监听导航事件
与NavController交互是在目的地之间导航的主要方法。 NavController负责用新目标替换NavHost的内容。 在许多情况下,UI元素 - 例如顶级应用栏或其他持久性导航控件(如BottomNavigationBar)在NavHost之外存在,并且需要在目的地之间导航时进行更新。

NavController提供了一个OnDestinationChangedListener接口,当NavController的当前目标或其参数发生更改时,该接口将被调用。 可以通过addOnDestinationChangedListener()方法注册新的侦听器,如下面的示例所示。 请注意,在调用addOnDestinationChangedListener()时,如果当前目标存在,则会立即将其发送给您的侦听器。

```Kotlin
navController.addOnDestinationChangedListener { navController, destination, arguments ->
textView.setText(destination.label)
}

NavigationUI使用OnDestinationChangedListener使这些常见的UI组件可以感知导航事件。 但请注意,您还可以单独使用OnDestinationChangedListener来使任何自定义UI或业务逻辑感知导航事件。

顶部应用栏

顶部应用栏在应用顶部 提供了一致的位置,用于显示当前屏幕的信息和操作。
顶部导航
NavigationUI包含在用户浏览应用时自动更新顶部应用栏中内容的方法。例如,NavigationUI使用导航图中的目标标签可以使顶部应用栏的标题保持最新。

使用NavigationUI下面讨论的顶级应用栏方法时,可以使用{argName}标签中的格式从提供给目标的参数中自动填充附加到目标的标签。

NavigationUI 提供以下顶级应用栏类型的支持:

  • Toolbar
  • CollapsingToolbarLayout
  • ActionBar

AppBarConfiguration

NavigationUI使用AppBarConfiguration 对象来管理应用程序显示区域左上角的“导航”按钮的行为。默认情况下,当用户位于导航图的顶级目标位置时,导航按钮将隐藏,并在任何其他目标位置显示为“向上”按钮。

要将导航图的起始目的地用作唯一的顶级目的地,您可以创建一个AppBarConfiguration对象并传入相应的导航图,如下所示:

1
val appBarConfiguration = AppBarConfiguration(navController.graph)

如果要自定义哪些目标被视为顶级目标,则可以将一组目标ID传递给构造函数,如下所示:

1
val appBarConfiguration = AppBarConfiguration(setOf(R.id.main, R.id.android))

创建Toolbar

要使用NavigationUI创建工具栏,请首先在Activity中定义工具栏,如下所示:

1
2
3
4
5
6
7
8
<LinearLayout>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar" />
<fragment
android:id="@+id/nav_host_fragment"
... />
...
</LinearLayout>

接下来,setupWithNavController() 从您的主要活动的onCreate()方法调用,如下所示:

1
2
3
4
5
6
7
8
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_main)

...
//主导航Fragment
val navHostFragment = nav_host_fragment as NavHostFragment
// 其中toolbal为需要绑定的视图控件,目前支持12种视图控件;
NavigationUI.setupWithNavController(toolbar, navHostFragment.navController)

⚠️:注意:使用工具栏时,导航会自动处理“导航”按钮的单击事件,因此您无需覆盖onSupportNavigateUp()。!!!如果使用带menu菜单的控件如BottomNavigationView时,需要确保menu的id和需要跳转的导航图中的fragment的ID一致!!!

包括CollapsingToolbarLayout

要CollapsingToolbarLayout在工具栏中添加a ,请先在主要活动中定义工具栏和周围布局,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<LinearLayout>
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="@dimen/tall_toolbar_height">

<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleGravity="top"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>

<fragment
android:id="@+id/nav_host_fragment"
... />
...
</LinearLayout>

接下来,setupWithNavController() 从您的主要活动的onCreate方法调用,如下所示:

1
2
3
4
5
6
7
8
9
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_main)

...

//主导航Fragment
val navHostFragment = nav_host_fragment as NavHostFragment
// 其中toolbal为需要绑定的视图控件,目前支持12种视图控件;
NavigationUI.setupWithNavController(toolbar, navHostFragment.navController)

Action bar

要使用默认操作栏包含导航支持,请setupActionBarWithNavController() 从主Activity的onCreate()方法中调用 ,如下所示。请注意,您需要声明自己的AppBarConfiguration外部onCreate(),因为您在覆盖时也使用它onSupportNavigateUp():

1
2
3
4
5
6
7
8
9
10
11
private lateinit var appBarConfiguration: AppBarConfiguration

...

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

val navController = findNavController(R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)
}

接下来,覆盖onSupportNavigateUp()以处理向上导航:

1
2
3
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}

将目的地绑定到菜单项

NavigationUI还提供了将目标绑定到菜单驱动的UI组件的帮助程序。NavigationUI包含一个辅助方法,onNavDestinationSelected()它MenuItem与NavController托管关联目标的方法一起使用 。如果id在的MenuItem比赛的id目标时,NavController可以然后导航到目的地。

作为一个例子,下面的XML片断定义一个菜单项,并具 有共同的目的地id(id需要一致) ,details_page_fragment:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
... >

...

<fragment android:id="@+id/details_page_fragment"
android:label="@string/details"
android:name="com.example.android.myapp.DetailsFragment" />
</navigation>

1
2
3
4
5
6
7
8
9
<menu xmlns:android="http://schemas.android.com/apk/res/android">

...

<item
android:id="@id/details_page_fragment"
android:icon="@drawable/ic_details"
android:title="@string/details" />
</menu>

例如,如果您的菜单是通过Activity的onCreateOptionsMenu()添加的,则可以通过覆盖Activity的onOptionsItemSelected()来调用onNavDestinationSelected()来关联菜单项和目标,如下所示:

1
2
3
4
override fun onOptionsItemSelected(item: MenuItem): Boolean {
val navController = findNavController(R.id.nav_host)
return item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
}

现在,当用户单击details_page_fragment菜单项时,应用程序会自动导航到 具有相同ID的相应目标 。

添加导航抽屉

导航抽屉是一个UI面板,显示应用程序的主导航菜单。 当用户触摸应用栏中的抽屉图标或用户从屏幕的左边缘滑动手指时,抽屉出现。
导航抽屉
抽屉图标显示在使用DrawerLayout的所有顶级目标上。 顶级目标是应用程序的根级目标。 它们不会在应用栏中显示“向上”按钮。

要添加导航抽屉,首先将DrawerLayout声明为根视图。 在DrawerLayout内,添加主UI内容的布局和包含导航抽屉内容的另一个视图。

例如,以下布局使用具有两个子视图的DrawerLayout:NavHostFragment包含主要内容,NavigationView用于导航抽屉的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="utf-8"?>
<!-- Use DrawerLayout as root container for activity -->
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">

<!-- Layout to contain contents of main body of screen (drawer will slide over this) -->
<fragment
android:name="androidx.navigation.fragment.NavHostFragment"
android:id="@+id/nav_host_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />

<!-- Container for contents of drawer - use NavigationView to make configuration easier -->
<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true" />

</android.support.v4.widget.DrawerLayout>

接下来,DrawerLayout 通过将其传递到导航图,将其连接到AppBarConfiguration,如下所示:

1
val appBarConfiguration = AppBarConfiguration(navController.graph, drawerLayout)

⚠️:使用时NavigationUI,顶部应用栏帮助器会在当前目标更改时自动在抽屉图标和“向上”图标之间切换。你不需要使用 ActionBarDrawerToggle。
接下来,在主Activity类中,setupWithNavController() 从主活动的onCreate()方法调用 ,如下所示:

1
2
3
4
5
6
7
8
9
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_main)

...

val navController = findNavController(R.id.nav_host_fragment)
findViewById<NavigationView>(R.id.nav_view)
.setupWithNavController(navController)
}

底部导航

NavigationUI也可以处理底部导航。当用户选择菜单项时,NavController调用 onNavDestinationSelected() 并自动更新底部导航栏中的所选项。
底部导航
要在应用中创建底部导航栏,请先在主要活动中定义栏,如下所示:

1
2
3
4
5
6
7
8
9
<LinearLayout>
...
<fragment
android:id="@+id/nav_host_fragment"
... />
<android.support.design.widget.BottomNavigationView
android:id="@+id/bottom_nav"
app:menu="@menu/menu_bottom_nav" />
</LinearLayout>

接下来,在Activity中,setupWithNavController() 从主活动的onCreate()方法调用 ,如下所示:

1
2
3
4
5
6
7
8
9
override fun onCreate(savedInstanceState: Bundle?) {
setContentView(R.layout.activity_main)

...

val navController = findNavController(R.id.nav_host_fragment)
findViewById<BottomNavigationView>(R.id.bottom_nav)
.setupWithNavController(navController)
}

嵌套导航图

可以将一系列目标分组为称为根图的父导航图中的嵌套图。 嵌套图对于组织和重用应用程序UI的各个部分非常有用,例如自包含的登录流程。

嵌套图封装了其目标。 与根图一样,嵌套图必须将目标标识为起始目标。 嵌套图外部的目标(例如根图上的目标)仅通过其起始目标访问嵌套图。

图显示了一个简单的汇款应用程序的导航图。 从左侧的起始目的地开始,图表有两个流程:一个沿着顶部用于发送货币,另一个沿着底部用于查看帐户余额。
汇款导航图
要将目标分组到嵌套图中,请执行以下操作:

  1. 在导航编辑器中,按住Shift键,然后单击要包含在嵌套图中的目标。
  2. 右键单击以打开上下文菜单,然后选择 Move to Nested Graph > New Graph 。目标包含在嵌套图中。图显示了 Navigation Editor 中的嵌套图:
    Graph Editor中的嵌套图
  3. 单击嵌套图。Attributes 面板中显示以下属性 :

    • Type,其中包含”Nested Graph”
    • ID,包含嵌套图的系统分配ID。此ID用于引用代码中的嵌套图。
  4. 双击嵌套图形以显示其目标。

  5. 单击 Text 选项卡以切换到XML视图。图表中添加了嵌套导航图。此导航图有自己的navigation 元素,以及它自己的ID和startDestination指向嵌套图中第一个目标的属性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    <?xml version="1.0" encoding="utf-8"?>
    <navigation xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    app:startDestination="@id/mainFragment">
    <fragment
    android:id="@+id/mainFragment"
    android:name="com.example.cashdog.cashdog.MainFragment"
    android:label="fragment_main"
    tools:layout="@layout/fragment_main" >
    <action
    android:id="@+id/action_mainFragment_to_chooseRecipient"
    app:destination="@id/sendMoneyGraph" />
    <action
    android:id="@+id/action_mainFragment_to_viewBalanceFragment"
    app:destination="@id/viewBalanceFragment" />
    </fragment>
    <fragment
    android:id="@+id/viewBalanceFragment"
    android:name="com.example.cashdog.cashdog.ViewBalanceFragment"
    android:label="fragment_view_balance"
    tools:layout="@layout/fragment_view_balance" />
    <navigation android:id="@+id/sendMoneyGraph" app:startDestination="@id/chooseRecipient">
    <fragment
    android:id="@+id/chooseRecipient"
    android:name="com.example.cashdog.cashdog.ChooseRecipient"
    android:label="fragment_choose_recipient"
    tools:layout="@layout/fragment_choose_recipient">
    <action
    android:id="@+id/action_chooseRecipient_to_chooseAmountFragment"
    app:destination="@id/chooseAmountFragment" />
    </fragment>
    <fragment
    android:id="@+id/chooseAmountFragment"
    android:name="com.example.cashdog.cashdog.ChooseAmountFragment"
    android:label="fragment_choose_amount"
    tools:layout="@layout/fragment_choose_amount" />
    </navigation>
    </navigation>
  6. 在您的代码中,将连接根图的操作的资源ID传递给嵌套图:

    1
    view.findNavController().navigate(R.id.action_mainFragment_to_sendMoneyGraph)
  7. 返回 Design 选项卡,可以通过单击 Root 返回到根图 。

使用引用其他导航图

在导航图中,您可以使用include引用其他图形。 虽然这在功能上与使用嵌套图相同,但include允许您使用其他项目模块或库项目中的图形,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- (root) nav_graph.xml -->
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/nav_graph"
app:startDestination="@id/fragment">

<include app:graph="@navigation/included_graph" />

<fragment
android:id="@+id/fragment"
android:name="com.example.myapplication.BlankFragment"
android:label="Fragment in Root Graph"
tools:layout="@layout/fragment_blank">
<action
android:id="@+id/action_fragment_to_second_graph"
app:destination="@id/second_graph" />
</fragment>

...
</navigation>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- included_graph.xml -->
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/second_graph"
app:startDestination="@id/includedStart">

<fragment
android:id="@+id/includedStart"
android:name="com.example.myapplication.IncludedStart"
android:label="fragment_included_start"
tools:layout="@layout/fragment_included_start" />
</navigation>

数据传递

导航允许您通过定义目标的参数将数据附加到导航操作。例如,用户配置文件目标可能会使用用户ID参数来确定要显示的用户。

通常,您应该更倾向于仅在目标之间传递最少量的数据。例如,您应该传递一个键来检索一个对象而不是传递该对象本身,因为所有已保存状态的总空间在Android上是有限的。如果需要传递大量数据,请考虑使用片段之间共享数据中ViewModel所述的方法 。

定义目标参数

要在目标之间传递数据,首先通过以下步骤将参数添加到接收它的目标来定义参数:

  1. 在导航编辑器中,单击接收参数的目标。
  2. 在 Attributes 面板中,单击 Add(+)。
  3. 在出现的 Add Argument Link 窗口中,输入参数名称,参数类型,参数是否可为空,以及默认值(如果需要)。
  4. 单击 Add 。请注意,该参数现在显示 在 Arguments 面板的 Attributes 列表中。
  5. 接下来,单击将您带到此目的地的相应操作。在 Attributes 面板中,您现在应该在 Argument Default Values 部分中看到新添加的参数。
  6. 您还可以看到该参数是以XML格式添加的。单击 Text 选项卡以切换到XML视图,并注意您的参数已添加到接收参数的目标。一个例子如下所示:
1
2
3
4
5
6
<fragment android:id="@+id/myFragment" >
<argument
android:name="myArg"
app:argType="integer"
android:defaultValue="0" />
</fragment>

覆盖操作中的目标参数

目标级参数和默认值由导航到目标的所有操作使用。如果需要,您可以通过在操作级别定义参数来覆盖参数的默认值(如果尚不存在,则设置一个参数)。此参数必须与目标中声明的参数 具有相同的名称和类型 。

下面的XML声明了一个带有参数的操作,该参数覆盖了上面示例中的目标级参数:

1
2
3
4
5
6
7
<action android:id="@+id/startMyFragment"
app:destination="@+id/myFragment">
<argument
android:name="myArg"
app:argType="integer"
android:defaultValue="1" />
</action>

使用Safe Args传递类型安全的数据

Navigation Architecture Component有一个名为Safe Args的Gradle插件,它生成简单的对象和构建器类,以便对目标和操作指定的参数进行类型安全访问。

要使用Safe Args,请先将androidx.navigation.safeargs插件添加到应用程序的build.gradle文件中,如下所示:

1
2
3
4
5
6
apply plugin: 'com.android.application'
apply plugin: 'androidx.navigation.safeargs'

android {
//...
}

例外还需在项目build.gradle中添加:

1
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha09"

启用插件后,生成的代码包含操作的其他简单对象和构建器类以及发送和接收目标。这些类描述如下:

  • 为动作发起的每个目标创建一个类。此类的名称是始发目标的名称,后面附加 “Directions” 一词。例如,如果始发目标是已命名的片段,SpecifyAmountFragment则将调用生成的类 SpecifyAmountFragmentDirections。
  • 此类具有针对始发目标中定义的每个操作的方法。
  • 对于用于传递参数的每个操作,将创建一个内部类,其名称基于操作。例如,如果调用confirmationAction,该操作 ,则命名该类ConfirmationAction。
  • 为接收目标创建一个类。此类的名称是目标的名称,后面附加单词“Args”。例如,如果命名目标片段,ConfirmationFragment,则调用生成的类ConfirmationFragmentArgs。使用此类的fromBundle()方法来检索参数。

以下示例说明如何使用这些方法设置参数并将其传递给navigate() 方法:

1
2
3
4
5
6
override fun onClick(v: View) {
val amountTv: EditText = view!!.findViewById(R.id.editTextAmount)
val amount = amountTv.text.toString().toInt()
val action = SpecifyAmountFragmentDirections.confirmationAction(amount)
v.findNavController().navigate(action)
}

在接收目标的代码中,使用该 getArguments()方法检索包并使用其内容:

1
2
3
4
5
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val tv: TextView = view.findViewById(R.id.textViewAmount)
val amount = ConfirmationFragmentArgs.fromBundle(arguments).amount
tv.text = amount.toString()
}
使用具有全局操作的Safe Args

使用具有全局操作的 Safe Args时 ,必须为android:id根元素提供值,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_nav"
app:startDestination="@id/mainFragment">

...

</navigation>

导航为基于该值Directions的元素生成一个类android:id。例如,如果您有一个 元素android:id=@+id/main_nav,则调用生成的类 MainNavDirections。元素中的所有目标都扩展了MainNavDirections类,并且可以使用与上一节中描述的方法相同的方法访问所有关联的全局操作。

使用Bundle对象在目标之间传递数据

如果您不使用Gradle,则仍可以使用Bundle对象在目标之间传递参数。使用创建Bundle对象并将其传递到目标navigate(),如下所示

1
2
var bundle = bundleOf("amount" to amount)
view.findNavController().navigate(R.id.confirmationAction, bundle)

在接收目标的代码中,使用该 getArguments()方法检索Bundle并使用其内容:

1
2
val tv = view.findViewById<TextView>(R.id.textViewAmount)
tv.text = arguments.getString("amount")

支持

添加对新目标类型的支持
NavControllers依靠一个或多个Navigator对象来执行导航操作。默认情况下,所有 NavControllers支持通过使用ActivityNavigator 类及其嵌套 ActivityNavigator.Destination 类导航到另一个活动而离开导航图 。为了能够导航到任何其他类型的目标,Navigator必须将一个或多个其他对象添加到 NavController。例如,当使用片段作为目标时,会 NavHostFragment 自动将FragmentNavigator 类添加到其中 NavController。

要向a添加新的Navigator对象 NavController,必须使用相应的Navigator类的 getNavigatorProvider()方法,然后使用类的addNavigator()方法。以下代码显示了将虚构CustomNavigator 对象添加到以下内容的示例NavController:

1
2
val customNavigator = CustomNavigator()
navController.navigatorProvider += customNavigator

大多数Navigator 类都有一个嵌套的目标子类。此子类可用于指定目标唯一的其他属性。有关Destination子类的详细信息,请参阅相应Navigator类的参考文档 。

条件导航

您的应用程序可能有一系列条件目标,这些目标仅在某些条件下使用,例如当用户需要登录时。这些目标应创建为单独的目标,或嵌套导航图,另一个目标作为另一个目标启动需要。图1显示了导航到配置文件目的地的用户,该配置文件目的地确定用户未登录后,要求用户导航到登录目的地。登录目的地然后在登录完成后将用户返回到配置文件目的地。

登录目标应在返回到配置文件目标后从导航堆栈中弹出。popBackStack() 导航回原始目的地时调用 方法。原始目标将从导航堆栈“弹出”并变为活动状态。

全局导航

您可以使用全局操作来创建多个目标可以使用的公共操作。例如,您可能希望不同目的地的按钮导航到同一主应用程序屏幕。

导航编辑器中的一个全局操作由指向关联目标的小箭头表示,如图1所示
导入嵌套图的全局操作

创建一个全局行动

要创建全局操作,请执行以下操作:

  1. 在“ 曲线图编辑器”中,单击目标以突出显示它。
  2. 右键单击目标以显示上下文菜单。
  3. 选择添加操作>全局。箭头()出现在目的地的左侧。
  4. 单击“ 文本”选项卡以导航到XML文本视图。全局操作的XML类似于以下内容:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?xml version="1.0" encoding="utf-8"?>
    <navigation xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main_nav"
    app:startDestination="@id/mainFragment">

    ...

    <action android:id="@+id/action_global_mainFragment"
    app:destination="@id/mainFragment"/>

    </navigation>

使用全局操作

要在代码中使用全局操作,请将全局操作的资源ID传递navigate() 给每个UI元素的方法,如以下示例所示:

1
2
3
viewTransactionButton.setOnClickListener { view ->
view.findNavController().navigate(R.id.action_global_mainFragment)
}

迁移到导航

该NavController导航图及其导航图包含在单个活动中。因此,在迁移现有项目以使用导航架构组件时,请通过为每个活动中的目标创建导航图,重点关注一次迁移一个活动。
活动及其各个导航图
然后,可以通过向导航图添加活动目标来链接单独的活动,从而替换startActivity()整个代码库中的现有用法。

一个Activity中的导航图指向第二个Activity
在多个活动共享相同布局的情况下,可以组合导航图,将导航调用替换为活动目标,以直接在两个导航图之间导航调用。

包含组合导航图的活动。

参考:
[1]. 官网 https://developer.android.com/topic/libraries/architecture/navigation/
[2]. 官网demo https://github.com/googlecodelabs/android-navigation
[3]. 官网教程 https://codelabs.developers.google.com/codelabs/android-navigation/#0
[4]. 关于navigation的解析 https://www.jianshu.com/p/ad040aab0e66

SpringBoot Database H2

发表于 2018-12-24 | 分类于 SpringBoot

前言

内存数据库(Embedded database或in-momery database)具有配置简单、启动速度快、尤其是其可测试性等优点,使其成为开发过程中非常有用的轻量级数据库。在spring中支持HSQL、H2和Derby三种数据库。H2是Thomas Mueller提供的一个开源的、纯java实现的关系数据库。

各数据库特性对比:

H2 Derby HSQLDB MySQL PostgreSQL
Pure Java Yes Yes Yes No No
Memory Mode Yes Yes Yes No No
Encrypted Database Yes Yes Yes No No
ODBC Driver Yes No No Yes Yes
Fulltext Search Yes No No Yes Yes
Multi Version Concurrency Yes No Yes Yes Yes
Footprint (jar/dll size) ~1 MB ~2 MB ~1 MB ~4 MB ~6 MB

准备

  • JDK 1.8或更高版本
  • Maven 3.5或更高版本

技术栈

  • Spring Data JPA
  • Spring Boot

项目目录结构

项目目录结构

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.mugwort</groupId>
<artifactId>spring-web</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>spring-web</name>
<description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<!-- Spring web开发依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 数据库依赖 -->
<!-- h2 嵌入式数据库 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!-- jpa 依赖,持久化操作依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class SystemBean {
private String name;
private String lastaudit;
@Id
@GeneratedValue
@Column(name = "id")
private long id;


public String getLastaudit() {
return lastaudit;
}

public void setLastaudit(String lastaudit) {
this.lastaudit = lastaudit;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public String toString() {
return id + " | " + name + " | " + lastaudit;
}
}
  • @Id 声明此属性为主键。该属性值可以通过应该自身创建,但是Hibernate推荐通过Hibernate生成;
  • @GeneratedValue
    指定主键的生成策略。
    • TABLE:使用表保存id值;
    • IDENTITY:identitycolumn;
    • SEQUENCR :sequence;
    • AUTO:根据数据库的不同使用上面三个,默认值;
  • @Column 声明该属性与数据库字段的映射关系。

数据库操作

1
2
3
4

@Repository
public interface SystemRepository extends CrudRepository<SystemBean, Long> {
}

Spring Data JPA包含了一些内置的Repository,实现了一些常用的方法:findone,findall,save等。

应用配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#####
spring.application.name=Bootstrap Spring boot web with H2
### jpa 只进行更新,默认是启动的时候不管三七二十一就删除原来的表结构重新生成
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
#
#显示SQL语句
spring.jpa.show-sql=true
#
# 每次启动程序,程序都会运行resources/db/schema.sql文件,对数据库的结构进行操作。
spring.datasource.schema=classpath:schema.sql
# 每次启动程序,程序都会运行resources/db/data.sql文件,对数据库的数据操作。
spring.datasource.data=classpath:data.sql
#### 驱动类
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#When using database URLs like jdbc:h2:~/test, the database is stored in the user directory. For Windows, this is usually C:\Documents and #Settings\<userName> or C:\Users\<userName>. If the base directory is not set (as in jdbc:h2:./test), the database files are stored in the directory where #the application is started (the current working directory). When using the H2 Console application from the start menu, this is <Installation Directory>/#bin. The base directory can be set in the database URL. A fixed or relative path can be used. When using the URL jdbc:h2:file:./data/sample, the database #is stored in the directory data (relative to the current working directory). The directory is created automatically if it does not yet exist. It is also #possible to use the fully qualified directory name (and for Windows, drive name). Example: jdbc:h2:file:C:/data/test
####===== connect to ======
##这里设置的是数据存储在内存中,也可以存储在磁盘
spring.datasource.url=jdbc:h2:mem:h2test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.name=sa
spring.datasource.password=
spring.datasource.platform=h2
### h2 properties
spring.h2.console.path=/console
spring.h2.console.enabled=true
###### web 访问接口
server.port=8081
### 日志
logging.level.root=info

在application.properties文件中对数据库进行连接配置

spring.datasource.url=jdbc:h2:mem:h2test,配置h2数据库的连接地址
spring.datasource.driver-class-name=org.h2.Driver,配置JDBC Driver
spring.datasource.username=sa,配置数据库用户名
spring.datasource.password=,配置数据库密码

当你完成依赖和连接配置这两步之后,你就可以在程序种使用h2了。spring会自动帮你完成DataSource的注入。

数据初始化配置

如果你需要在程序启动时对数据库进行初始化操作,则在application.properties文件中对数据库进接配置

spring.datasource.schema=classpath:schema.sql,进行该配置后,每次启动程序,程序都会运行resources/schema.sql文件,对数据库的结构进行操作。
spring.datasource.data=classpath:data.sql,进行该配置后,每次启动程序,程序都会运行resources/data.sql文件,对数据库的数据操作。

该配置非常适合开发环境,我会把数据库的结构构建sql放在resources/schema.sql,数据sql放在resources/data.sql中。这样每次运行程序我都可以得到一个新的数据库。这样就不需要我每次为了测试而修改数据中的内容了。
schema.sql代码:

1
2
3
4
5
6
7
DROP  TABLE IF EXISTS  system_bean;
create table system_bean(
id int not null auto_increment,
name varchar(100) not null ,
lastaudit date not null,
primary key (id)
)

data.sql代码:

1
2
3
INSERT INTO system_bean(name,lastaudit)VALUES('Windows Server 2012 R2 ','2017-08-11');
INSERT INTO system_bean(name,lastaudit)VALUES('RHEL 7','2017-07-21');
INSERT INTO system_bean(name,lastaudit)VALUES('Solaris 11','2017-08-13');

h2 web consloe配置

h2 web consloe是一个数据库GUI管理应用,就和phpMyAdmin类似。程序运行时,会自动启动h2 web consloe。当然你也可以进行如下的配置。

spring.h2.console.settings.web-allow-others=true,进行该配置后,h2 web consloe就可以在远程访问了。否则只能在本机访问。
spring.h2.console.path=/h2-console,进行该配置,你就可以通过YOUR_URL/h2-console访问h2 web consloe。YOUR_URL是你程序的访问URl。
spring.h2.console.enabled=true,进行该配置,程序开启时就会启动h2 web consloe。当然这是默认的,如果你不想在启动程序时启动h2 web consloe,那么就设置为false。

参考:
[1]. https://juejin.im/post/5ab4b339f265da238c3a9d0a
[2]. https://412887952-qq-com.iteye.com/blog/2322756

Android pie slice开发小记

发表于 2018-10-12 | 分类于 Android

Android pie slice开发小记

slice创建和运行

  1. 首先新建一个项目并选择使用的api为API 28:Android 9(pie)或者在已有的项目调整其编译、最低和目标版本为28(最低)。
  2. 创建 slice provider:
    new slice step
    当生成slice provider后,编译器会报如下错误:
    1
    2
    3
    Manifest merger failed : Attribute application@appComponentFactory value=(androidx.core.app.CoreComponentFactory) from [androidx.core:core:1.0.0] AndroidManifest.xml:22:18-86
    is also present at [com.android.support:support-compat:28.0.0] AndroidManifest.xml:22:18-91 value=(android.support.v4.app.CoreComponentFactory).
    Suggestion: add 'tools:replace="android:appComponentFactory"' to <application> element at AndroidManifest.xml:5:5-35:19 to override.

以上错误主要是android在api 28版本后,appconmpat、cardview和constraintlayout等都移到Androidx包名下,所以修正方式为:

1
2
3
4
5
6
7
8
9
10
11
12
      implementation fileTree(dir: 'libs', include: ['*.jar'])
// implementation 'com.android.support:appcompat-v7:28.0.0'
// implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'androidx.appcompat:appcompat:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

implementation 'androidx.annotation:annotation:1.0.0'
implementation 'androidx.slice:slice-builders:1.0.0'

testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.0-alpha3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0-alpha3'

参考地址:https://stackoverflow.com/questions/50782435/android-design-support-library-for-api-28-p-not-working

  1. sliceProvider编码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    public class PieSliceProvider extends SliceProvider {
    /**
    * Instantiate any required objects. Return true if the provider was successfully created,
    * false otherwise.
    */
    @Override
    public boolean onCreateSliceProvider() {
    return true;
    }

    /**
    * Converts URL to content URI (i.e. content://com.mugwort.demo...)
    */
    @Override
    @NonNull
    public Uri onMapIntentToUri(@Nullable Intent intent) {
    // Note: implementing this is only required if you plan on catching URL requests.
    // This is an example solution.
    Uri.Builder uriBuilder = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT);
    if (intent == null) return uriBuilder.build();
    Uri data = intent.getData();
    if (data != null && data.getPath() != null) {
    String path = data.getPath().replace("/", "");
    uriBuilder = uriBuilder.path(path);
    }
    Context context = getContext();
    if (context != null) {
    uriBuilder = uriBuilder.authority(context.getPackageName());
    }
    return uriBuilder.build();
    }

    /**
    * Construct the Slice and bind data if available.
    */
    public Slice onBindSlice(Uri sliceUri) {
    Context context = getContext();
    SliceAction activityAction = createActivityAction();
    if (context == null || activityAction == null) {
    return null;
    }
    if ("/".equals(sliceUri.getPath())) {
    // Path recognized. Customize the Slice using the androidx.slice.builders API.
    // Note: ANRs and strict mode is enforced here so don't do any heavy operations.
    // Only bind data that is currently available in memory.
    return new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY)
    .addRow(
    new RowBuilder()
    .setTitle("URI found.hello i'am here")
    .setPrimaryAction(activityAction)
    )
    .build();
    } else {
    // Error: Path not found.
    return new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY)
    .addRow(
    new RowBuilder()
    .setTitle("URI not found.")
    .setPrimaryAction(activityAction)
    )
    .build();
    }
    }

    private SliceAction createActivityAction() {
    //return null;
    //Instead of returning null, you should create a SliceAction. Here is an example:
    return SliceAction.create(
    PendingIntent.getActivity(
    getContext(), 0, new Intent(getContext(), MainActivity.class), 0
    ),
    IconCompat.createWithResource(getContext(), R.drawable.ic_launcher_foreground),
    ListBuilder.ICON_IMAGE,
    "Open App"
    );
    }

    /**
    * Slice has been pinned to external process. Subscribe to data source if necessary.
    */
    @Override
    public void onSlicePinned(Uri sliceUri) {
    // When data is received, call context.contentResolver.notifyChange(sliceUri, null) to
    // trigger PieSliceProvider#onBindSlice(Uri) again.
    }

    /**
    * Unsubscribe from data source if necessary.
    */
    @Override
    public void onSliceUnpinned(Uri sliceUri) {
    // Remove any observers if necessary to avoid memory leaks.
    }
    }

分析:

  • 通过类我们发现SliceProvider继承于ContentProvider,其APP间数据的传递通过
    ContentProvider的方式,应用APP向搜索APP对外提供其对应Slice的Uri,封装成Slice对象通过Parcelable序列化的方式实现APP之间的数据传递。
  • slice的绑定和展示:onBindSlice,
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public Slice onBindSlice(Uri sliceUri) {
    Context context = getContext();
    SliceAction activityAction = createActivityAction();
    if (context == null || activityAction == null) {
    return null;
    }
    //“/”是在manifest中定义的
    if ("/".equals(sliceUri.getPath())) {
    // Path recognized. Customize the Slice using the androidx.slice.builders API.
    // Note: ANRs and strict mode is enforced here so don't do any heavy operations.
    // Only bind data that is currently available in memory.
    return new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY)
    .addRow(
    new RowBuilder()
    .setTitle("URI found.hello i'am here")
    .setPrimaryAction(activityAction)
    )
    .build();
    } else {
    // Error: Path not found.
    return new ListBuilder(getContext(), sliceUri, ListBuilder.INFINITY)
    .addRow(
    new RowBuilder()
    .setTitle("URI not found.")
    .setPrimaryAction(activityAction)
    )
    .build();
    }
    }

Uri的scheme统一为content,如上述例子的Uri为:
content://x.x.x/

  • SliceAction,这个类似notifications,可以使用PendingIntents 来处理用户的点击事件,比如点击Slice模块打开宿主APP:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private SliceAction createActivityAction() {
    //return null;
    //Instead of returning null, you should create a SliceAction. Here is an example:
    return SliceAction.create(
    PendingIntent.getActivity(
    getContext(), 0, new Intent(getContext(), MainActivity.class), 0
    ),
    IconCompat.createWithResource(getContext(), R.drawable.ic_launcher_foreground),
    ListBuilder.ICON_IMAGE,
    "Open App"
    );
    }
  • 通过安装
    SliceViewer,在搜索框输入uri即可添加slice并跳转:
    SliceViewer

Slice模板

  • ListBuilder:Slices通过ListBuilder类来创建。在ListBuilder中,你可以添加不同类型的行模块在你的Slice中进行展示。
  • SliceAction:对于每一个Slice来说,最基础的构造实现类是SliceAction,在SliceAction你可以添加PendingIntent来实现用户操作,比如Toggle选择操作:
    1
    SliceAction toggleAction =SliceAction.createToggle(createToggleIntent(),"Toggle adaptive brightness",true);

SliceAction可以配置在搜索APP中显示的模块三种不同的显示方式:

  ICON_IMAGE:tiny size and tintable:
shortcut

SMALL_IMAGE:small size and non-tintable:
small

  LARGE_IMAGE: largest size and non-tintable:
large

模块构造Builder

  对于每个Slice模块的创建构造,谷歌官方提供了HeaderBuilder、RowBuilder、GridBuilder、RangeBuilder模块四种构造器。其中,HeaderBuilder只支持一行头部的展示view;RowBuilder可以添加一行view进行展示,如此前没有添加header,则首行row默认为header;GridBuilder支持上述所说的三种模块展示方式;而RangeBuilder则支持进度条相关的view展示。

延时加载

  对于一些需要耗时加载数据的操作,比如网络请求图片等,可以采取与ListView加载图片类似的方法,先本地加载一个默认的占位数据,等耗时操作完成回调回来真实数据的时候调用getContentResolver().notifyChange(sliceUri) 方法,通知搜索APP调用Slice Uri,完成真实数据的显示。

结语

  Slice的功能模板非常的强大,通过不同的builder组合可以在搜索模块中搭配出丰富多彩的Slice,快速直达用户想要的功能。但是Slice只提供了三种模板,自带模板中对安卓原生控件的支持有所欠缺,比如ScollView等,可能需要用户自定义自己的模板才能实现更强大的功能。

参考:
[0].https://www.androidauthority.com/android-slices-872250/
[1].https://www.jianshu.com/p/a90563606e1f
[2].http://digi.aili.com/1642/2811309.html

基础数据与byte间的转化

发表于 2018-08-27 | 分类于 Java

基础知识

计算机中存储是用补码!!,同时注意一下计算省略了高位不变部分和 字节大小端问题

基础数据

  • byte:有符号,在网络传输中都是会用到byte的,它占1个字节,共8位,比如说11111111就可以用1个
    byte表示,转化为10进制:- (2的6次+2的5次+2的4次+2的3次+2的2次+2的1次+2的0次) = -127。
    其中前7位表示数字,最高位表示符号,0为正,1为负。范围是 (-2的7次 ~ 2的7次 - 1),那为什么前
    面最小是-127,范围最小又是-128呢?因为规定-0(10000000)为-128。
  • short:有符号,占2个字节,共16位。同byte一样,它的取值范围就是 (-2的15次 ~ 2的15次 - 1)。
  • int:有符号,占4个字节,共32位。它的取值范围就是(-2的31次 ~ 2的31次)。
  • long:有符号,占8个字节,共64位,它的取值范围就是(-2的63次 ~ 2的63次)。

位运算

  • ^:表示异或位运算,两者相同则为0,两者不同则为1。比如说15^2,15用二进制表示就是1111,2用2
    进制表示就是0010,两者进行异或运算,结果就是1101,转换为十进制就是13。
A B A^B
1 1 0
1 0 1
0 1 1
0 0 0
  • |:表示或运算,两者只有有一个为1就为1, 比如说13|2,13用二进制表示就是1101,2用二进制表示
    就是0010,两者进行或运算,那么结果就是1111,转换为十进制就是15。
A B A | B
1 1 1
1 0 1
0 1 1
0 0 0
  • &:表示与运算,两者都为1就为1,其余都为0,比如说15&2, 13用二进制表示就是1111,2用二进制
    表示就是0010, 两者进行与运算,那么结果就是0010,转换为十进制就是2。
A B A&B
1 1 1
1 0 1
0 1 1
0 0 0
  • ~:取反,就是本来是0变成1,本来是1变成0。
A ~A
1 0
0 1
  • <<:左移运算符,不需要考虑符号位,因为在后面补0,把二进制的数字向左移动,低位补0。比如说 3<<2。
    3的二进制码为11,向左移动2位就是1100,那么结果就是12。

  • >>:右移运算符,带符号位。根据这个要移动的数字决定正负,如果是正数,那么就在前面补0,如果是
    负数就在前面补1。比如说 3>>2,3的二进制码为00000011,向右移动2位,就变成00000000,转化为十
    进制就是0了(3是正数所以在高位补0)。再比如说-3>>2,-3的二进制码为10000011,转化为补码
    (只要涉及到负数,就需要转换到补码计算,正数之所以不用转化,是因为他们的补码和原码是一致的)
    11111101,进行位移运算,就是11111111,这个是补码,需要再转回原码。那么就是取反+1,结果就是
    10000001,转化为十进制就是-1。

  • >>>:右移运算符,与上面的区别就是这个运算符是 无符号的。不论正负,高位都补0。如果要用
    -3>>2来验证,因为高位补0,就需要把所有的位数都写全了。如果是int,那就要写成32位来运算。切记切记。
    上面很多因为高位的变化“取反再取反”会导致前面所有的位都没变所以我就简化了,但是这个>>>运算符不行哦,
    它会把负数变为正数。

栗子(eg):
比如说-15|3等于多少呢?有些人会觉得-15转化为二进制就是10001111,而3转化为二进制就是00000011,
那么结果应该是10001111呀,转换为十进制就是-15呀?大家可以自己写个demo就会发现是不对的。要注意
在计算机中所有的都是用补码的形式存储的,之所以上面介绍两个正数是对的,因为 正数的反码和补码都是一样的。而负数求补码应该是除去符号位取反+1,我们再来看看这个题-15|3,其中-15的原码为10001111,反码为11110000,那么补码就是11110001,然后3的补码为00000011,两者进行或操作就是11110011,你以为结束了么?还没有,再又要求它的原码了,原码就是补码再求补码再+1(是不是已经晕掉了?),也就是
10001101,结果就是-13。为了大家都好理解,我这里用算式整理一下:

求 -15|3

[-15]原码 = 10001111
[-15]反码 = 11110000 //原码求反码符号位不变
[-15]补码 = 11110001 //反码+1等于补码

[3]原码 = 00000011
[3]反码 = 00000011 //正数都一致
[3]补码 = 00000011 //正数都一致

-15|3 = 11110011 //两个补码进行或操作

[结果]补码 = 11110011 //上面求得的值
[结果]反码 = 10001100 //符号位不变
[结果]原码 = 10001101 //反码+1

100001101 转化为十进制就是-13。
不知道我这么解释会不会更加清楚一些呢?同理的,上面那些(尤其是求反‘~’我故意没写例子,大家自己去试试吧)。

在说一次,正数的原码,补码,反码都一样,如果一定要说为什么一样,我就举个例子。有这么一个等式7+(-7)=0。
我们知道
[-7]原 = 10000111
[-7]反 = 11111000
[-7]补 = 11111001

那么如果要存在一个值和[-7]补码相加等于0,是不是就是00000111!!所以正数的补码和原码是一致的。
这样解释虽然怪怪的,但是可以知道的确是这样的。

代码实现

基本步骤:
① 分析转化和代转化之间位数的关系
② 计算偏移量。每次都取最后8位
③ 把最后8位和0xff进行&操作
④ 得出结果。

有人会问,为什么要和0xff进行&操作?这里解释下,因为再分割的时候,我们要保证最后8位是一致的,更高位都要置0,这样才能保证数据的一致性。比如说由byte转化成short。位数从8位变成了16位,那么在计算机自己看来,它会把前面多出来的8位置1。而&0xff就可以把前面所有的位数都置0。

  1. short与byte的相互转化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

/**
* 前提:在计算机中所有的都是用补码的形式存储的。
* 将short转换为字节
*
* 例如 8的补码为0000 0000 0000 1000,所以要将转换为两个字节,第一个字节是将8右移8位然后和0x00ff求与,
* 而第二个字节则是右移0位,然后求与
*/
public static byte[] short2byte(short s){
byte[] b = new byte[2];
for(int i = 0; i < 2; i++){
int offset = 16 - (i+1)*8; //因为byte占1个字节,所以要计算偏移量
b[i] = (byte)((s >> offset)&0xff); //把16位分为2个8位进行分别存储
}
return b;
}

public static short byte2short(byte[] b){
short l = 0;
for (int i = 0; i < 2; i++) {
l<<=8; //<<=和 +=是一样的,意思就是 l = l << 8
l |= (b[i] & 0xff); //和上面也是一样的 l = l | (b[i]&0xff)
}
return l;
}
  1. int与byte的相互转化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static byte[] int2byte(int s){
byte[] b = new byte[2];
for(int i = 0; i < 4; i++){
int offset = 16 - (i+1)*8; //因为byte占1个字节,所以要计算偏移量
b[i] = (byte)((s >> offset)&0xff); //把32位分为4个8位进行分别存储
}
return b;
}


public static int byte2int(byte[] b){
int l = 0;
for (int i = 0; i < 4; i++) {
l<<=8; //<<=和 +=是一样的,意思就是 l = l << 8
l |= (b[i] & 0xff); //和上面也是一样的 l = l | (b[i]&0xff)
}
return l;
}
  1. long与byte的相互转化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* @方法功能 字节数组和长整型的转换
* @param 字节数组
* @return 长整型
*/
public static byte[] longToByte(long number) {
long temp = number;
byte[] b = new byte[8];
for (int i = 0; i < b.length; i++) {
b[i] = new Long(temp & 0xff).byteValue();
// 将最低位保存在最低位
temp = temp >> 8;
// 向右移8位
}
return b;
}

/**
* @方法功能 字节数组和长整型的转换
* @param 字节数组
* @return 长整型
*/
public static long byteToLong(byte[] b) {
long s = 0;
long s0 = b[0] & 0xff;// 最低位
long s1 = b[1] & 0xff;
long s2 = b[2] & 0xff;
long s3 = b[3] & 0xff;
long s4 = b[4] & 0xff;// 最低位
long s5 = b[5] & 0xff;
long s6 = b[6] & 0xff;
long s7 = b[7] & 0xff; // s0不变
s1 <<= 8;
s2 <<= 16;
s3 <<= 24;
s4 <<= 8 * 4;
s5 <<= 8 * 5;
s6 <<= 8 * 6;
s7 <<= 8 * 7;
s = s0 | s1 | s2 | s3 | s4 | s5 | s6 | s7;
return s;
}
  1. char与byte的转化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 字符到字节转换
*
* @param ch
* @return
*/
public static void putChar(byte[] bb, char ch, int index) {
int temp = (int) ch;
// byte[] b = new byte[2];
for (int i = 0; i < 2; i ++ ) {
bb[index + i] = new Integer(temp & 0xff).byteValue(); // 将最高位保存在最低位
temp = temp >> 8; // 向右移8位
}
}

/**
* 字节到字符转换
*
* @param b
* @return
*/
public static char getChar(byte[] b, int index) {
int s = 0;
if (b[index + 1] > 0)
s += b[index + 1];
else
s += 256 + b[index + 0];
s *= 256;
if (b[index + 0] > 0)
s += b[index + 1];
else
s += 256 + b[index + 0];
char ch = (char) s;
return ch;
}
  1. float与byte的转化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* float转换byte
*
* @param bb
* @param x
* @param index
*/
public static void putFloat(byte[] bb, float x, int index) {
// byte[] b = new byte[4];
int l = Float.floatToIntBits(x);
for (int i = 0; i < 4; i++) {
bb[index + i] = new Integer(l).byteValue();
l = l >> 8;
}
}

/**
* 通过byte数组取得float
*
* @param bb
* @param index
* @return
*/
public static float getFloat(byte[] b, int index) {
int l;
l = b[index + 0];
l &= 0xff;
l |= ((long) b[index + 1] << 8);
l &= 0xffff;
l |= ((long) b[index + 2] << 16);
l &= 0xffffff;
l |= ((long) b[index + 3] << 24);
return Float.intBitsToFloat(l);
}
  1. double与byte的转化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* double转换byte
*
* @param bb
* @param x
* @param index
*/
public static void putDouble(byte[] bb, double x, int index) {
// byte[] b = new byte[8];
long l = Double.doubleToLongBits(x);
for (int i = 0; i < 4; i++) {
bb[index + i] = new Long(l).byteValue();
l = l >> 8;
}
}

/**
* 通过byte数组取得float
*
* @param bb
* @param index
* @return
*/
public static double getDouble(byte[] b, int index) {
long l;
l = b[0];
l &= 0xff;
l |= ((long) b[1] << 8);
l &= 0xffff;
l |= ((long) b[2] << 16);
l &= 0xffffff;
l |= ((long) b[3] << 24);
l &= 0xffffffffl;
l |= ((long) b[4] << 32);
l &= 0xffffffffffl;
l |= ((long) b[5] << 40);
l &= 0xffffffffffffl;
l |= ((long) b[6] << 48);
l &= 0xffffffffffffffl;
l |= ((long) b[7] << 56);
return Double.longBitsToDouble(l);
}
  1. boolean与byte的相互转化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 将boolean转成byte[]
* @param val
* @return byte[]
*/
public static byte[] Boolean2ByteArray(boolean val) {
int tmp = (val == false) ? 0 : 1;
return ByteBuffer.allocate(4).putInt(tmp).array();
}

/**
* 将byte[]转成boolean
* @param data
* @return boolean
*/
public static boolean ByteArray2Boolean(byte[] data) {
if (data == null || data.length < 4) {
return false;
}
int tmp = ByteBuffer.wrap(data, 0, 4).getInt();
return (tmp == 0) ? false : true;
}

参考地址:
[1].long、shrot和int转化 https://blog.csdn.net/u012403290/article/details/68943827
[2].long、shrot和int转化 https://blog.csdn.net/thanklife/article/details/17002641
[3]. char、float、double与byte转化 https://blog.csdn.net/LEEtcWorks/article/details/7390731
[4]. 通过buffer转换 https://blog.csdn.net/u010983881/article/details/60870360

NIO-Buffer解析

发表于 2018-08-23 | 分类于 Java

       在NIO中,数据的读写操作始终是与缓冲区相关联的。Channel将数据
读入缓冲区,然后我们又从缓冲区访问数据。写数据时,首先将要发送的数据按顺序填入缓冲区。基本上,
缓冲区只是一个列表,它的所有元素都是基本数据类型(通常为字节型)。缓冲区是定长的,它不像一些类
那样可以扩展容量(例如,List,StringBuffer等)。注意,ByteBuffer是最常用的缓冲区,因为:1)
它提供了读写其他数据类型的方法,2)信道的读写方法只接收ByteBuffer。

Buffer索引

缓冲区不仅仅是用来存放一组元素的列表。在读写数据时,它有内部状态来跟踪缓冲区的当前位置,以及有效
可读数据的结束位置等。为了实现这些功能,每个缓冲区维护了指向其元素列表的4个索引,如下表所示。
(不久我们将看到如何使用缓冲区的各种方法来修改索引值。)

索引 描述 存取器/修改器/用法
capacity 缓冲区的元素总是(不可修改) int capacity()
position 下一个要读/写的元素(从0开始) int position(), Buffer position(int newPosition)
limit 第一个不可读/写元素 int limit(), Buffer limit(int newLimit)
mark 用户选定的position 高的前一个位置或0 Buffer mark(),Buffer reset()

position和limit之间的距离指示了可读取/存入的字节数,java中提供了两个方便的方法来计算这个距离。

  • boolean hasRemaining():当缓冲区至少还有一个元素时,返回true;
  • int remaining():返回缓冲区包含的元素个数;
    在这些变量中,始终保持以下关系不变:
    0 ≤ mark ≤ position ≤ limit ≤ capacity
    mark变量的值“记录”了一个将来可返回的位置,reset()方法则将postion的值还原成上次调用mark()
    方法后的position值(除非这样做会违背上面的不变关系);

Buffer创建

       通常使用分配空间或包装一个现有的基本类型数组来创建缓冲区。创建ByteBuffer的静态工厂方法,以及
相应的capacity,position,和limit的初始值下表。所有新创建的Buffer实例都没有定义其mark值,
在调用mark()方法前,任何试图使用reset()方法来设置position的值的操作都将抛出InvalidMarkException异常。

方法 capacity position limit
ByteBuffer allocate(int capacity) capacity 0 capacity
ByteBuffer allocateDirect(int capacity) capacity 0 capacity
ByteBuffer wrap(byte[] array) array.length 0 array.length
ByteBuffer wrap(byte[] array,int offer,int length) array.length offset offset-array.length

要分配一个新的实例,只需要简单地调用想要创建的缓冲区类型的allocate()静态方法,并指定元素的总数:

1
2
ByteBuffer byteBuf = ByteBuffer.allocate(20);
DoubleBuffer dblBuf = DoubleBuffer.allocate(5);

       在上面代码中,byteBuf分配了20个字节,dblBuf分配了5个Java的
double型数据。这些缓冲区都是定长的,因此无法扩展或缩减它们的容量。如果发现刚创建的缓冲区容量太小,
惟一的选择就是重新创建一个大小合适的缓冲区。
还可以通过调用wrap()静态方法,以一个已有的数组为参数,来创建缓冲区:

1
2
3
4
byteArray[] = new byte[BUFFERSIZE];
// ...Fill array...
ByteBuffer byteWrap = ByteBuffer.wrap(byteArray);
ByteBuffer subByteWrap = ByteBuffer.wrap(byteArray, 3,3);

       通过包装的方法创建的缓冲区 保留了被包装数组内保存的数据。实际上,
wrap()方法只是简单地创建了一个具有指向被包装数组的引用的缓冲区,该数组称为 后援数组。对后援数组
中的数据做的任何修改都将改变缓冲区中的数据,反之亦然。如果我们为wrap()方法指定了偏移量(offset)
和长度(length),缓冲区将使用整个数组为后援数组,同时将position和limit的值初始化为偏移量
(offset)和偏移量+长度(offset+length)。在偏移量之前和长度之后的元素依然可以通过缓冲区访问。
使用分配空间的方式来创建缓冲区其实与使用包装的方法区别不大。惟一的区别是allocate()方法创建了
自己的后援数组。在缓冲区上调用array()方法即可获得后援数组的引用。通过调用arrayOffset()方法,
甚至还可以获取缓冲区中第一个元素在后援数组中的偏移量。使用wrap()方法和非零偏移量参数创建的缓冲区,
其数组偏移量依然是0。
       到目前为止,我们实现的所有缓冲区都将数据存放在Java分配的后援数组中。
通常,底层平台(操作系统)不能使用这些缓冲区进行I/O操作。操作系统必须使用自己的缓冲区来进行I/O,
并将结果复制到缓冲区的后援数组中。这些复制过程 可能非常耗费系统资源,尤其是在有很多读写需求的时候。
Java的NIO提供了一种 直接缓冲区(direct buffers) 来解决这个问题。使用直接缓冲区,Java将从平台
能够直接进行I/O操作的存储空间中为缓冲区分配后援存储空间,从而省略了数据的复制过程。这种低层的、
本地的I/O通常在字节层进行操作,因此只能为 ByteBuffer进行直接缓冲区分配 。

1
ByteBuffer byteBufDirect =ByteBuffer.allocateDirect(BUFFERSIZE);

       通过调用isDirect()方法可以查看一个缓冲区是否是直接缓冲区。由于
直接缓冲区没有后援数组,在它上面调用array()或arrayOffset()方法都将抛出
UnsupportedOperationException异常。在考虑是否使用直接缓冲区时需要牢记几点。首先,要知道调用
allocateDirect()方法并不能保证能成功分配直接缓冲区–有的平台或JVM可能不支持这个操作,因此在
尝试分配直接缓冲区后必须调用isDirect()方法进行检查。其次,要知道 分配和销毁直接缓冲区通常比分配
和销毁非直接缓冲区要消耗更多的系统资源
,因为直接缓冲区的后援存储空间通常存在与JVM之外,对它的管理
需要与操作系统进行交互。所以,只有当需要在很多I/O操作上长时间使用时,才分配直接缓冲区。实际上,
在相对于非直接缓冲区能明显提高系统性能时,使用直接缓冲区是个不错的主意。

存储和接受数据

       只要有了缓冲区,就可以用它来存放数据了。作为数据的”容器”,缓冲区
既可用来输入也可用来输出。这一点就与流不同,流只能向一个方向传递数据。使用put()方法可以将数据放入
缓冲区,使用get()方法则可以从缓冲区获取数据。信道的read()方法隐式调用了给定缓冲区的put(),而其
write()方法则隐式调用了缓冲区的get()方法。下面展示ByteBuffer的get()和put()方法,当然,其他
类型的缓冲区也有类似的方法。
ByteBuffer:获取和存放字节,有两种类型的get()和put():基于相对位置和基于绝对位置。基于相对位置
的版本根据position的当前值,从”下一个”位置读取或存放数据,然后根据数据量给position增加适当的值
(即,单字节形式增加1数组形式增加array.length, 数组/偏移量/长度形式则增加length)。也就是
说,每次调用put()方法,都是在缓冲区中的已有元素后面追加数据,每次调用get()方法,都是读取缓冲区的
后续元素。不过,如果这些操作会导致position的值超出limit的限制,get()方法将抛出BufferUnderflowException异常,
put()方法将抛出BufferOverflowException异常。例如,如果传给get()方法的目标数组长度大于缓冲区
的剩余空间大小,get()方法将抛出BufferUnderflowException异常,部分数据的get/put是不允许的。
基于绝对位置的get()和put()以指定的索引位置为参数,从该位置读取数据或向该位置写入数据。绝对位置
形式的get和put不会改变position的值。如果给定的索引值超出了limit的限制,它们将抛出IndexOutOfBoundsException异常。
除了字节类型外,ByteBuffer类还提供了其他类型数据的相当位置和绝对位置的get/put方法。这样一来,就有点像DataOutputStream了。

  • 相对位置:
    • byte get()
    • ByteBuffer get(byte[] dst)
    • ByteBuffer get(byte[] dst, int offset, int length)
    • ByteBuffer put(byte b)
    • ByteBuffer put(byte[] src)
    • ByteBuffer put(byte[] src, int offset, int length)
    • ByteBuffer put(ByteBuffer src)
  • 绝对位置:

    • byte get(int index)
    • ByteBuffer put(int index, byte b)
  • ByteBuffer:读取和存放Java多字节基本数据

    • get()
    • get(int index)
    • ByteBuffer put( value)
    • ByteBuffer put(int index, value)

       其中”“代表Char,Double,Int,Long,Short之一,
而”“代表char,double,int,long,short之一。
每次调用基于相对位置的put()或get()方法,都将根据特定参数类型的长度增加position的值:short加2,
int加4,等。不过,如果这样做会导致position的值超出limit的限制,get()和put()方法将分别抛出
BufferUnderflowException和BufferOverflowException异常:get和put不允许只对部分数据进行操作。
发生了下溢/上溢(under/overflow)时,position的值不变。可能你已经注意到,很多get/put方法都
返回一个ByteBuffer。实际上它们返回的就是调用它们的那个ByteBuffer。这样做可以实现 链式调用(call chaining),
即第一次调用的结果可以直接用来进行后续的方法调用。例如,可以像下面那样将整数1和2存入ByteBuffer实例
myBuffer中:myBuffer.putInt(1).putInt(2);
       多字节数据类型有一个字节顺序,称为big-endian或little-endian。
Java默认使用big-endian
。通过使用内置的ByteOrder.BIG_ENDIAN和ByteOrder.LITTLE_ENDIAN实例,
可以获取和设定多字节数据类型写入字节缓冲区时的字节顺序。

  • ByteBuffer:缓冲区中的字节顺序
    • ByteOrder order()
    • ByteBuffer order(ByteOrder order)
      第一个方法以ByteOrder常量的形式返回缓冲区的当前字节顺序。第二个方法用来设置写多字节数据时的字节顺序。

下面来看一个使用字节顺序的例子:

1
2
3
4
5
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.putShort((short) 1);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putShort((short) 1);
// Predict the byte values for buffer and test your prediction

       看了这些有关字节顺序的讨论,你可能希望知道自己的处理器是什么字节
顺序,ByteOrder定义了一个方法来解答这个问题:
ByteOrder:查找字节顺序
static final ByteOrder BIG_ENDIAN
static final ByteOrder LITTLE_ENDIAN
static ByteOrder nativeOrder()
nativeOrder()方法返回常量BIG_ENDIAN或LITTLE_ENDIAN之一。

Buffer准备:clear()、flip()和rewind()

在使用缓冲区进行输入输出数据之前,必须确定缓冲区的position,limit都已经设置了正确的值。首先
我们看看上面三个方法对position和limit的修改操作:

ByteBuffer方法 准备Buffer以实现 position limit mark
clear() 将数据read()/put()进缓冲区 0 capacity 为定义
flip() 从缓冲区write()/get() 0 position 为定义
rewind() 从缓冲区rewrite()/get() 0 unchanged 为定义

三种在Buffer类的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
* Clears this buffer. The position is set to zero, the limit is set to
* the capacity, and the mark is discarded.
*
* <p> Invoke this method before using a sequence of channel-read or
* <i>put</i> operations to fill this buffer. For example:
*
* <blockquote><pre>
* buf.clear(); // Prepare buffer for reading
* in.read(buf); // Read data</pre></blockquote>
*
* <p> This method does not actually erase the data in the buffer, but it
* is named as if it did because it will most often be used in situations
* in which that might as well be the case. </p>
*
* @return This buffer
*/
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}

/**
* Flips this buffer. The limit is set to the current position and then
* the position is set to zero. If the mark is defined then it is
* discarded.
*
* <p> After a sequence of channel-read or <i>put</i> operations, invoke
* this method to prepare for a sequence of channel-write or relative
* <i>get</i> operations. For example:
*
* <blockquote><pre>
* buf.put(magic); // Prepend header
* in.read(buf); // Read data into rest of buffer
* buf.flip(); // Flip buffer
* out.write(buf); // Write header + data to channel</pre></blockquote>
*
* <p> This method is often used in conjunction with the {@link
* java.nio.ByteBuffer#compact compact} method when transferring data from
* one place to another. </p>
*
* @return This buffer
*/
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}

/**
* Rewinds this buffer. The position is set to zero and the mark is
* discarded.
*
* <p> Invoke this method before a sequence of channel-write or <i>get</i>
* operations, assuming that the limit has already been set
* appropriately. For example:
*
* <blockquote><pre>
* out.write(buf); // Write remaining data
* buf.rewind(); // Rewind buffer
* buf.get(array); // Copy data into array</pre></blockquote>
*
* @return This buffer
*/
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}

解释:(参考:https://blog.csdn.net/FS1360472174/article/details/52141800)

  • clear: clear 并没有真正的清除数据,将position设置为0,limit设置为capacity;
  • flip :一般是切换到读操作。或者是为写操作准备一个新的序列
    eg:重复向一个ByteBuffer写数据的时候,赋值完毕,必须要flip.开始一个新的新序列,否则position
    会等于limit,返回空值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public static void main(String[] args) {
    byte[] bytes1=new byte[]{1, 6, 3};
    ByteBuffer buffer =fromByteArray(bytes1);
    System.out.println(buffer);
    byte[] bytes2 =new byte[]{1,2,3};
    ByteBuffer buffer2=fromByteArray(bytes2);
    System.out.println(buffer2);
    }

    /**
    * If you are building up a ByteBuffer by repeatedly writing into it, and then want to give it away, you must remember to flip() it.
    * If we did not flip() it, the returned ByteBuffer would be empty because the position would be equal to the limit.
    * @param bytes
    * @return
    */
    public static ByteBuffer fromByteArray(byte[] bytes) {
    final ByteBuffer ret = ByteBuffer.wrap(new byte[bytes.length]);

    ret.put(bytes);
    ret.flip();

    return ret;
    }
    }
  • rewind:倒回,将position 设置为0,重新读取;

压缩Buffer中数据

compact()方法将position与limit之间的元素复制到缓冲区的开始位置,从而为后续的put()/read()调
用让出空间。position的值将设置为要复制的数据的长度,limit的值将设置为capacity,mark则变成未定义。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] arg) {
byte[] bytes = new byte[]{1, 6, 3, 1, 1, 1, 1, 1, 1, 1, 1};

ByteBuffer buffer = ByteBuffer.wrap(bytes);
byte b = buffer.get();
System.out.println("b:" + b + " position:" + buffer.position()+" limit:"+buffer.limit()+" capacity:"+buffer.capacity());
buffer.compact();
System.out.println("compact position:" + buffer.position()+" limit:"+buffer.limit()+" capacity:"+buffer.capacity());
buffer.flip();
System.out.println("flip position:" + buffer.position()+" limit:"+buffer.limit()+" capacity:"+buffer.capacity());

}

结果如下:
b:1 position:1 limit:11 capacity:11
compact position:10 limit:11 capacity:11
flip position:0 limit:10 capacity:11

       为什么要使用这个操作呢?假设你有一个缓冲区要写数据。回顾前面的内容我们知道,对write()方法的非阻塞调用只会写出其能够发送的数据,而不会阻塞等待所有数据发送完。因此write()方法不一定会将缓冲区中的所有元素都发送出去。又假设现在要调用read()方法,在缓冲区中没有发送的数据后面读入新数据。处理方法之一就是简单地设置position = limit和limit = capacity。当然,在读入新数据后,再次调用write()方法前,还需要将这些值还原。这样做有个问题即缓冲区的空间最终将消耗殆尽,如上图中,只剩下一个元素位置可以再存入一个字节。此外,缓冲区前面的空间又被浪费掉了。这就是compact()方法要解决的问题。在调用write()方法后和添加新数据的read()方法前调用compact()方法,则将所有”剩余”的数据移动到缓冲区的开头,从而为释放最大的空间来存放新数据。

1
2
3
4
5
6
7
8
// Start with buffer ready for reading
while (channel.read(buffer) != -1) {
buffer.flip();
channel.write(buffer);
buffer.compact();
}
while (buffer.hasRemaining())
channel.write(buffer);

注意,如开始已经提到的,复制数据是一个非常耗费系统资源的操作,因此要保守地使用compact()方法。

Buffer透视:duplicate(),slice()等

       NIO提供了多种方法来创建一个与给定缓冲区共享内容的新缓冲区,这些方法对元素的处理过程各有不同。基本上,这种新缓冲区有自己独立的状态变量(position,limit,capacity和mark),但与原始缓冲区共享了同一个后援存储空间。任何对新缓冲区 内容 的修改都将反映到 原始缓冲区上 。可以将新缓冲区看作是从另一个角度对同一数据的透视。

duplicate()方法用于创建一个与原始缓冲区共享内容的新缓冲区。新缓冲区的position,limit,mark和capacity都初始化为原始缓冲区的索引值,然而,它们的这些值是相互独立的。如下表格为在ByteBuffer上创建不同透视的方法:

方法 capacity position limit mark
duplicate() capacity position limit mark
slice() remaining() 0 remaining() 为定义
asReadOnlyBuffer() capacity position limit mark
asCharBuffer() remaining()/2 0 remaining()/2 为定义
asDoubleBuffer() remaining()/4 0 remaining()/4 为定义
asFloatBuffer() remaining()/4 0 remaining()/4 为定义
asIntBuffer() remaining()/4 0 remaining()/4 为定义
asLongBuffer() remaining()/8 0 remaining()/8 为定义
asShortBuffer() remaining()/2 0 remaining()/2 为定义

       由于共享了内容,对原始缓冲区或任何复本所做的改变在所有复本上都可见。
下面回到前面的例子,假设要将在网络上发送的所有数据都写进日志。

1
2
3
4
5
6
7
// Start with buffer ready for writing
ByteBuffer logBuffer = buffer.duplicate();
while (buffer.hasRemaining()) // Write all data to network
networkChannel.write(buffer);
while (logBuffer.hasRemaining()) // Write all data to
//logger
loggerChannel.write(buffer);

注意,使用了缓冲区复制操作,向网络写数据和写日志就可以在不同的线程中并行进行。slice()方法用于创建一个共享了原始缓冲区子序列的新缓冲区。新缓冲区的position值是0,而其limit和capacity的值都等于原始缓冲区的limit和position的差值。slice()方法将新缓冲区数组的offset值设置为原始缓冲区的position值,然而,在新缓冲区上调用array()方法还是会返回整个数组。
Channel在读写数据时只以ByteBuffer为参数,然而我们可能还对使用其他基本类型的数据进行通信感兴趣。ByteBuffer能够创建一种独立的”视图缓冲区(view buffer)”,用于将ByteBuffer的内容解释成其他基本类型(如CharBuffer)。这样就可以从该缓冲区中读取(写入数据是可选操作)新类型的数据。新缓冲区与原始缓冲区共享了同一个后援存储空间,因此,在 任一缓冲区上的修改在新缓冲区和原始缓冲区上都可以看到。新创建的视图缓冲区的position值为0,其内容从原始缓冲区的position所指位置开始。这与slice()操作非常相似。不过,由于视图缓冲区操作的是多字节元素,新缓冲区的capacity和limit的值等于剩余总字节数除以每个该类型元素对应的字节数(例如,创建DoubleBuffer时则除以8)。
下面来看一个例子。假设通过某个Channel接收到一条消息,该消息由一个单独字节,后跟大量big-endian顺序的双字节整数(如short型)组成。由于该消息是通过Channel送达的,它一定在一个ByteBuffer中,在此为buf。消息的第一个字节包含了消息中双字节整数的数量。你可能要调用第一个字节指定次数的buf.getShort()方法,或者你可以一次获取所有的整数,如下所示:

1
2
3
4
5
6
7
8
9
10
// ...get message by calling channel.read(buf) ...
int numShorts = (int)buf.get();
if (numShorts < 0) {
throw new SomeException()
} else {
short[] shortArray = new short[numShorts];
ShortBuffer sbuf = buf.asShortBuffer();
sbuf.get(shortArray); // note: will throw if header was
incorrect!
}

asReadOnlyBuffer()方法的功能与duplicate()方法相似,只是任何会修改新缓冲区内容的方法都将抛出ReadOnlyBufferException异常。包括各种型式的put(),compact()等,甚至连在缓冲区上调用无方向性的array()和arrayOffset()方法也会抛出这个异常。当然,对产生这个只读缓冲区的非只读缓冲区进行的任何修改,
仍然会与新的只读缓冲区共享。就像用duplicate()创建的缓冲区一样,只读缓冲区也有独立的缓冲区状态变量。可以使用isReadOnly()方法来检查一个缓冲区是否是只读的。如果原缓冲区已经是只读的,调用duplicate()或slice()方法也将创建新的只读缓冲区。

字符编码

       字符是由字节序列进行编码的,而且在字节序列与字符集合之间有各种映射(称为字符集)方式。NIO缓冲区的另一个用途是在各种字符集之间进行转换。要使用这个功能,还需要了解java.nio.charset包中另外两个类:CharsetEncoder和CharsetDecoder类。要进行编码,需要使用一个Charset实例来创建一个编码器并调用encode方法:

1
2
3
4
Charset charSet = Charset.forName("US-ASCII");
CharsetEncoder encoder = charSet.newEncoder();
ByteBuffer buffer = encoder.encode(CharBuffer.wrap("Hi
mom"));

要进行解码,需要使用Charset实例来创建一个解码器,并调用decode方法:

1
2
CharsetDecoder decoder = charSet.newDecoder();
CharBuffer cBuf = decoder.decode(buffer);

虽然这种方法能够正常工作,但当需要进行多次编码时,效率就会变得较低。例如,每次调用encode/decode
方法都会创建一个新Byte/CharBuffer实例。其他导致低效率的地方与编码器的创建和操作有关。

1
2
3
4
5
6
7
8
9
10
encoder.reset();
if (encoder.encode(CharBuffer.wrap("Hi "),buffer,false)
== CoderResult.OVERFLOW) {
// ... deal with lack of space in buffer ...
}
if (encoder.encode(CharBuffer.wrap("Mom"),buffer,true)
== CoderResult.OVERFLOW) {
// ... ditto ...
}
encoder.flush(buffer);

encode()方法将给定CharBuffer转换为一个字节序列,并将其写入给定的缓冲区。如果缓冲区太小,encode()方法的返回值等于CoderResult.OVERFLOW。如果输入的数据完全被接收,并且编码器还准备对更多数据进行编码,encode()方法的返回值则等于CoderResult.UNDERFLOW。另外,如果输入的数据格式有错误,则将返回一个CoderResult对象,并指示了所存在的问题的位置和类型。只有到达了输入数据的结尾时,才将最后的boolean参数设为true。flush()方法将任何缓存的编码数据推送到缓冲区。注意,在新创建的编码器上调用reset()方法并不是必需的,该方法用来重新设置编码器的内部状态,以使其能够进行再次编码。

总结

最后通过一段简短代码展示allocate、flip、get、compact和clear方法对position、limit的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void main(String[] arg) throws IOException {

ByteBuffer buffer1 = ByteBuffer.allocate(10);
RandomAccessFile fine = new RandomAccessFile(new File("b.txt"), "rw");
FileChannel fc = fine.getChannel();//分配空间后position为0,limit=capacity=10;
output("allocate", buffer1);
fc.read(buffer1);
output("read", buffer1);
buffer1.flip(); //将limit设置为position,然后将position设置为0
output("flip", buffer1);
buffer1.get(); //将position加一
output("get", buffer1);
buffer1.rewind(); //重置position为0
output("rewind", buffer1);
buffer1.compact(); //将position到limit的数据移到数组前端,并将position设置为(limit-position的距离),limit=capacity
output("compact", buffer1);
buffer1.clear(); //重置position为0,limit=capacity
output("clear", buffer1);

}

public static void output(String tag, ByteBuffer buffer) {
System.out.println(tag + " position:" + buffer.position() + " limit:" + buffer.limit() + " capacity:" + buffer.capacity());
}

结果如下:
allocate position:0 limit:10 capacity:10
read position:6 limit:10 capacity:10
flip position:0 limit:6 capacity:10
get position:1 limit:6 capacity:10
rewind position:0 limit:6 capacity:10
compact position:6 limit:10 capacity:10
clear position:0 limit:10 capacity:10
get position:2 limit:10 capacity:10

参考:
[1] (译文)java中的ShortBuffer https://blog.csdn.net/u010142437/article/details/42082735
[2] Buffer详解 https://blog.csdn.net/guofengpu/article/details/51995730

12…6

CallteFoot

The blog from a Android coder

56 日志
23 分类
71 标签
© 2020 CallteFoot
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4