Android 音视频学习:使用 AudioRecord 和 AudioTrack API 完成音频 PCM 数据的采集和播放,并实现 PCM 保存为 WAV 文件

在具体使用 AudioRecord 采集音频之前,简单了解下 PCM。

什么是 PCM?

我们知道,声音本身是模拟信号,而计算机只能处理离散的数字信号,要在计算机中处理声音,就需要将声音数字化,这个过程叫模数转换(A/D 变换) 。模数转换直接生成的二进制序列称为 PCM(pulse code modulation, 脉冲编码调制)数据。它是数字音频在计算机、光盘、数字电话和其他数字音频应用中的标准形式。

要将模拟信号转为 PCM 时,需要将声音量化,我们一般从如下几个维度描述一段声音:

采样频率:每秒钟采集声音样本的次数,它用赫兹(Hz)来表示。

采样频率越高,声音的质量也就越好,声音的还原也就越真实,但同时它占的资源比较多。由于人耳的分辨率很有限,太高的频率并不能分辨出来。在 16 位声卡中有 22KHz、44KHz 等几级,,其中,22KHz 相当于普通 FM 广播的音质,44KHz 已相当于 CD 音质了,目前的常用采样频率都不超过 48KHz。

采样位数:表示每次采样的精度,也可以说是声卡的分辨率。位数越多,能记录的范围就越大。

声道数:很好理解,有单声道和立体声之分,单声道的声音只能使用一个喇叭发声(有的也处理成两个喇叭输出同一个声道的声音),立体声的 PCM 可以使两个喇叭都发声(一般左右声道有分工) ,更能感受到空间效果。

时长:采样的时长

使用 AudioRecord 采集音频

AudioRecord 类是 Android 系统提供的用于实现录音的功能类。

开始录音的时候,AudioRecord 需要初始化一个相关联的声音 buffer, 这个 buffer 主要是用来保存新的声音数据。这个 buffer 的大小,我们可以在对象构造期间去指定。它表明一个 AudioRecord 对象还没有被读取(同步)声音数据前能录多长的音(即一次可以录制的声音容量)。声音数据从音频硬件中被读出,数据大小不超过整个录音数据的大小(可以分多次读出),即每次读取初始化 buffer 容量的数据。

使用 AudioRecord 采集音频的一般步骤:

  • 初始化一个音频缓存大小,该缓存大于等于 AudioRecord 对象用于写声音数据的缓存大小,最小录音缓存可以通过AudioRecord#getMinBufferSize() 方法得到。

  • 构造一个 AudioRecord 对象,需要传入缓冲区大小,如果缓存容量过小,将导致对象构造的失败。

  • 开始录音

  • 创建一个数据流,一边从 AudioRecord 中读取声音数据到初始化的缓存,一边将缓存中数据导入数据流。

  • 关闭数据流

  • 停止录音

初始化缓存大小

可以通过 AudioRecord#getMinBufferSize() 方法得到最小录音缓存大小,传入的参数依次是采样频率、声道数和采样位数。

1
2
private int mBufferSizeInBytes;
mBufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);

构造 AudioRecord 对象

1
2
private AudioRecord mAudioRecord;
mAudioRecord = new AudioRecord(audioSource, sampleRateInHz, channelConfig, audioFormat, mBufferSizeInBytes);

初始化一个 buffer 数组

1
byte[] audioData = new byte[mBufferSizeInBytes];

开始录音

1
mAudioRecord.startRecording();

数据流读写

创建一个数据流,一边从 AudioRecord 中读取声音数据到初始化的 buffer,一边将 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
/**
* 将音频信息写入文件
*/
private void writeAudioDataToFile() throws IOException {
String pcmFilePath = FileUtil.getPcmFilePath(mContext, mPcmFileName);
File file = new File(pcmFilePath);
if (file.exists()) {
file.delete();
}
OutputStream bos = null;
try {
bos = new BufferedOutputStream(new FileOutputStream(file));
byte[] audioData = new byte[mBufferSizeInBytes];
while (mStatus == Status.STATUS_START) {
int readSize = mAudioRecord.read(audioData, 0, mBufferSizeInBytes);
if (readSize > 0) {
try {
bos.write(audioData, 0, readSize);
if (mRecordStreamListener != null) {
mRecordStreamListener.onRecording(audioData, 0, readSize);
}
} catch (IOException e) {
Log.e(TAG, "writeAudioDataToFile", e);
}
} else {
Log.w(TAG, "writeAudioDataToFile readSize: " + readSize);
}
}
bos.flush();
if (mRecordStreamListener != null) {
mRecordStreamListener.finishRecord();
}
} finally {
if (bos != null) {
bos.close();// 关闭写入流
}
}
}

停止录音

1
2
3
4
5
if (null != mAudioRecord) {
mAudioRecord.stop();
mAudioRecord.release();
mAudioRecord = null;
}

AudioRecord 和 MediaRecorder 的对比

Android SDK 提供了两套音频采集的 API,分别是:MediaRecorder 和 AudioRecord,前者是一个更加上层一点的API,它可以直接把手机麦克风录入的音频数据进行编码压缩(如 AMR、MP3 等)并存成文件,而后者则更接近底层,能够更加自由灵活地控制,可以得到原始的一帧帧 PCM 音频数据。

如果想简单地做一个录音机,录制成音频文件,则推荐使用 MediaRecorder,而如果需要对音频做进一步的算法处理、或者采用第三方的编码库进行压缩、以及网络传输等应用,则建议使用 AudioRecord,其实 MediaRecorder 底层也是调用了 AudioRecord 与 Android Framework 层的 AudioFlinger 进行交互的。直播中实时采集音频自然是要用 AudioRecord 了。

使用 AudioTrack 播放 PCM 音频

AudioTrack 类可以完成 Android 平台 PCM 数据流的播放工作。AudioTrack 有两种数据加载模式:MODE_STREAM 和 MODE_STATIC, 对应着两种完全不同的使用场景。

MODE_STREAM:在这种模式下,通过 write 一次次把音频数据写到 AudioTrack 中。这和平时通过 write 调用往文件中写数据类似,但这种方式每次都需要把数据从用户提供的 Buffer 中拷贝到 AudioTrack 内部的 Buffer 中,在一定程度上会引起延时。为解决这一问题,AudioTrack 就引入了第二种模式。

MODE_STATIC:在这种模式下,只需要在 play 之前通过一次 write 调用,把所有数据传递到 AudioTrack 中的内部缓冲区,后续就不必再传递数据了。这种模式适用于像铃声这种内存占用较小、延时要求较高的文件。但它也有一个缺点,就是一次 write 的数据不能太多,否则系统无法分配足够的内存来存储全部数据。

创建 AudioTrack 播放对象

参数与创建 AudioRecord 有相似之处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
final int minBufferSize = AudioTrack.getMinBufferSize(AUDIO_SAMPLE_RATE_INHZ, channelConfig, AUDIO_ENCODING);
mAudioTrack = new AudioTrack(
new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build(),
new AudioFormat.Builder()
.setSampleRate(AUDIO_SAMPLE_RATE_INHZ)
.setEncoding(AUDIO_ENCODING)
.setChannelMask(channelConfig)
.build(),
minBufferSize,
AudioTrack.MODE_STREAM,
AudioManager.AUDIO_SESSION_ID_GENERATE);

开始播放

