荏苒追寻个人博客

做一个有追求的青年


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 日程表

Java杂谈——不同Java版本的特性比较说明

发表于 2023-10-13 | 分类于 Java , 杂谈

https://rainmonth.github.io/posts/J191110.html

摘要

问题记录

  1. Ideal 运行时提示

    1
    2
    objc[7169]: Class JavaLaunchHelper is implemented in both /Library/Java/JavaVirtualMachines/jdk1.8.0
    and /Library/Java/JavaVirtualMachines/jdk1.8.0.jdk/Contents/Home/jre/lib/libinstrument.dylib (0x10ea194e0). One of the two will be used. Which one is undefined.

    解决方案:

    Idea->Help->Edit Custom Propertis,打开ideal.properties文件,添加以下配置,然后重启即可

    1
    idea.no.launcher=true
  2. 查看虚拟机类型

Java 杂谈——取文件夹大小的几种方式

发表于 2023-10-13 | 分类于 Java , 杂谈

https://rainmonth.github.io/posts/J191108.html

摘要

阅读全文 »

Java之AOP简介及简单使用

发表于 2023-10-13 | 分类于 Java , AOP

https://rainmonth.github.io/posts/J191203.html

摘要

参考文章:深入理解Android之AOP

阅读全文 »

Android 音视频基础——音频焦点管理

发表于 2023-10-13 | 分类于 Android , 音视频基础

https://rainmonth.github.io/posts/A210109.html

为什么要音频焦点管理

Android系统是支持多个应用同时播放声音的(这点能力还是有的)只不过这样就会同一时间出现多个音频,并且声音大小还没有什么区分,导致了十分糟糕的使用体验,因此音频焦点管理便有了必要。

如何进行焦点管理

Android系统通过AudioManager类来完成音频焦点管理,其中请求音频焦点用 requestAudioFocus方法,释放音频焦点用 abandonAudioFocus方法,焦点的改变监听实现 OnAudioFocusChangeListener即可。

音频的类型

既然需要进行音频焦点管理,就需要知道Android 系统中音频的类型,针对具体的音频类型,采用具体的管理策略,那么Android系统通常的音频类型如下,都在 AudioManager中有定义:

  • STREAM_VOICE_CALL,通话的声音,这类通常是不允许打断的;
  • STREAM_SYSTEM,系统声音;
  • STREAM_RING,电话铃声;
  • STREAM_MUSIC,音乐软件的歌曲播放等;
  • STREAM_ALARM,闹钟铃声;
  • STREAM_NOTIFICATION,通知声音;
  • STREAM_DTMF,双音多频铃声(DTMF,Dural Tone Multi Frequency);

还有几种App不可用(系统内部使用的)以及需要申请权限才能使用的,这些不是重点。

  • STREAM_BLUETOOTH_SCO,蓝牙通话声音;(外部不可用)
  • STREAM_SYSTEM_ENFORCED,某些国家或地区的系统强制声音;(外部不可用)
  • STREAM_TTS,文本转语音类型声音;
  • STREAM_ACCESSIBILITY,需要申请权限(android.Manifest.permission.MODIFY_AUDIO_ROUTING)

这个类型的值通常是用来构造 AudioAttributes 对象的,用来告诉系统,当前要请求音频焦点的音频流是何种类型的音频。

请求音频焦点

通过AudioManager的requestAudioFocus()来进行音频焦点的获取,所有的重载方法最终都会调用到

1
public int requestAudioFocus(@NonNull AudioFocusRequest afr, @Nullable AudioPolicy ap)

常用的请求焦点方法有:

  • 8.0之前:public int requestAudioFocus(OnAudioFocusChangeListener l, int streamType, int durationHint)

  • 8.0以后:public int requestAudioFocus(@NonNull AudioFocusRequest focusRequest)

音频焦点的请求参数

  • OnAudioFocusChangeListener,音频焦点改变监听;

  • streamType,请求的音频类型;

  • durationHint,请求的音频焦点类型,通常有一下几个值:

    • AUDIOFOCUS_GAIN,音频焦点持续时间是未知的,通常表示长时间持有音频焦点,例如播放音乐、录音等

    • AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE,段时间获取焦点,并希望其他应用不再播放声音;

    • AUDIOFOCUS_GAIN_TRANSIENT,短时间获取焦点,如播放导航声音或者播放通知声音;

    • AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,短时间获取焦点,并希望其他应用以较低的音量播放声音;

  • AudioFocusRequest,专门用来封装音频焦点请求的类,可以通过Builder模式来构造,上面的三个参数,AudioFocusRequest 中都有,都可以通过AudioFocusRequest来设置;