MODE_STREAM 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void playAudioData() throws IOException {
InputStream dis = null;
try {
ThreadHelper.getInstance().runOnUiThread(() -> {
Toast.makeText(mContext, "播放开始", Toast.LENGTH_SHORT).show();
});
dis = new DataInputStream(new BufferedInputStream(new FileInputStream(mFilePath)));
byte[] bytes = new byte[mBufferSizeInBytes];
int length;
mAudioTrack.play();
// write 是阻塞的方法
while ((length = dis.read(bytes)) != -1 && mStatus == Status.STATUS_START) {
mAudioTrack.write(bytes, 0, length);
}
ThreadHelper.getInstance().runOnUiThread(() -> {
Toast.makeText(mContext, "播放结束", Toast.LENGTH_SHORT).show();
});
} finally {
if (dis != null) {
dis.close();
}
}
}

MODE_STATIC 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
try {
InputStream in = getResources().openRawResource(R.raw.ding);
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
for (int b; (b = in.read()) != -1; ) {
out.write(b);
}
mAudioData = out.toByteArray();
} finally {
in.close();
}
} catch (IOException e) {
Log.e(TAG, "Failed to read", e);
}

mAudioTrack.write(mAudioData, 0, mAudioData.length);
mAudioTrack.play();

停止播放

1
2
3
4
5
if (mAudioTrack != null) {
mAudioTrack.stop();
mAudioTrack.release();
mAudioTrack = null;
}

AudioTrack 与 MediaPlayer 的对比

Android 中播放声音可以用 MediaPlayer 和 AudioTrack,两者都提供了 Java API 供应用开发者使用。

虽然都可以播放声音,但两者还是有很大的区别的。

其中最大的区别是 MediaPlayer 可以播放多种格式的声音文件,例如 MP3,AAC,WAV,OGG,MIDI 等。MediaPlayer 会在 framework 层创建对应的音频解码器,而 AudioTrack 只能播放已经解码的 PCM 流,如果对比支持的文件格式的话则是 AudioTrack 只支持 wav 格式的音频文件,因为 wav 格式的音频文件大部分都是 PCM流。AudioTrack 不创建解码器,所以只能播放不需要解码的 wav 文件。

MediaPlayer 在 framework 层还是会创建 AudioTrack,把解码后的 PCM 数流传递给 AudioTrack,AudioTrack再传递给 AudioFlinger 进行混音,然后才传递给硬件播放,所以是 MediaPlayer 包含了 AudioTrack。

PCM 转 WAV

Waveform Audio File Format(WAVE,又或者是因为 WAV 后缀而被大众所知的),它采用 RIFF(Resource Interchange File Format)文件格式结构。通常用来保存 PCM 格式的原始音频数据,所以通常被称为无损音频。

WAV 和 PCM 的关系

PCM 数据本身只是一个裸码流,它是由声道、采样位数、采样频率、时长共同决定的,因此我们至少要知道其中的三个才能将 PCM 所代表的数据提取出来。

一种常见的方式是使用 WAV 格式定义的规范将 PCM 码流和描述信息封装起来。查看 PCM 和对应 WAV 文件的 hex 文件,可以发现,WAV 文件只是在 PCM 文件的开头多了 44bytes,来表征其声道数、采样频率和采样位数等信息。

PCM 转 WAV 的实现代码如下:

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
public class PcmToWavUtil {
/**
* 缓存的音频大小
*/
private int mBufferSize;
/**
* 采样率
*/
private int mSampleRate;
/**
* 声道数
*/
private int mChannel;

/**
* @param sampleRate sample rate、采样率
* @param channel channel、声道
* @param encoding Audio data format、音频格式
*/
public PcmToWavUtil(int sampleRate, int channel, int encoding) {
this.mSampleRate = sampleRate;
this.mChannel = channel;
this.mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, encoding);
}


/**
* pcm文件转wav文件
*
* @param inFilename 源文件路径
* @param outFilename 目标文件路径
*/
public void pcmToWav(String inFilename, String outFilename) {
FileInputStream in;
FileOutputStream out;
long totalAudioLen;
long totalDataLen;
long longSampleRate = mSampleRate;
int channels = mChannel == AudioFormat.CHANNEL_IN_MONO ? 1 : 2;
long byteRate = 16 * mSampleRate * channels / 8;
byte[] data = new byte[mBufferSize];
try {
in = new FileInputStream(inFilename);
out = new FileOutputStream(outFilename);
totalAudioLen = in.getChannel().size();
totalDataLen = totalAudioLen + 36;

writeWaveFileHeader(out, totalAudioLen, totalDataLen,
longSampleRate, channels, byteRate);
while (in.read(data) != -1) {
out.write(data);
}
in.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}


/**
* 加入wav文件头
*/
private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
long totalDataLen, long longSampleRate, int channels, long byteRate)
throws IOException {
byte[] header = new byte[44];
// RIFF/WAVE header
header[0] = 'R';
header[1] = 'I';
header[2] = 'F';
header[3] = 'F';
header[4] = (byte) (totalDataLen & 0xff);
header[5] = (byte) ((totalDataLen >> 8) & 0xff);
header[6] = (byte) ((totalDataLen >> 16) & 0xff);
header[7] = (byte) ((totalDataLen >> 24) & 0xff);
//WAVE
header[8] = 'W';
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
// 'fmt ' chunk
header[12] = 'f';
header[13] = 'm';
header[14] = 't';
header[15] = ' ';
// 4 bytes: size of 'fmt ' chunk
header[16] = 16;
header[17] = 0;
header[18] = 0;
header[19] = 0;
// format = 1
header[20] = 1;
header[21] = 0;
header[22] = (byte) channels;
header[23] = 0;
header[24] = (byte) (longSampleRate & 0xff);
header[25] = (byte) ((longSampleRate >> 8) & 0xff);
header[26] = (byte) ((longSampleRate >> 16) & 0xff);
header[27] = (byte) ((longSampleRate >> 24) & 0xff);
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
// block align
header[32] = (byte) (2 * 16 / 8);
header[33] = 0;
// bits per sample
header[34] = 16;
header[35] = 0;
//data
header[36] = 'd';
header[37] = 'a';
header[38] = 't';
header[39] = 'a';
header[40] = (byte) (totalAudioLen & 0xff);
header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
out.write(header, 0, 44);
}
}

具体源码已经放在 GitHub:AndroidMultiMediaLearning

参考资料

程序员技术资源分享群

独学而无友,则孤陋而寡闻。

我是个爱分享的程序员,遇上好的资源、工具软件都会主动分享给朋友。

好东西怎么能独享呢,分享给别人的同时,可能会收获更多。

所以,创建了一个技术资源分享群,欢迎爱分享的你加入进来。

本群主要是技术资源分享,包括:

  • 技术资源

  • 工具软件

  • 技术心得

  • 技术热点

为了让群价值最大化,交流更有效率:

  • 鼓励有价值的内容分享

  • 鼓励友善、互相帮助、积极努力的氛围

  • 不要只做伸手党

  • 禁止低级趣味下流庸俗的内容

  • 禁止讨论涉政敏感话题

  • 禁止广告和商业推广

进群方式:

为了群的质量,这里不贴群二维码了,想进的朋友加下我的微信,我拉你进群。(备注:技术群)

我的微信:wzy980691533

Android 音视频学习:使用三种不同的方式绘制图片

本系列是个人 Android 音视频学习总结,这是第一篇,主要学习内容是:

在 Android 平台上绘制一张图片,使用三种不同的 API,ImageView、SurfaceView、自定义 View。

ImageView 绘制图片

这种方式较为普遍简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ImageViewActivity extends BaseActivity {

private ImageView mImageView;

@Override
protected View getContentView() {
mImageView = new ImageView(this);
return mImageView;
}

@Override
protected int getTitleResId() {
return R.string.image_view;
}

@Override
protected void initView() {
super.initView();
// 绘制图片
mImageView.setImageBitmap(FileUtil.getDrawImageBitmap(this));
}
}

SurfaceView 绘制图片

SurfaceView 是 View 的一个子类,特点在于其实现了双缓冲技术,适用于频繁刷新页面的场景。

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
public class SurfaceViewActivity extends BaseActivity {

private SurfaceView mSurfaceView;

@Override
protected int getTitleResId() {
return R.string.surface_view;
}

@Override
protected View getContentView() {
mSurfaceView = new SurfaceView(this);
return mSurfaceView;
}

@Override
protected void initView() {
super.initView();
mSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (holder == null) {
return;
}

Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.STROKE);

Canvas canvas = holder.lockCanvas();
// 绘制图片
canvas.drawBitmap(FileUtil.getDrawImageBitmap(SurfaceViewActivity.this), 0, 0, paint);
holder.unlockCanvasAndPost(canvas);
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {

}
});
}
}

自定义 View 绘制图片

还可以通过自定义 View 绘制图片。

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
public class CustomView extends View {

private Paint mPaint;
private Bitmap mBitmap;

public CustomView(Context context) {
this(context, null);
}

public CustomView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mBitmap = FileUtil.getDrawImageBitmap(getContext());

}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mBitmap != null) {
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
}
}
}

具体源码看这里:AndroidMultiMediaLearning,下一篇总结使用 AudioRecord 和 AudioTrack API 完成音频 PCM 数据的采集和播放,并实现读写音频 wav 文件。

如何让百度收录 GitHub Pages 个人博客

很多程序员朋友都有在 GitHub Pages 上搭建自己的个人博客,对于个人博客,没有被搜索引擎收录的话,别人基本是是看不到的,再好的技术文无法被分享也是白搭。

基于 GitHub Pages 的个人博客, Google 收录非常及时全面。然而,到目前为止,GitHub 还是拒绝百度爬虫的访问,直接返回 403。

官方给出原因是,百度爬虫爬得太狠,影响了 Github Pages 服务的正常使用。这就导致了,但凡在 Github Pages 搭建的个人博客,都无法被百度收录。

现有的解决办法

1、使用 coding.net 建立镜像网站

我之前使用过 coding.net,在本地 repo 的配置文件中同时添加 GitHub 和 coding.net 远程 repo 地址,发布时,两边都会部署到,加上域名智能解析,对于国内的请求,转发到 Coding Page 即可。

但是通过 coding.net 访问个人主页时会先出现跳转页面,导致百度无法正确爬取。

2、利用 CDN

这个没试过,理论上来说,百度在第一次爬取时,CDN 上必须要已经有相应页面的缓存,否则,爬取的请求会被转发到 GitHub 源站,GitHub 还是会拒绝。

3、使用 Nginx 反向代理

Nginx 做反向代理,直接代理百度爬虫,去 GitHub Pages 请求,然后将结果返回给百度爬虫。

这种方式可行,只不过,这些方法都需要一定的定制能力,对于个人开发者,还得买一台 VPS 或者云服务器。

可靠、免费还简单的方法

Guillermo Rauch 大神创业搞了一个静态站 hosting 服务 zeit.co,可以通过 GitHub Hooks 实现自动部署,zeit 提供 存储 + CDN + DNS 一套完整的服务。

我给个人网站配置完成后,去百度站长试了一下,发现抓取成功了,sitemap 也提交成功了,坐等百度收录。

1

下面我把配置的步骤记录下来,给有需要的朋友一个参考。

zeit 网站主要就三个步骤:

  • Github 账户登陆 zeit.io,授予 zeit repo 的 read 权限;

  • 导入 GitHub 博客 repo;

  • 稍等片刻,部署成功。

项目名中的 . 自动替换成 -,生成了一个类似于 xxxx.now.sh 的链接,点击可以访问你的博客主页,这时候静态资源已经部署到 zeit 的边缘 CDN 节点上了,下次你 GitHub 项目的任何更新会触发 zeit 项目更新。

接下来的就是切换域名,通过智能 DNS 将国内流量切过去。通过 zeit.io 提供的 DNS 解析服务配置自己的域名,然后在百度站长里配置信息。

在 Domains 下为项目添加你的个人域名。

我添加后出现以下配置错误,原因我的域名权威 dns 是 dnspod。

一种解决方式是将直接使用 zeit 提供的 nameserver 智能 DNS,另一种方式,就是保留 dnspod 作为权威 dns 服务器,但是要添加一条 ANAME 记录。

有两张配置方式,一种是改 nameserver,我用的是这种,权威dns服务器改成左边那些,我看到你还是用的dnspod来解析的。另一种方式,就是保留dnspod作为权威dns服务器,但是要添加一条ANAME记录。

我使用的是第一种方式,直接在阿里云替换了 DNS 服务器,直接用 zeit 提供的 nameserver 智能 DNS。

回到 zeit,刷新下,正常是这样,这里是给你签发 https 证书,免费的。

过一会儿应该就好了。

看一下 DNS 解析地址,说明 zeit 域名已经配置成功了。

最后就是在百度站长里面添加个人域名了。这里注意选择 https 协议,因为 zeit 默认都是 https 了。

网站验证我采用的是文件验证,下载验证文件放在你博客本地 repo 的 source 目录下,部署到 GitHub,当然也会及时更新到 zeit。然后完成验证就好了,试一下链接诊断,看能不能正常抓取,失败的话,看看抓取的 ip 地址是不是还是之前的缓存,等待一段时间重新抓取下,时间取决于 dns 的 ttl。

zeit.co 官网上看,台湾和香港都有 CDN 节点,免费账户可以有 20G/月,个人博客应该是够用了。

配置还是很简单的,赶紧试试吧,有问题欢迎交流。

Retrofit 源码分析

前面的文章我们分析了 OkHttp 的核心源码,而 Retrofit 与 OkHttp 的结合使用,也是目前主流的方式,这篇文章主要分析下目前 Android 最优秀的网络封装框架 Retrofit。

在分析 Retrofit 源码之前,先看下 Retrofit 的简单使用。

基本使用

一般情况,Retrofit 的使用流程按照以下三步:

1、将 HTTP API 定义成接口形式

1
2
3
4
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}

2、构建 Retrofit 实例,生成 GitHubService 接口的实现。

1
2
3
4
5
6
// Retrofit 构建过程
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();

GitHubService service = retrofit.create(GitHubService.class);

3、发起网络请求,可以做同步或异步请求。

1
2
3
Call<List<Repo>> repos = service.listRepos("octocat");

call.execute() 或者 call.enqueue()

这里,Retrofit 用注解标识不同的网络请求类型,极大的简化了 OkHttp 的使用方式。

这篇文章主要关注的几个问题:

  • Retrofit 实例是如何创建的,它初始化了哪些东西?

  • GitHubService 实例是如何创建的,这些注解是如何映射到每种网络请求的 ?

  • 网络请求的流程是怎样的?

Retrofit 创建过程

1
2
3
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build();

这里可以看出,Retrofit 实例是使用建造者模式通过 Builder 类进行创建。

建造者模式简言之:将一个复杂对象的构建与表示分离,使得用户在不知道对象的创建细节情况下可以直接创建复杂的对象。

Retrofit