焦点请求结果处理

8.0(API<26)之前,请求音频焦点只会返回两种结果:

  • AUDIOFOCUS_REQUEST_GRANTED,请求成功

  • AUDIOFOCUS_REQUEST_FAILED,请求失败

8.0(API>=26)之后,请求结果多了一个值:

  • AUDIOFOCUS_REQUEST_DELAYED,这个表示延时请求成功,注意,要想获得这个请求结果,必须先调用AudioFocusRequest.Builder#setAcceptsDelayedFocusGain(true)方法,否则不会返回该结果。

注意,通常在调用 requestAudioFocus相关方法请求音频焦点时,系统并没有强制要求一定要等待收到 AUDIOFOCUS_REQUEST_GRANTED 返回结果时才能进行播放,没有请求到也可以播放的,只不过播放体验不好。

请求焦点后其他音频自动降低音量

在8.0之前,当请求焦点使用 AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK时,表明希望 请求焦点的应用在请求到焦点后,其他应用降低音量,但是很可能由于其他应用开发这忘记了这么做,导致达不到预期的效果(其他音频自动降低音量),因为系统并没有强制要求这么做。

在8.0之后,这个自动降低音量的行为,是系统默认的了,也就是说,如果其他应用没有处理音频焦点的变化,当其他应用以AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK方式获取到焦点后,系统会自动降低其他应用的音量。

释放音频焦点

通过调用 AudioManager的abandonAudioFocusRequest(@NonNull AudioFocusRequest focusRequest)方法来释放音频焦点,注意:这个focusRequest 参数必须是请求时用到的

focusRequest。

PS:通请求焦点一样,释放焦点针对8.0以前和8.0以后的也要区分对待,需要做好兼容。

参考文章

Android音频焦点管理

Android 音视频基础7——MacOs下编译FFMpeg 4.2.2

发表于 2023-10-13 | 分类于 Android , 音视频基础

简介

本文主要介绍Mac环境下FFMpeg的编译方法,同时还会简单介绍FFMpeg。

FFmpeg是用于处理多媒体内容(例如音频,视频,字幕和相关元数据)的库和工具的集合。它包含的库有:

  • libavcodec,提供了多种编解码器的实现;
  • libavformat,实现流协议,容器格式和基本I / O访问;
  • libavutil,包括哈希,解压缩器和其他实用程序功能;
  • libavfilter,提供了一种通过滤波器链来更改解码的音频和视频的方法;
  • libavdevice,提供采用硬件设备进行图片补货和视频播放的抽象封装;
  • libswresample,实现了音频混合和重采样工作;
  • libswscale,实现了色彩转换和缩放等工作;

它包含的工具有:

  • ffmpeg,处理,转换和流式传输多媒体内容的命令行工具箱。
  • ffplay,mini多媒体播放器;
  • ffprobe,检查多媒体内容的简单分析工具;
  • 其他小工具,例如aviocat,ismindex和qt-faststart
阅读全文 »

Android 适配——资源整理

发表于 2023-10-13 | 分类于 Android , 适配

https://rainmonth.github.io/posts/A230808.html

  • Android适配——动态权限管理
  • Android适配——屏幕适配方案研究
  • Android适配——全局悬浮窗的实现
  • Android适配——原生与H5交互
  • Android适配——状态栏攻略
  • Android适配——Notification适配指南

Android 14快速适配要点

Android 系统源码分析——MotionEvent

发表于 2023-10-13 | 分类于 Android , 系统源码分析

https://rainmonth.github.io/posts/A191019.html

Android MotionEvent与GestureDetector

参考文章:

  1. Android MotionEvent详解;
  2. Android 应用处理MotionEvent的过程;

MotionEvent

坐标相关

  • getX()/getY(),相对于View左上角的坐标;
  • getRawX()/getRawY(),相对于屏幕左上角的坐标;

Pointer

  • id,id在过程中不回改变
  • index,index可以改变

getActionMasked()与getAction()

  • getActionMasked(),返回的是事件的类型所代表的int值;

  • getAction(),返回的是Pointer的index值和事件类型值组合而成的int值

    getActionmasked() = getAction & ACTION_MASK;

GestureDetector

Android 开源库分析——OkHttp3

发表于 2023-10-13 | 分类于 Android , 开源库分析

本文链接:https://rainmonth.github.io/posts/A210402.html

  • 分析版本,OkHttp-3.12.10
  • 分析环境,IntellJIdeal