Retrofit 包含 7 个成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public final class Retrofit {
// 网络请求配置对象,存储网络请求相关的配置,如网络请求的方法、数据转换器、网络请求适配器、网络请求工厂、基地址等
private final Map<Method, ServiceMethod<?>> serviceMethodCache = new ConcurrentHashMap<>();

// 网络请求器的工厂:生产网络请求器
final okhttp3.Call.Factory callFactory;

// 网络请求的 URL 地址
final HttpUrl baseUrl;

// 数据转换器工厂的集合
final List<Converter.Factory> converterFactories;

// 网络请求适配器工厂的集合
final List<CallAdapter.Factory> callAdapterFactories;

// 回调方法执行器
final @Nullable Executor callbackExecutor;

// 是否缓存创建的 ServiceMethod
final boolean validateEagerly;
}

再看 Retrofit 构造函数,除了 serviceMethodCache, 其他成员变量都在这里进行赋值。

1
2
3
4
5
6
7
8
9
10
Retrofit(okhttp3.Call.Factory callFactory, HttpUrl baseUrl,
List<Converter.Factory> converterFactories, List<CallAdapter.Factory> callAdapterFactories,
@Nullable Executor callbackExecutor, boolean validateEagerly) {
this.callFactory = callFactory;
this.baseUrl = baseUrl;
this.converterFactories = converterFactories; // Copy+unmodifiable at call site.
this.callAdapterFactories = callAdapterFactories; // Copy+unmodifiable at call site.
this.callbackExecutor = callbackExecutor;
this.validateEagerly = validateEagerly;
}

Retrofit.Builder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static final class Builder {
private final Platform platform; // 平台
private @Nullable okhttp3.Call.Factory callFactory; // 网络请求工厂,默认使用OkHttpCall(工厂方法模式)
private @Nullable HttpUrl baseUrl; // 网络请求URL地址
private final List<Converter.Factory> converterFactories = new ArrayList<>(); // 数据转换器工厂的集合
private final List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(); // 网络请求适配器工厂的集合
private @Nullable Executor callbackExecutor; // 回调方法执行器
private boolean validateEagerly;

Builder(Platform platform) {
this.platform = platform;
}

public Builder() {
this(Platform.get());
}
...
}

这里主要关注 Platform,在 Builder 构造函数中调用了 Platform.get() ,然后赋值给自己的 platform 变量,我们来看看 Platform 类。

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
class Platform {
private static final Platform PLATFORM = findPlatform();

static Platform get() {
return PLATFORM;
}

private static Platform findPlatform() {
try {
// 判断是否是 Android 平台
Class.forName("android.os.Build");
if (Build.VERSION.SDK_INT != 0) {
return new Android();
}
} catch (ClassNotFoundException ignored) {
}
try {
// Java 平台
Class.forName("java.util.Optional");
return new Java8();
} catch (ClassNotFoundException ignored) {
}
return new Platform();
}
}

Platform.get() 方法会调用 findPlatform() 方法,这里主要是判断是 Android 平台还是 Java 平台,如果是 Android 平台会返回一个 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static class Android extends Platform {
@IgnoreJRERequirement // Guarded by API check.
@Override boolean isDefaultMethod(Method method) {
if (Build.VERSION.SDK_INT < 24) {
return false;
}
return method.isDefault();
}

@Override public Executor defaultCallbackExecutor() {
return new MainThreadExecutor();
}

@Override List<? extends CallAdapter.Factory> defaultCallAdapterFactories(
@Nullable Executor callbackExecutor) {
if (callbackExecutor == null) throw new AssertionError();
DefaultCallAdapterFactory executorFactory = new DefaultCallAdapterFactory(callbackExecutor);
return Build.VERSION.SDK_INT >= 24
? asList(CompletableFutureCallAdapterFactory.INSTANCE, executorFactory)
: singletonList(executorFactory);
}

@Override int defaultCallAdapterFactoriesSize() {
return Build.VERSION.SDK_INT >= 24 ? 2 : 1;
}

@Override List<? extends Converter.Factory> defaultConverterFactories() {
return Build.VERSION.SDK_INT >= 24
? singletonList(OptionalConverterFactory.INSTANCE)
: Collections.<Converter.Factory>emptyList();
}

@Override int defaultConverterFactoriesSize() {
return Build.VERSION.SDK_INT >= 24 ? 1 : 0;
}

static class MainThreadExecutor implements Executor {
private final Handler handler = new Handler(Looper.getMainLooper());

@Override public void execute(Runnable r) {
handler.post(r);
}
}
}

关注三个重要的方法:

  • defaultCallbackExecutor: 返回默认的 Executor 对象,正是 Retrofit 的成员变量回调执行器,它的内部采用 Handler 负责子线程到主线程的切换工作。

  • defaultCallAdapterFactories:返回的是默认的 CallAdpter.Factory 的集合,也就是 Retrofit 的成员变量网络请求适配器工厂集合,如果是 Android 7.0 以上或者 Java 8,使用并发包中的 CompletableFuture 保证了回调的同步。

  • defaultConverterFactories:返回的是默认的 Converter.Factory 的集合,也就是 Retrofit 的成员变量数据转换器工厂集合。

build 过程

接着看一下 Builder.build() 方法。

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
public Retrofit build() {
if (baseUrl == null) {
throw new IllegalStateException("Base URL required.");
}

okhttp3.Call.Factory callFactory = this.callFactory;
if (callFactory == null) {
// 默认使用 OkHttp
callFactory = new OkHttpClient();
}

Executor callbackExecutor = this.callbackExecutor;
if (callbackExecutor == null) {
// 默认的 callbackExecutor
callbackExecutor = platform.defaultCallbackExecutor();
}

// 添加你配置的 CallAdapter.Factory 到 List,然后把 Platform 默认的 defaultCallAdapterFactories 添加到 List
// Make a defensive copy of the adapters and add the default Call adapter.
List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor));

// 添加 BuiltInConverters 和手动配置的 Converter.Factory 到 List,然后把 Platform 默认的 defaultConverterFactories 添加到 List
// Make a defensive copy of the converters.
List<Converter.Factory> converterFactories = new ArrayList<>(
1 + this.converterFactories.size() + platform.defaultConverterFactoriesSize());

// Add the built-in converter factory first. This prevents overriding its behavior but also
// ensures correct behavior when using converters that consume all types.
converterFactories.add(new BuiltInConverters());
converterFactories.addAll(this.converterFactories);
converterFactories.addAll(platform.defaultConverterFactories());

// 返回一个 Retrofit 对象
return new Retrofit(callFactory, baseUrl, unmodifiableList(converterFactories),
unmodifiableList(callAdapterFactories), callbackExecutor, validateEagerly);
}

至此,Retrofit 的创建流程就完成了,它的成员变量的值如下:

  • serviceMethodService:暂时为空的 ConcurrentHashMap

  • callFactory:默认OkHttpClient 对象

  • baseUrl:根据配置的 baseUrl,构建 HttpUrl 对象

  • callAdapterFactories:配置的和默认的网络请求适配器工厂集合

  • converterFactories:配置的和默认的数据转换器工厂集合

  • callbackExecutor:MainThreadExecutor 对象

  • validateEagerly:默认 false

创建网络请求接口实例

接着来看 GitHubService 实例是如何创建的。

1
GitHubService service = retrofit.create(GitHubService.class);

Service 创建

retrofit.create() 使用了外观模式和代理模式创建了网络请求接口实例。

外观模式:定义一个统一接口,外部与通过该统一的接口对子系统里的其他接口进行访问。

代理模式:通过访问代理对象的方式来间接访问目标对象。

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 <T> T create(final Class<T> service) {

// 对参数 service 进行校验, service 必须是一个接口,而且没有继承别的接口
Utils.validateServiceInterface(service);
// 判断是否需要提前验证
if (validateEagerly) {
eagerlyValidateMethods(service);
}
// 利用动态代理技术,自动生成 Service 接口的实现类,将 Service 接口方法中的参数交给 InvocationHandler 处理
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
private final Object[] emptyArgs = new Object[0];

@Override public @Nullable Object invoke(Object proxy, Method method,
@Nullable Object[] args) throws Throwable {
// Object 类的方法直接调用
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
// 如果是对应平台本身类就有的方法,直接调用
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
// 否则通过 loadServiceMethod 方法获取到对应 ServiceMethod 并 invoke
return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
}
});
}

retrofit.create() 返回的是代理对象 Proxy,并转换为 T 类型,即 GitHubService。这里利用了动态代理技术,自动生成 Service 接口的实现类,将 Service 接口方法中的参数交给 InvocationHandler 处理。

对于 Object 类本身独有以及对应平台本身存在的方法,就直接调用,否则通过 loadServiceMethod() 对 Service 接口中对应的 method 进行解析处理,之后对其调用 invoke() 方法。

可以看出,Retrofit 不是在创建 Service 接口实例时就立即对所有接口中的方法进行注解解析,而是采用了在方法被调用时才进行注解的解析,也就是懒加载。

validateEagerly 的作用

我们看看 validateEagerly 这个变量,看看它控制着什么。validateEagerly 为 true 会进入 eagerlyValidateMethods() 方法。

1
2
3
4
5
6
7
8
private void eagerlyValidateMethods(Class<?> service) {
Platform platform = Platform.get();
for (Method method : service.getDeclaredMethods()) {
if (!platform.isDefaultMethod(method) && !Modifier.isStatic(method.getModifiers())) {
loadServiceMethod(method);
}
}
}

这里循环取出接口中的 Method,调用 loadServiceMethod() loadServiceMethod() 先从 serviceMethodCache 获取 Method 对应的 ServiceMethod,如果有直接返回,否则对 Method 进行解析得到一个 ServiceMethod 对象,存入缓存中。

所以 validateEagerly 变量是用于判断是否需要提前验证解析的,默认为 false,如果在 Retrofit 创建时设置为 true,会对 Service 接口中所有方法进行提前解析处理。

ServiceMethod 创建过程

loadServiceMethod() 方法的具体实现如下,这里采用了 Double Check 的方式尝试从 serviceMethodCache 中获取 ServiceMethod 对象,如果获取不到则通过 ServiceMethod.parseAnnotations() 方法对该 method 的注解进行处理并将得到的 ServiceMethod 对象加入缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ServiceMethod<?> loadServiceMethod(Method method) {
// 1. 先从缓存中获取,如果有则直接返回
ServiceMethod<?> result = serviceMethodCache.get(method);
if (result != null) return result;
synchronized (serviceMethodCache) {
// 2. 这里又获取一次,原因是网络请求一般是多线程环境下,ServiceMethod 可能创建完成了
result = serviceMethodCache.get(method);
if (result == null) {
// 3. 解析方法注解,创建 ServiceMethod
result = ServiceMethod.parseAnnotations(this, method);
// 存入缓存
serviceMethodCache.put(method, result);
}
}
return result;
}

我们详细看一下 ServiceMethod 创建过程。 ServiceMethod.parseAnnotations() 方法具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
// 通过 RequestFactory 解析注解配置(工厂模式、内部使用了建造者模式)
RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);

Type returnType = method.getGenericReturnType();
if (Utils.hasUnresolvableType(returnType)) {
throw methodError(method,
"Method return type must not include a type variable or wildcard: %s", returnType);
}
if (returnType == void.class) {
throw methodError(method, "Service methods cannot return void.");
}
// HttpServiceMethod 解析注解的方法
return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
}

1、通过 RequestFactory 解析注解配置

通过工厂模式和建造者模式创建 RequestFactory,解析封装注解配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
return new Builder(retrofit, method).build();
}

Builder(Retrofit retrofit, Method method) {
this.retrofit = retrofit;
this.method = method;
// 获取网络请求接口方法里的注解
this.methodAnnotations = method.getAnnotations();
// 获取网络请求接口方法里的参数类型
this.parameterTypes = method.getGenericParameterTypes();
// 获取网络请求接口方法里的注解内容
this.parameterAnnotationsArray = method.getParameterAnnotations();
}

2、ServiceMethod 的创建