通过本文,可以了解如下内容:

  • OkHttp的基本使用;
  • OkHttp三次握手实现;
  • OkHttp的拦截器实现;
  • OkHttp的缓存实现;
  • OkHttp如何实现WebSocket;
  • 了解ArrayDeque,因为OkHttp中大量的使用ArrayDeque

基本使用

get请求

同步get请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void getSync(String url) {
System.out.println("同步请求");
OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
Request request = new Request.Builder().url(url)
.build();
Call call = okHttpClient.newCall(request);
try {
Response response = call.execute();
if (response.isSuccessful()) {
System.out.println(response.body().toString());
}
} catch (IOException e) {
e.printStackTrace();
}

}

异步get请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void get(String url) {
System.out.println("异步请求");
OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
final Request request = new Request.Builder()
.url(url)
.get()
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {

}

@Override
public void onResponse(Call call, Response response) throws IOException {
if (response.isSuccessful()) {
System.out.println(response.body().toString());
}
}
});
}

post请求

介绍post请求之前先了解下MediaType。

MediaType指的是媒体类型,表示表示的时浏览器将以什么形式、什么编码对资源进行解析。主要包含三个信息:type、subType以charset,MediaType常见类型如下:

  • text/(html,HTML文本格式;plain,纯文本格式;xml,xml格式;x-markdown,Markdown格式;)
  • image/(gif,gif图片;jpeg,jpg图片;png,png图片;)
  • application/(json,json格式数据;x-www-form-urlencode,键值对形式编码的参数;xhtml+xml,xhtml文本格式;xml,xml格式;pdf,pdf格式;msword,word格式;octet-stream,二进制数据(常用用于下载))
  • multipart/(form-data,不对字符编码发送大量二进制数据或包含non-ASCII字符的文本)

上面每种格式的前半部分表示type(“/”前的部分),后半部分表示subtype(“/”后的部分);

发送表单

1
2
3
4
5
6
7
8
9
10
11
12
13
 public void postForm(String url) throws IOException {
System.out.println("同步postForm");
OkHttpClient okHttpClient = ClientUtils.getOkHttpClient();
RequestBody requestBody = new FormBody.Builder()
.add("user", "randy")
.add("pwd", "xxx")
.build();
final Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
Response response = okHttpClient.newCall(request).execute();
}

发送json

1
2
3
4
5
6
7
8
9
10
11
public void postJson(String url) throws IOException {
System.out.println("同步postJson");
String json = "{\"user\":\"Randy\",\"nick\":\"荏苒追寻\"}";
OkHttpClient okHttpClient = ClientUtils.getOkHttpClient();
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json"), json);
final Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
Response response = okHttpClient.newCall(request).execute();
}

发送Multipart

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void postMultiPart(String url) throws IOException {
System.out.println("同步 postMultiPart");
OkHttpClient okHttpClient = ClientUtils.getOkHttpClient();
RequestBody requestBody = new MultipartBody.Builder()
.addFormDataPart("user", "randy")
.addFormDataPart("nick", "xxx")
.addFormDataPart("file", "file.txt",
RequestBody.create(MediaType.parse("application/octet-stream"), new File("filePath")))
.build();
Request request = new Request.Builder()
.url(url)
.post(requestBody)
.build();
Response response = okHttpClient.newCall(request).execute();
}

建立WebSocket连接

OkHttpClient 可以直接发送WebSocket连接请求,代码如下:

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
/**
* @param url webSocket 链接地址
*/
public WebSocket webSocket(String url) {
Request request = new Request.Builder()
.url(url)
.get()
.build();

OkHttpClient okHttpClient = ClientUtils.getOkHttpClient();

return okHttpClient.newWebSocket(request, new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
}

@Override
public void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text);
}

@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
super.onMessage(webSocket, bytes);
}

@Override
public void onClosing(WebSocket webSocket, int code, String reason) {
super.onClosing(webSocket, code, reason);
}

@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
super.onClosed(webSocket, code, reason);
}

@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
}
});
}

具体的OkHttpClent WebSocket 说明,参考文章:OkHttp3实现WebSocket连接

基本使用小结

上面就是使用OkHttp进行网络请求的常用场景,总结下来就是:

  1. 获取OkHttpClient实例(一般是单例模式);
  2. 构建Request,post请求需要先构建RequestBody;
  3. 调用OkHttpClient的newCall(Request request)方法获取Call;
  4. 获取Call对象后,
    1. 同步请求调用Call.execute()获取Response对象,并处理请求结果;
    2. 异步请求调用Call.enqueue(Callback callback),并在回调中处理结果;