ServiceMethod 的创建在 HttpServiceMethod 的 parseAnnotations() 方法中。

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
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {
boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
boolean continuationWantsResponse = false;
boolean continuationBodyNullable = false;

Annotation[] annotations = method.getAnnotations();
Type adapterType;
// 如果方法是 kotlin 中的 suspend 方法
if (isKotlinSuspendFunction) {
// 获取 Continuation 的范型参数,它就是 suspend 方法的返回值类型
Type[] parameterTypes = method.getGenericParameterTypes();
Type responseType = Utils.getParameterLowerBound(0,
(ParameterizedType) parameterTypes[parameterTypes.length - 1]);
// 如果 Continuation 的范型参数是 Response,则说明它需要的是 Response,那么将 continuationWantsResponse 置为 true;
if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {
// Unwrap the actual body type from Response<T>.
responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
continuationWantsResponse = true;
} else {
// TODO figure out if type is nullable or not
// Metadata metadata = method.getDeclaringClass().getAnnotation(Metadata.class)
// Find the entry for method
// Determine if return type is nullable or not
}

adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
} else {
// 否则获取方法返回值的范型参数,即为请求需要的返回值的类型
adapterType = method.getGenericReturnType();
}

// 根据网络请求接口方法的返回值和注解类型
// 从 Retrofit 对象中获取对于的网络请求适配器
CallAdapter<ResponseT, ReturnT> callAdapter =
createCallAdapter(retrofit, method, adapterType, annotations);

// 得到响应类型
Type responseType = callAdapter.responseType();
if (responseType == okhttp3.Response.class) {
throw methodError(method, "'"
+ getRawType(responseType).getName()
+ "' is not a valid response body type. Did you mean ResponseBody?");
}
if (responseType == Response.class) {
throw methodError(method, "Response must include generic type (e.g., Response<String>)");
}
// TODO support Unit for Kotlin?
if (requestFactory.httpMethod.equals("HEAD") && !Void.class.equals(responseType)) {
throw methodError(method, "HEAD method must use Void as response type.");
}

// 根据网络请求接口方法的返回值和注解类型从Retrofit对象中获取对应的数据转换器
Converter<ResponseBody, ResponseT> responseConverter =
createResponseConverter(retrofit, method, responseType);

okhttp3.Call.Factory callFactory = retrofit.callFactory;
if (!isKotlinSuspendFunction) {
// 不是 suspend 方法的话则直接创建并返回一个 CallAdapted 对象
return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
} else if (continuationWantsResponse) {
//noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForResponse<>(requestFactory,
callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
} else {
//noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
return (HttpServiceMethod<ResponseT, ReturnT>) new SuspendForBody<>(requestFactory,
callFactory, responseConverter, (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
continuationBodyNullable);
}
}

HttpServiceMethod.parseAnnotations() 的主要作用就是获取 CallAdapter 以及 Converter 对象,并构建对应 HttpServiceMethod

  • CallAdapter :根据网络接口方法的返回值类型来选择具体要用哪种 CallAdapterFactory,然后获取具体的 CallAdapter。
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
private static <ResponseT, ReturnT> CallAdapter<ResponseT, ReturnT> createCallAdapter(
Retrofit retrofit, Method method, Type returnType, Annotation[] annotations) {
try {
//noinspection unchecked
return (CallAdapter<ResponseT, ReturnT>) retrofit.callAdapter(returnType, annotations);
} catch (RuntimeException e) { // Wide exception range because factories are user code.
throw methodError(method, e, "Unable to create call adapter for %s", returnType);
}
}

public CallAdapter<?, ?> callAdapter(Type returnType, Annotation[] annotations) {
return nextCallAdapter(null, returnType, annotations);
}

public CallAdapter<?, ?> nextCallAdapter(@Nullable CallAdapter.Factory skipPast, Type returnType,
Annotation[] annotations) {

int start = callAdapterFactories.indexOf(skipPast) + 1;

// 遍历 CallAdapter.Factory 集合寻找合适的工厂(该工厂集合在第一步构造 Retrofit 对象时进行添加)
for (int i = start, count = callAdapterFactories.size(); i < count; i++) {
CallAdapter<?, ?> adapter = callAdapterFactories.get(i).get(returnType, annotations, this);
if (adapter != null) {
return adapter;
}
}
...
}
  • 获取 Converter:根据网络请求接口方法的返回值和注解类型从 Retrofit 对象中获取对应的数据转换器,和创建 CallAdapter 基本一致,遍历 Converter.Factory 集合并寻找具体的 Converter。
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
private static <ResponseT> Converter<ResponseBody, ResponseT> createResponseConverter(
Retrofit retrofit, Method method, Type responseType) {
Annotation[] annotations = method.getAnnotations();
try {
return retrofit.responseBodyConverter(responseType, annotations);
} catch (RuntimeException e) { // Wide exception range because factories are user code.
throw methodError(method, e, "Unable to create converter for %s", responseType);
}
}

public <T> Converter<ResponseBody, T> responseBodyConverter(Type type, Annotation[] annotations) {
return nextResponseBodyConverter(null, type, annotations);
}

public <T> Converter<ResponseBody, T> nextResponseBodyConverter(
@Nullable Converter.Factory skipPast, Type type, Annotation[] annotations) {

int start = converterFactories.indexOf(skipPast) + 1;
for (int i = start, count = converterFactories.size(); i < count; i++) {
Converter<ResponseBody, ?> converter =
converterFactories.get(i).responseBodyConverter(type, annotations, this);
if (converter != null) {
//noinspection unchecked
return (Converter<ResponseBody, T>) converter;
}
}
...
}
  • 构建 HttpServiceMethod:根据是否是 kotlin suspend 方法分别返回不同类型的 HttpServiceMethod。如果不是 suspend 方法的话则直接创建并返回一个 CallAdapted 对象,否则根据 suspend 方法需要的是 Response 还是具体的类型,分别返回 SuspendForResponse 和 SuspendForBody 对象。

ServiceMethod.invoke()

ServiceMethod 是一个抽象类,invoke() 是一个抽象方法,具体实现在子类中。

1
2
3
abstract class ServiceMethod<T> {
abstract @Nullable T invoke(Object[] args);
}

它的子类是 HttpServiceMethod,HttpServiceMethod 的 invoke() 方法中,首先构造一个 OkHttpCall,然后通过 adapt() 方法实现对 Call 的转换。

1
2
3
4
5
6
7
8
9
abstract class HttpServiceMethod<ResponseT, ReturnT> extends ServiceMethod<ReturnT> {
@Override
final @Nullable ReturnT invoke(Object[] args) {
Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
return adapt(call, args);
}

protected abstract @Nullable ReturnT adapt(Call<ResponseT> call, Object[] args);
}

adapt() 是一个 抽象方法,所以具体实现在 HttpServiceMethod 的子类中。

HttpServiceMethod 有三个子类,非协程的情况是 CallAdapted,另外两个子类则是在使用协程的情况下为了配合协程的 SuspendForResponse 以及 SuspendForBody 类。

  • CallAdapted:通过传递进来的 CallAdapter 对 Call 进行转换。

  • SuspendForResponse:首先根据传递进来的 Call 构造了一个参数为 Response 的 Continuation 对象然后通过 Kotlin 实现的 awaitResponse() 方法将 call 的 enqueue 异步回调过程封装成 一个 suspend 的函数。

  • SuspendForBody:SuspendForBody 则是根据传递进来的 Call 构造了一个 Continuation 对象然后通过 Kotlin 实现的 await()awaitNullable() 方法将 call 的 enqueue 异步回调过程封装为了一个 suspend 的函数。

发起网络请求

创建 Call

1
Call<List<Repo>> repos = service.listRepos("octocat");

从前面的分析了解到,Service 对象是动态代理对象,当调用 listRepos() 方法时会调用到 InvocationHandler的 invoke() 方法,得到最终的 Call 对象。

如果没有传入 CallAdapter 的话,默认情况返回的 Call 是 OkHttpCall 对象,它实现了 Call 接口。

同步请求

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
@Override
public Response<T> execute() throws IOException {
okhttp3.Call call;

synchronized (this) {
if (executed) throw new IllegalStateException("Already executed.");
executed = true;

if (creationFailure != null) {
if (creationFailure instanceof IOException) {
throw (IOException) creationFailure;
} else if (creationFailure instanceof RuntimeException) {
throw (RuntimeException) creationFailure;
} else {
throw (Error) creationFailure;
}
}

call = rawCall;
if (call == null) {
try {
// 1. 创建 OkHttp 的 Call 对象
call = rawCall = createRawCall();
} catch (IOException | RuntimeException | Error e) {
throwIfFatal(e); // Do not assign a fatal error to creationFailure.
creationFailure = e;
throw e;
}
}
}

if (canceled) {
call.cancel();
}

// 2. 执行请求并解析返回结果
return parseResponse(call.execute());
}

很简单,主要就是创建 OkHttp 的 Call 对象,调用 Call 的 execute 方法,对 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
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
@Override
public void enqueue(final Callback<T> callback) {
checkNotNull(callback, "callback == null");

okhttp3.Call call;
Throwable failure;

synchronized (this) {
if (executed) throw new IllegalStateException("Already executed.");
executed = true;

call = rawCall;
failure = creationFailure;
if (call == null && failure == null) {
try {
// 1. 创建 OkHttp 的 Call 对象
call = rawCall = createRawCall();
} catch (Throwable t) {
throwIfFatal(t);
failure = creationFailure = t;
}
}
}

if (failure != null) {
callback.onFailure(this, failure);
return;
}

if (canceled) {
call.cancel();
}

// 2. 调用 Call 的异步执行方法
call.enqueue(new okhttp3.Callback() {
@Override
public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) {
Response<T> response;
try {
// 3. 解析返回结果
response = parseResponse(rawResponse);
} catch (Throwable e) {
throwIfFatal(e);
callFailure(e);
return;
}

try {
// 4. 执行回调
callback.onResponse(OkHttpCall.this, response);
} catch (Throwable t) {
throwIfFatal(t);
t.printStackTrace(); // TODO this is not great
}
}

@Override
public void onFailure(okhttp3.Call call, IOException e) {
callFailure(e);
}

private void callFailure(Throwable e) {
try {
callback.onFailure(OkHttpCall.this, e);
} catch (Throwable t) {
throwIfFatal(t);
t.printStackTrace(); // TODO this is not great
}
}
});
}

也很简单,主要就是创建 OkHttp 的 Call 对象,调用 Call 的 enqueue 方法,解析返回结果,执行回调。

总结

至此,Retrofit 的源码基本上就看完了,虽然还有很多细节没有提及,但 Retrofit 的整体流程很清晰了。

Retrofit 本质上是一个 RESTful 的 Http 网络请求框架的封装,通过大量的设计模式封装了 OkHttp,使得更加简单易用。它内部主要是用动态代理的方式,动态将网络请求接口的注解解析成 HTTP 请求,最后执行请求的过程。

建议将 Retrofit 的源码下载下来,使用 IDEA 可以直接打开阅读。我这边已经将源码下载下来,进行了注释说明,有需要的可以直接从 Android open framework analysis 查看。

参考

1、Android开源框架源码鉴赏:Retrofit

2、Android:手把手带你 深入读懂 Retrofit 2.0 源码

3、带你一步步剖析Retrofit 源码解析:一款基于 OkHttp 实现的网络请求框架

我的 2019 年个人总结

虽然 2020 年已经过去一周时间,但还是想记录一下我的 2019 年个人总结,复盘一下自己过去一年的工作、学习和生活状况。

工作方面

年初面了几家心仪的公司,但没有得到想要的结果,认识到自身实力一般,而且外界的环境也不是很好,也就还在现在的公司继续干着。

2018 年部门效益很差,那年年底还裁掉了一些同事。为了生存下来,2019 年整个团队像是在打一场战役,这一年大家都挺忙的,忙碌的时候加班比较严重,所幸团队成员的积极性还算高。

虽然如王兴说的 「2019 可能会是过去 10 年最差的一年,但却是未来 10 年最好的一年」,但庆幸的是 2019 年部门的业绩应该是超额完成了,这里面有每一个员工的付出和努力。

毕竟只有企业活得好,员工才能得到应得的回报。企业的成败,每一个员工都有责任,在一个企业里,应该尽职尽责做好自己的工作。

2020 年,不管自己在哪个企业,都希望自己保持主动、积极、尽职尽责。

技术学习方面

主要涉及了 Android 音视频知识, Android 性能优化,Android 源码分析以及 Android 面试系列等等。

其实对这一年的技术成长不是很满意。很多东西从一开始没有做好计划,导致学习的深度和效率不高,技术提升也并不显著。

所以最近几天,我会好好思考下,今年的技术学习路线到底是什么,做一份详尽的学习计划。

这一年在个人博客、公众号以及知乎等平台写了几十篇文章,数量还是太少了。

在互联网平台,做消费者的同时做一个生产者,比只做消费者的收获要大很多。因此我也通过写文章的方式分享一些自己的技术总结、个人见识、生活经验等等,也有帮助到一些朋友,解决了他们的问题,这是一件很棒的事情。只要能帮助到一个朋友,就说明了我的文章是有价值的。

2020 年我也会争取多多分享,目前确定的会长期持续更新的有 Android 面试系列文章和每周分享周刊。

技术之外,这一年主要是在微信阅读上面读书,大概读了三十几本书。相比比从前不看书的我,这个提升还是很大的。2020 年,继续保持。

生活方面

这一年可能是这一生极为重要的一个年份了。因为今年完成了结婚、生娃两件大事。和妻子结束了 9 年的恋爱马拉松,步入婚姻的殿堂,2019 年 12 月 27 日,喜提佩奇,平安喜乐,辛苦妻子。

这一年,身份角色的转变,伴随而来的是爱与责任。2020 年,多一点时间陪伴亲人。

以上,算是 2019 年的一点总结。

其实一年的时间非常快。如果没有目标,没有预先做好计划,按计划执行,一年的时间很容易被荒废,到年底会发现这一年啥事没做成。

2020 年,有一些目标和想法,没必要轻易说出来,关键还是看行动吧。

2019 年,感谢大家的关注,2020 年祝福每一位朋友,万事顺意,平安健康。

每周分享第 6 期

这里记录过去一周,我看到的值得分享的内容。

本周刊开源(GitHub: zywudev/weekly),欢迎投稿,或者推荐好玩的东西。

titu

(题图:Payson Wick)

资源

1、Python最佳实践指南

《Python 开发最佳实践指南》,中文版开源电子书,翻译自英语原版。

2、微软 REST API 设计指南

微软官方出品的 REST API 指导规范。

3、Web 开发者 2020 年学习指南

GitHub 上有一位开发者根据 Udemy 的热门课程,整理了一份 Web 开发者 2020 年学习指南。其中包含常用的 Web 开发工具、设计软件、主流框架、基础知识、后端 & DevOps 技术堆栈等分类。

工具

1、Code Search

Android 官方最近为 AOSP 引入了代码搜索工具。

体验了下,挺好用的,可以在任意开源分支上切换,还支持交叉引用查找,感觉可以替代掉第三方的网站了。

2、网红脸生成器

一个网红脸生成器项目(非真实人脸)这是一个用 StyleGAN 训练出的网红脸生成器。

作者还开源了其他几个人脸生成器。

64_examples

3、Crater

一款免费开源的 Web 与移动端发票应用:Crater。

可跟踪记录日常费用支出情况,并生成专业发票与消费报告。界面设计清新而简洁,适用于自由职业者或小型企业。

文摘

1、《孩子王》阿城

我的父亲是世界中力气最大的人。他在队里扛麻袋,别人都比不过他。我的父亲又是世界中吃饭最多的人。家里的饭,都是母亲让他吃饱。这很对,因为父亲要做工,每月拿钱来养活一家人。但是父亲说:“我没有王福力气大。因为王福在识字。”父亲是一个不能讲话的人,但我懂他的意思。队上有人欺负他,我明白。所以我要好好学文化,替他说话。父亲很辛苦,今天他病了,后来慢慢爬起来,还要去干活,不愿失去一天的钱。我要上学,现在还替不了他。早上出的白太阳,父亲在山上走,走进白太阳里去。我想,父亲有力气啦。

言论

1、

最后十年

在我 20 来岁的时候,我觉得青春只剩最后十年了

在我 30 来岁的时候,我觉得职场只剩最后十年了

在我 40 来岁的时候,我觉得人生只剩最后十年了

在我 50 来岁的时候,我觉得精力只剩最后十年了

在我 60 来岁的时候,我觉得生命只剩最后十年了

是的,我们人生只有最后十年……

—— hao chen

2、

我们的人生是由命运和因果报应两条法则互相交织而成的。这两者互相干涉,比如当命运非常恶劣时,做一点好事,并不会出现好的结果,因为仅有的一点善行为强势的厄运所淹没。同样,当好运非常旺盛时,稍稍做点坏事,也不会马上出现恶因招恶果的情形。

– 稻盛和夫

3、

任何傻瓜都能写计算机能理解的代码,优秀的程序员编写人类能够理解的代码

– Martin Fowler

每周分享第 5 期

这里记录过去一周,我看到的值得分享的内容。

titu