拦截器

拦截器是OkHttpClient设计的精髓所在,它采用采用责任链模式。责任链代码具体体现在如下地方:

1
2
3
4
5
6
7
8
9
10
11
// RealCall.java
Response getResponseWithInterceptorChain() throws IOException {
List<Interceptor> interceptors = new ArrayList();
// 添加一系列的拦截器
...
// 构造责任链
Chain chain = new RealInterceptorChain(interceptors, (StreamAllocation)null, (HttpCodec)null, (RealConnection)null, 0, this.originalRequest, this, this.eventListener, this.client.connectTimeoutMillis(), this.client.readTimeoutMillis(), this.client.writeTimeoutMillis());
// 开始责任链处理
Response response = chain.proceed(this.originalRequest);
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// RealInterceptorChain.java
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
// 一系列的检查,不合条件抛出异常
...

// 重新构建责任链
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
writeTimeout);
Interceptor interceptor = interceptors.get(index);
// 在intercept(next)调用后责任链开始执行
Response response = interceptor.intercept(next);
// 一系列的检查,不合条件抛出异常
...

return response;
}

责任的传递是通过调用获取到的拦截器的intercept(Chain chain)方法,并在方法内部调用Chain.proceed(Request request)方法来实现的,最后一个拦截器CallServerInterceptor的intercept(Chain chain)方法中没有调用proceed(Request request)方法,说明任务到这个拦截器的处理工作完毕了。

拦截器的种类

要了解拦截器的种类,还是得看RealCall的getResponseWithInterceptorChain()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Response getResponseWithInterceptorChain() throw IOException {
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));

...
return response
}

总结下责任链中各个节点拦截器的作用,如下:

拦截器 作用
应用拦截器 拿到的是原始请求,可以添加一些自定义header、通用参数、参数加密、网关接入等等。
RetryAndFollowUpInterceptor 处理错误重试和重定向
BridgeInterceptor 应用层和网络层的桥接拦截器,主要工作是为请求添加cookie、添加固定的header,比如Host、Content-Length、Content-Type、User-Agent等等,然后保存响应结果的cookie,如果响应使用gzip压缩过,则还需要进行解压。
CacheInterceptor 缓存拦截器,如果命中缓存则不会发起网络请求。
ConnectInterceptor 连接拦截器,内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。
networkInterceptors(网络拦截器) 用户自定义拦截器,通常用于监控网络层的数据传输。
CallServerInterceptor 请求拦截器,在前置准备工作完成后,真正发起了网络请求。

RetryAndFollowUpInterceptor

处理错误和重定向。核心方法是followUpRequest(Response userResponse, Route route),该方法会根据返回的responseCode来进行相应的逻辑处理,最终得到相应的Request对象。这里主要看那些导致重试和重定向的responseCode:

  • 重试,HTTP_CLIENT_TIMEOUT和HTTP_UNAVAILABLE(重试一次);
  • 重定向,HTTP_MULT_CHOICE、HTTP_MOVED_PERM、HTTP_MOVED_TEMP、HTTP_SEE_OTHER,会进行重定向的尝试;

还有一个比较核心的点就是,该拦截器的intercept方法中,完成了StreamAllocation对象的创建,并将其值传递到了RealInterceptorChain中。

BridgeInterceptor

说明见上面的表格,需要注意的是,如果我们没有指定Accept-Encoding: gzip,OkHttpClient在Response的header中存在Accept-Encoding: gzip的情况下会默认帮我们处理Gzip压缩。但是如果我们制定了Accept-Encoding: gzip,则需要我们自己进行Response的Gzip压缩处理了。

CacheInterceptor

为什么说命中缓存就不会发起网络请求呢?看看CacheInterceptor的Intercept(Chain chain)方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 命中了缓存,直接return了,此时没有执行责任链,即这次网络请求的任务已经完成了。所以此时不会再执行网络请求了,因为负责建立连接ConnectInterceptor和真正执行网络请求CallServerInterceptor的拦截器都没有执行。
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}

Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}

OkHttpClient 的缓存设计算法采用的是Lru,基于Okio实现(相当于基于依据Http的缓存设计思路,用Okio进行了实现)。关于OkHttpClient的缓存,打算单独分析。

ConnectInterceptor

拦截器作用于职责:内部会维护一个连接池,负责连接复用、创建连接(三次握手等等)、释放连接以及创建连接上的socket流。整个ConnectInterceptor的代码还是比较简洁的,但是主要的处理都封装到了StreamAllocation这个类中了。StreamAllocation
建立连接的流程

  1. StreamAllocation的newStream(...);

  2. StreamAllocation的findHealthyConnection(...),找到RealConnection;

  3. findHealthyConnection(...)的while(true)循环中调用findConnection(...),直到找到了一个健康的、可用的RealConnection才退出循环;

    1. StreamAllocation本身可以标记找到的RealConnection,方便下次查找;
    2. 先通过当前请求地址,从ConnectionPool中找;
    3. 通过全局路由路径,从ConnectionPool中找;
    4. 都找不到,就要新建一个连接,完成TCP+TLS了()
  4. 调用RealConnection的connect(…)方法,里面会根据情况调用connectTunnel和connectSocket,这个过程是阻塞的(一个while(true)循环,但最终都是调用connectSocket,方法内部会调用不同平台的connectSocket方法,创建成功后,将创建出来的RealConnection放入ConnectionPool。而三次握手也就发生在这个建立socket连接的过程中了,因此OkHttp的三次握手实现时依赖于socket的。

    了解三次握手

ConnectionPool内部有一个线程池,用来定期清理尝时间不用的RealConnection(毕竟RealConnection是系统资源)

CallServerInterceptor

真正发起网络请求的地方

问题

  1. addInterceptor(Interceptor interceptor)和addNetworkInterceptor(Interceptor interceptor)区别?

    1. 目的:前者是添加应用拦截器(此时请求还未真正发起,是一些与处理过程),后者是添加网络拦截器,它位于ConnectInterceptor和CallServerIntercptor之间(此时网络链路已经准备好,只带请求发送了)
    2. 执行时机:前者最先执行,后者在链接建立之后,请求发送之前;
    3. 调用:应用拦截器和网络拦截器的proceed(Chain chain)
  2. OkHttpClient 的Dispatcher是如何管理三个请求的队列的?
    首先这三个队列分别是:正在运行的同步双端队列(runningSyncCalls)、准备就绪的异步双端队列(readyAsyncCalls)、正在运行的异步双端队列(runningAsyncCalls)

client.dispatcher().executed(this)干了什么呢?

1
2
3
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}

将RealCall对象加入到正在运行的同步队列中,这样在getResponseWithInterceptorChain()调用结束得到Response对象后,最终运行finally块时,调用client.dispatcher().finished(this);,那么看看这个方法:

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
void finished(RealCall call) {
finished(runningSyncCalls, call);
}

private <T> void finished(Deque<T> calls, T call) {
Runnable idleCallback;
synchronized (this) {
// 移除之前加入的同步请求
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
idleCallback = this.idleCallback;
}

// 尝试在readyAsynCall队列中获取待运行的任务,然后加入到runningAsynCall队列中运行
boolean isRunning = promoteAndExecute();

if (!isRunning && idleCallback != null) {
idleCallback.run();
}
}
/**
* 从就绪的异步任务中按规则取出一定数量异步任务,加入到可运行的异步任务队列中,然后将运行中的异步任务丢该线程池运行
* 这里的获取规则如下:
* 1. 当前运行的异步任务书超过最大请求数时,直接break;
* 2. 当前的异步任务超过美国host的最大限制是,直接跳过;
* 这就说明了一个问题:随缘OkHttpClient采用的Dispatcher中的线程池核心线程为0,工作线程为Integer.MAX_VALUE,但通过 maxRequests (默认值为64)进行了线程数量的控制,不至于因线程过多导致
* OOM的发生
*/
private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));

List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();

if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
if (runningCallsForHost(asyncCall) >= maxRequestsPerHost) continue; // Host max capacity.

i.remove();
executableCalls.add(asyncCall);
runningAsyncCalls.add(asyncCall);
}
isRunning = runningCallsCount() > 0;
}

for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService());
}