(题图:Duy Hoang

文章

1、兽爷丨大象踩了他一脚

大公司欺负员工真的是轻而易举,就像大象踩蚂蚁一样。

2、华为做过的恶

收集华为作过的恶,记录这些不应该被遗忘的历史。

工具

1、Snipaste

Snipaste 是一个简单但强大的截图工具,也可以让你将截图贴回到屏幕上!下载并打开 Snipaste,按下 F1 来开始截图,再按 F3,截图就在桌面置顶显示了。就这么简单!

资源

1、Best-websites-a-programmer-should-visit

学习计算机软件,一些很有用的网站。

言论

1、

原中央网信办副主任彭波对华为事件的评论

  • 水可以载舟,也可以覆舟。舆论可以把你追捧成“民族英雄”,也可以把你踩在脚下,而且很可能是同一批人。 应该看到“捧”你和“踩”你,都是“爱”你——踩你时,绝大部分网友是希望你更好,希望一个伟大的公司更加伟大,一个心目中偶像级的企业更加完美。 要善待这种爱心。 网上最恐怖的情形,不是沸反盈天、骂声一片,而是鸦雀无声、万马齐喑。

  • 此事标志着在新的社会主要矛盾背景下,人们对于公平正义的追求更加强烈。人们用更加严苛的眼光看待企业,看待社会,涉及“公平正义”的问题,往往触动全社会最敏感的神经。

  • 不管是谁,永远要敬畏互联网,敬畏网民,敬畏舆论;不管是多大的企业,千万不要傲慢,不要任性。

  • 千万不要把事情随便上纲上线,不要把一般的舆情事件政治化,尽管可能有“境内外阶级敌人”的破坏。

  • 网上舆论的治理如同治水,正确的方法是疏堵结合,以疏为主。有时候处理不当带来的次生舆情,危害大于原始舆情。

  • 同情“弱者”,不符合法治精神,“强者”也有合法的权益,真理不一定掌握在“弱者”手上,但同情“弱者”符合舆情规律。

  • 机构(包括政府机关和企业)处理事情的思维逻辑是“法理情”,先考虑是否合法,再考虑是否有理,最后考虑民众情感上能否接受;而民众处理事情的逻辑是“情理法”,先考虑感情上能否接受。

  • 重大舆情,要由公司公共关系部门统筹处理,实务部门和法务部门在需要时才出现;实务部门和法务部门不能主导舆情处置。

  • 永远留有后手,不要轻易把自己置于悬崖边上。 只有公开透明,才能赢得公平公正。世界上没有不明真相的群众,只有掌握真相又没有说明真相的机构。在一片谴责之声出现时,机构仍不说明真相,这里一定有“难言之隐”,大家要有耐心,让子弹再飞一会儿。拒绝恶意猜测,拒绝恶意炒作。

2、

拉里佩奇的管理法则

  • 不要推诿:自己去做任何能让产品研发进展更快的事情。

  • 如果你不能为产品增值,那么你就不要成为碍事的人。让那些真正做事的人相互讨论,你去做其他事情吧。

  • 不要官僚主义。

  • 创意比年龄更重要。提出想法的人很初级不意味着TA不应该获得尊重和合作。

  • 最糟糕的事情就是只会说「不,不行。」,而不提出切实可行的改进措施来。

3、

关于251,看到一个妙评:大象踩了一脚蚂蚁,没踩死,对蚂蚁说,你可以踩我一脚。

图片

互联网现状

1、

photo1

2、

photo2

3、

photo3

Android 面试题(8):抽象类和接口的区别?

抽象类和接口的区别

1、抽象类可以提供成员方法的实现细节,而接口中只能存在 public 抽象方法;

接口在 Java 长达 20 多年的时间中,都只能拥有抽象方法,直到 JDK1.8 才能拥有实现的方法(还必须用default关键字修饰)

2、抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是 public static final 类型的;

3、接口中不能含有构造器、静态代码块以及静态方法,而抽象类可以有构造器、静态代码块和静态方法;

4、一个类只能继承一个抽象类,而一个类却可以实现多个接口;

5、抽象类访问速度比接口速度要快,因为接口需要时间去寻找在类中具体实现的方法。

换个角度思考,抽象类和接口的区别在于设计目的不同。

接口的设计目的,是对类的行为进行约束,强制不同的类具有相同的行为,它只约束行为的有无,不对如何实现进行限制。

抽象类的设计目的,是代码复用。当不同的类具有相同的行为,且其中一部分行为的实现方式一致时,可以让这些类都派生于一个抽象类。

为什么接口中不能定义变量?

如果接口可以定义变量,但是接口中的方法又都是抽象的,在接口中无法通过行为来修改属性。有的人会说了,没有关系,可以通过实现接口的对象的行为来修改接口中的属性。这当然没有问题,但是考虑这样的情况。如果接口 A 中有一个public 访问权限的静态变量 a。按照 Java 的语义,我们可以不通过实现接口的对象来访问变量 a,通过 A.a = xxx; 就可以改变接口中的变量 a 的值了。正如抽象类中是可以这样做的,那么实现接口 A 的所有对象也都会自动拥有这一改变后的 a 的值了,也就是说一个地方改变了 a,所有这些对象中 a 的值也都跟着变了。这和抽象类有什么区别呢,怎么体现接口更高的抽象级别呢,怎么体现接口提供的统一的协议呢,那还要接口这种抽象来做什么呢?所以接口中不能出现变量,如果有变量,就和接口提供的统一的抽象这种思想是抵触的。所以接口中的属性必然是常量,只能读不能改,这样才能为实现接口的对象提供一个统一的属性。

通俗的讲,你认为是要变化的东西,就放在你自己的实现中,不能放在接口中去,接口只是对一类事物的属性和行为更高层次的抽象。对修改关闭,对扩展(不同的实现 implements)开放,接口是对开闭原则的一种体现。

参考:Java中接口里定义的成员变量

每周分享第 4 期

这里记录过去一周,我看到的值得分享的内容。

titu

(题图: Martin Schmidli

文章

1、和死神赛跑:趁父亲还在世,我想用人工智能留住他

《连线》杂志的一篇文章《我用 AI 机器人留住去世的父亲》,作者在得知父亲肺癌晚期后,录下了自己与父亲的对话。后来利用这些对话资料,建造了一个人工智能对话机器人。父亲去世以后,跟机器人对话,机器人说出父亲会说的话。

有网友把这篇文章翻译成了中文,读完很是感动。

dadbot-opener

2、网易裁员事件引发的思考:5点建议,越早懂,越能保护自己

这一周,一篇《网易裁员,让保安把身患绝症的我赶出公司。》的文章获得广泛关注。 最后双方也达成了和解。

作为普通群众,我们应该从这件事中有所获得,我们应该学到的也许不是公关技巧,而是被很多人忽略的《劳动合同法》的基本知识。

3、深度调查:人贩子“梅姨”身后嗜血的“寻人灰产”

寻亲背后一直有一条灰色产业链,只是你不知道。希望这些吃人血馒头的恶人能被绳之以法。

另外推荐大家了解下公安部失踪儿童消息紧急发布平台,也叫着 「团圆系统」。

工具

1、Carbon

一个生成代码图片的网站,很多好看的样式。

carbon

2、codelf

变量命名神器,写代码不知道变量怎么命名就去这里查,命令多来自与 GitHub 爬虫,具备参考价值。

codelf

3、Saladict

聚合词典专业划词翻译,浏览器插件必备。sala

4、Sourcetrail

一个免费开源、跨平台的可视化源码探索项目。

特地为它写了一篇文章:开源免费的源码阅读神器 Sourcetrail

sourcetrail

5、英语轻松读

英语轻松读是针对已经有一定英语阅读能力的人群推出的阅读辅助工具。在看新闻的同时学习英语。

目前 ios App 已经上架,Android 正在开发中,稍等几天即可。

learn_english

资源

1、English Learning Blog by EngFluent – EngFluent

这个博客分享学习英语的经验和方法,听说读写都有。

言论和笑话

1、

在计算机科学中只有两件难事:缓存失效和命名 。

– Phil Karlton

2、

未来的世界,我们每人都必须要用一个政府发的手机,收集每个人的数据,且每天都要用手机的人脸识别进行微笑打卡,手机每天推送的文章都会有是否已读标识,如果没读,就会打电话催你,读了就要回复写心得,Al会自动判分,一天要挣够积分,挣不够分就是价值观有问题,要去补习班参加集体学习……

– Hao Chen

3、

我们这个币圈,不容易啊,真的不容易啊,大家这种有梦想,怀揣着一夜暴富的心理来做币,遇到这种情况真是,真是特别糟糕,我们一定要就是(哽咽),大家一定要(抽泣),同心协力(破声),好吗?

– 币圈维权群语音