return isRunning;`
}
  1. getResponseWithInterceptorChain()如何实现递归调用的?
    先看getResponseWithInterceptorChain()代码:

    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
    Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
    interceptors.addAll(client.networkInterceptors());
    }
    // 注意:这个interceptors的列表中最后都会添加一个 CallServerInterceptor ,这个 CallServerInterceptor 的Intercept方法中是不调用 Chain.proceed(Request request)方法的。
    // 递归就是通过不停的调用Chain.process(Chain chain)方法的。相当于CallServerinterceptor是递归的终止条件。
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
    originalRequest, this, eventListener, client.connectTimeoutMillis(),
    client.readTimeoutMillis(), client.writeTimeoutMillis());

    Response response = chain.proceed(originalRequest);
    if (retryAndFollowUpInterceptor.isCanceled()) {
    closeQuietly(response);
    throw new IOException("Canceled");
    }
    return response;
    }

    所以递归是不断的调用RealInterceptorChain的proceed(Request request)方法的,而递归终止的条件就是最终会走到CallServerInterceptor的intercept(Chain chain)方法,而该方法不会再调用Chain.proceed(Request request)

总结

至此OkHttpClient的基本内容就介绍完毕了,OkHttpClient十分强大强大,

  • 从设计模式角度,如对Http1和Http2的支持(采用的是策略模式),拦截器的处理(采用的是责任链模式),各种对象的创建如OkHttpClient、Request、Response(采用的Builder模式)
  • 从同步处理的角度,ConnectionPool中的同步处理、三个请求队列的操作处理;
  • 从线程池的角度,有Dispatcher(负责请求的分发调度)、ConnectionPool中的线程池(负责定期按规则清理不需要连接对象)
  • 从缓存设计的角度,采用Okio将http的缓存策略实现,并通过LRU算法(DiskLruCache)来维护缓存的更新。

Android 开源库分析——图片加载库的比较与封装

发表于 2023-10-13 | 分类于 Android , 开源库分析

本文链接:https://rainmonth.github.io/posts/A211010.html

概述

图片加载时App开发过程中很重要的应用场景,几乎任何应用都离不开图片的加载展示,本文先分析主流的图片加载框架,对其进行比较,然后在此基础上利用Java设计模式封装一个可以灵活配置、实时切换图片加载内核的图片加载框架。目前,主流的图片加载库有:UniversalImageLoader,Picasso,Glide,Fresco(按推出时间排序)。

UniversalImageLoader

Picasso

Glide

  • RequestManager,
  • RequestBuilder,
  • GlideBuilder,
  • Target,

Fresco

比较维度

  1. 异步加载机制
  2. 缓存设计
  3. 格式支持
  4. 显示效果
  5. 其他(包大小、支持Android版本)
  6. 根据应用场景进行库的选择

疑难杂症

  1. 大图加载

参考文章

  1. 图片加载库的比较和选型
  2. Android 图片加载库如何封装

Android 开源库分析——greenDAO

发表于 2023-10-13 | 分类于 Android , 开源库分析

本文链接:https://rainmonth.github.io/posts/A211027.html
Android greenDAO使用

参考内容:官方网站,打开官网发现官网极力推荐ObjectBox使用,后面会对ObjectBox页做一个简单的说明。

摘要

greenDAO是一款开源高效的ORM数据库,极大的降低开发者编写sql查询的时间(使用greenDAO后会自动生成对应的dao,可以进行CRUD操作)。而greenDAO的使用也极其简单。greenDAO具有以下特点:

  • 性能好,特别是在Android平台上(相对于其他的ORM框架来说)表现尤其出色;
  • 易用,提供了强大的API;
  • 内存消耗小;
  • 库体积小;
  • 支持数据库加密,greenDAO支持SQLChiper
  • 社区强大

ORM

ORM,即对象关系映射(Object-Relationship Mapping),具有以下特点:

  • 简单,ORM可以将SQLite(或MySql)的一张表映射成一个Java对象;
  • 精确,所有的数据表都按统一的标准映射成Java对象;
  • 易懂,ORM是数据库结构文档化,一张张表对Java程序员来说就是一个个实体类;
  • 易用,ORM包含了对生成Java持久化数据对象的一些列CRUD操作,风格统一,规范,便于维护;

参考:ORM框架简介

基本使用

配置greedDAO环境

  1. 根目录的build.gradle添加如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    buildscript {
    repositories {
    jcenter()
    mavenCentral() // 添加仓库地址
    }
    dependencies {
    classpath 'com.android.tools.build:gradle:3.5.3'
    classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0' // 添加插件
    }
    }
  2. 应用module的build.gradle文件添加如下代码

    1
    2
    apply plugin: 'com.android.application'
    apply plugin: 'org.greenrobot.greendao' // apply plugin

dependencies {
implementation ‘org.greenrobot:greendao:3.3.0’ // add library
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
3. greenDAO的一些配置
通过上面两步就可以在项目中使用greenDAO,在app module中添加greendao闭包处理,如下:
```groovy
apply plugin: 'com.android.application'
apply plugin: 'org.greenrobot.greendao' // apply plugin
greendao {
// 数据库版本,很重要,后面数据库的升级需要用到它
schemaVersion 1
// 指定dao文件所在的包名
daoPackage 'com.rainmonth.notebook.dao'
// 指定插件自动生成java文件存放的目录
targetGenDir 'src/main/java'
// 是否自动生成单元测试
generateTests true
}

dependencies {
implementation 'org.greenrobot:greendao:3.3.0' // add library
}

基础注解说明

  • @Entity,加在类上,声明表的实体类型,加上给注解后会根据类的属性生成必要的表,字段就是类的属性;该注解有如下属性

    • nameInDb, 表在数据库中的别名,不指定默认就是实体类名;
    • Index[],索引,可以跨列;
    • createInDb,默认true,标记在数据库中刚创建表;
    • schema,表的schema;
    • active,默认为true,实体(表)是否处于活跃状态,活跃状态的有更新删除和刷新的方法;
    • generateConstructors,默认true,是否生成构造函数;
    • generateGettersSetters,默认true,标记是否生成相应的get/set方法;
    • protobuf,默认为void.class,为实体类提供一个额外的class以生成一个特殊的DAO
  • @Id,加载实体类的属性上,定义自增id,有一个autoincrement属性,默认为false;

  • @Index,加载实体类属性上,表示索引,有value、name、unique三个属性;

  • @Generated,表明是属性、方法或构造方法是由greenDAO自己生成的代码;

  • @JoinEntity,用于建立多对多关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Entity
    public class Product {
    @Id private Long id;

    @ToMany
    @JoinEntity(
    entity = JoinProductsWithOrders.class,
    sourceProperty = "productId",
    targetProperty = "orderId"
    )
    private List ordersWithThisProduct;
    }

    @Entity
    public class JoinProductsWithOrders {
    @Id private Long id;
    private Long productId;
    private Long orderId;
    }

    @Entity
    public class Order {
    @Id private Long id;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- @JoinProperty,用来定义Join的的两个属性分别是name和referenceName
- @Keep,表明下次greenDAO再次生成代码是要不能修改被@Keep修饰的属性、方法或类型;
- @NotNull,非空注解;
- @OrderBy,跟排序相关,某字段加上了该注解,就根据该字段排序;
- @Property,定义@Entity实体类某个字段在数据库表中对应的名字;
- @ToMany,一对多或多对多;
- @ToOne,一对一;
```java
@Entity
public class Order {

@Id private Long id;

private long customerId;

@ToOne(joinProperty = "customerId")
private Customer customer;
}

@Entity
public class Customer {
@Id
private Long id;
}
  • @Transient,不会被greenDAO持久化到数据库中;
  • @Unique,创建表的时候被@Unique注释的属性需遵循唯一性约束;

实体类支持的字段类型

greenDAO默认支持的字段类型有如下:

1
2
3
4
5
6
7
8
9
10
boolean, Boolean
int, Integer
short, Short
long, Long
float, Float
double, Double
byte, Byte
byte[]
String
Date

核心类

greenDAO 使用是否简单,在使用@Entity注解定义好实体后,编译就可以自动生成一些用来进行数据库操作的处理。正常的主要类有DaoMaster、DaoSession、XxxEntity

DaoMaster

负责管理数据库对象和EntityDAO对象的关系,可以通过其内部类OpenHelper、DevOpenHelper、SQLiteOpenHelper创建不同模式的数据库。负责表的创建、更新,负责数据库的升级,负责DaoSession的创建

DaoSession

管理者所有的EntityDAO,提供了公用的实体CRUD方法(实际上是在AbstractDAOSession中实现的)

XxxEntity

根据定义的java类(@Entity注解修饰的类)生成的类文件,会根据@Entity注解的属性按需生成类方法(如是否需要生成构造函数,是否需要生成getter/setter方法等。

原理

由于greenDAO是一个ORM框架,干的就是对象关系映射这件事,所以要搞清楚对象到关系的映射是如何生成的。这里原理分析部分主要分析代码自动生成,这样以后自己也可以写插件自动生成代码了。

  1. greenDAO的api就是其定义的那些注解,开发者在原始的类中根据要在数据库中建立的表的关系,将api(即注解应用上去),这样开发者的工作就完成了,箱单简单;
  2. 前面在介绍基本使用时,在项目的根目录下的build.gradle引用了gradle插件org.greenrobot:greendao-gradle-plugin:3.3.0,并在app Module中的build.gradle文件作用应用了插件apply plugin: 'org.greenrobot.greendao'。注意应用这个插件的目的就是为了让greenDAO在编译能找到合适的时机来进行相关的代码生成。这说明两点:
    1. greenDAO的gradle插件只是为greenDAO生成代码找一个切口,跟greenDAO代码生成是没有直接联系的;
    2. 生成代码是在编译期进行的,所以greenDAO并不会影响app运行时的效率,但是会影响apk的编译速度。(实际项目中可以采用demo工程生成相关代码,然后将代码拷贝到实际的工程中来,这样就避免了这一影响)
  3. 有一个控制模块:greendao-code-modifier,它来控制greenDAO的代码生成,具体包括:
    1. 收集注解信息并判断注解信息是否要更新(收集注解信息采用的greendao-jdt,由此可见greenDAO采用的注解处理器是JDT,与常见的注解处理方法(APT、反射不同)
    2. 调用greenDAO的code-generator来进行代码,code-generator使用的代码生成模板为FreeMarker;
  4. 然后就得到了开发者实际运行时用到的几个核心类:DaoMaster、DaoSession、XxxEntity;
  5. greenDAO-core通过4中生成的三中核心类来跟SQLite进行交互。

用到的技术

  1. gradle插件技术,gradle插件开发介绍:https://www.jianshu.com/p/d53399cd507b;
  2. Android注解相关技术
  3. JDT注解处理与收集,JDT介绍:http://www.eclipse.org/jdt/overview.php
  4. 代码生成框架FreeMaker,FreeMarker快速入门:https://segmentfault.com/a/1190000011768799

greeDAO 数据库升级

为什么会要单独拎一节来讲数据库升级呢?首先,面试中经常会被问到,哈哈;其次这个是开发中很容易遇到的问题,下面来介绍下。

greenDAO的数据库升级处理主要是在DaoMaster中进行的,主要是:

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
/**
* Calls {@link #createAllTables(Database, boolean)} in {@link #onCreate(Database)} -
*/
public static abstract class OpenHelper extends DatabaseOpenHelper {
public OpenHelper(Context context, String name) {
super(context, name, SCHEMA_VERSION);
}

public OpenHelper(Context context, String name, CursorFactory factory) {
super(context, name, factory, SCHEMA_VERSION);
}

@Override
public void onCreate(Database db) {
Log.i("greenDAO", "Creating tables for schema version " + SCHEMA_VERSION);
createAllTables(db, false);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
super.onUpgrade(db, oldVersion, newVersion);
//todo 这里进行的时数据库升级操作
}

}

当我们数据表发生更改的时候,我们需要在app的build.gradle下的greendao{}升级schemaVersion版本,版本改变后,最终就会走到onUpgrade(...)里面。
粗暴的策略:删掉所有老的表,然后创建新的,然后所有的数据都丢了,然后该滚蛋了…….

通常的升级策略

  1. 针对有索引的表,先要删除相应的索引,否则数据备份是会存在冲突;
  2. 针对有变动的表,将原来的老表重命名为相应的临时表(改变表明);
  3. 创建新表
  4. 数据恢复,将临时表的数据恢复到新表中(新表临时表都包含的字段需要恢复)
  5. 删除临时表;
  6. 保底措施:如果表没出创建成功,重新创建

greenDAO降级处理

降级处理比较简单,删除表然后重新创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
LogHelper.i(TAG, "主数据库降级 [" + mDbName + "] from version " + oldVersion + " to " + newVersion);

try {
DaoMaster daoMaster = new DaoMaster(db);
Database database = daoMaster.getDatabase();

// 数据库版本号降级时,先删除所有表,然后重新创建
DaoMaster.dropAllTables(database, true);

DaoMaster.createAllTables(database, true);
} catch (Throwable e) {
LogHelper.e(TAG, mDbName + "onDowngrade from version " + oldVersion + " to " + newVersion + " err: " + e.toString());
}
}

疑问

  1. 不定义主键会发生什么?

  2. SqlLite数据库操作需要加锁吗?

  3. SqlLite数据库不同字段的限制条件是什么?

小结

了解了greenDAO的基本使用,特点及性能情况,同时对greenDAO整体的处理流程做了一个梳理,至于具体每个流程,如插件编写、注解处理、FreeMarker的具体使用细节,后面有机会单独介绍,这里不做赘述。

本文知识greenDAO的基本使用及原理介绍,高级使用请移步Android ORM 框架:GreenDao 使用详解(进阶篇)

<i class="fa fa-angle-left" aria-label="上一页"></i>1…345…22<i class="fa fa-angle-right" aria-label="下一页"></i>

216 日志
43 分类
43 标签
GitHub
© 2025 Randy Zhang
由 Hexo 强力驱动
|
主题 — NexT.Gemini v6.1.0