Younger的个人站点 不正经码农 2020-01-08T15:09:10.358Z http://youngerdev.com/ Younger Hexo Android Handler 机制及线程间通信 http://youngerdev.com/Android-Handler-机制及线程间通信.html 2019-11-28T14:52:44.000Z 2020-01-08T15:09:10.358Z 本文主要分析 Handler机制和源码,线程切换的原理,下面是大致的目录:
  • 子线程可以更新UI吗,为什么
  • 常用的更新子线程更新切换方式
  • Handler 源码解析
  • handler.post原理
  • runOnUiThread原理
  • 主线程一直死循环取消息,为什么没有卡死
  • 子线程间怎么发送消息

在onCreate()中开启子线程更新UI 有问题吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MainActivity extends Activity {
private TextView mName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mName = (TextView) findViewById(R.id.tv_name);
mName.setText("我是在UI线程更新UI");
new Thread(new Runnable() {
public void run() {
mName.setText("我是在非UI线程更新UI");
}
}).start();
}
}

这样可以看到更新成功了,且没有抛出异常. 子线程能直接更新UI ? 其实是不能。

为什么不能在子线程更新UI?

因为Android的UI控件不是线程安全的,多个线程并发访问可能会导致UI 控件处于不可预期的状态,那既然这样,为什么不加锁呢?
缺点: 加锁会让UI访问的逻辑变的复杂,也会降低UI 的访问效率,因为锁的机制会阻塞某些线程的执行. 所有就采用单线程的方式来更新UI

1
2
3
4
5
6
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}

但是上面为什么可以更新UI呢?

因为执行速度, 因为 ViewRootImpl 这个时候还没创建,这是在调用了 onResume 之后才创建的, ViewRootImpl关于UI 的操作都会 checkThread 如果不在主线程就会抛出异常。所以上面更新的时候还没执行到 checkThread 方法。

提到异步处理消息,我们常用的子线程更新UI 的方法有哪些呢?

  • Handler.sendMessage()
  • Handler的post()方法。
  • Activity的runOnUiThread()方法。
  • View.post(Runnable r)方法。

作为一个Android 开发,我们肯定会想到 Handler ,下面是一个最简单的但是不太规范的示例,这样我们就可以在子线程中做了处理然后更新 UI 了。

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

Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

new Thread(new Runnable() {
@Override
public void run() {

Message msg = handler.obtainMessage();
handler.sendMessage(msg);
}
}).start();
}

Handler 可以理解为处理器

首先看下构造方法 我们最常用的 new handler方法做了什么

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
    public Handler() {
this(null, false);
}
//调用2个参数的构造方法


public Handler(Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
// 为了代码整洁,省略了部分内容
。。。。
}

mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}


我们再看下其他不同参数个数的构造方法


public Handler(Looper looper, Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}

根据上面的构造方法可以看出来, 我们在创建Handler 时,如果不指定 callback 时,会默认为空, 如果没有指定 Looper 时,系统会自动 通过 Looper.myLooper() 帮我们指定当前线程的 Looper 。 如果 looper 对象为空,就会抛出异常。

我们看下 Looper.myLooper() 是怎么实现的呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}


再看get方法怎么实现的

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

根据方法和资料我们可以知道, ThreadLocal 是所属与线程的,使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。

根据经验,有get 肯定有set 的地方,我们看下 Looper 中的 ThreadLocal 的set 的地方

1
2
3
4
5
6
7
8
9
10
public static void prepare() {
prepare(true);
}

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

根据上面可以看到, 一个线程中最多只能有一个 Looper ,在想使用到 Looper 的线程 中调用 prepare 方法就可以创建出 Looper了, 在 new handler 时就不会出现 looper 为空的情况了

但是有没有发现,我们在主线程就是没有调用 prepare 方法呀, 使用的时候也没有报错。这又是怎么回事呢?

通过看App 启动流程的代码可以发现, 在 ActivityThread 类中的 main 方法 这个类中的main方法就是整个App 的主线程的执行入口

在这个方法中,通过调用 Looper.prepareMainLooper() 去初始化了主线程的 looper

1
2
3
4
5
6
7
8
9
10
11


public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}

可以看到 prepare(false); 创建了一个不可以退出的 looper。

然后 main 方法中还通过调用 Looper.loop() 开启主线程的循环。

Looper.loop() 死循环,处理消息

直接看loop方法到底做了什么

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
  public static void loop() {

final Looper me = myLooper();
//有looper才能循环吧,获取当前线程的 looper
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
// 获取当前looper 中的 MessageQueue
final MessageQueue queue = me.mQueue;

。。。

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


。。。

try {
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
。。。

msg.recycleUnchecked();
}
}

中间省略部分源码, 跟着我们上面的分析,因为主线程在 App 初始化时在程序的住入口已经初始化过Looper 和开启了 loop 循环, 内容就是 从当前线程也就是主线程 的looper 关联的 MessageQueue 里不停的死循环 取出 Message 消息, 如果有消息就调用 msg.target.dispatchMessage(msg); 分发消息。

msg.target.dispatchMessage(msg);

处理消息。消息分发 但是这个 msg.target 又是什么东西呢

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
经过看源码 msg.target 的赋值是在这做的 Message 类中

public static Message obtain(Handler h, int what,
int arg1, int arg2, Object obj) {
Message m = obtain();
m.target = h;
m.what = what;
m.arg1 = arg1;
m.arg2 = arg2;
m.obj = obj;

return m;
}



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

msg.target其实就是初始化的handler,然后调用Handler的dispatchMessage();

从消息池中取出消息,如果没有的话就直接new一个Message对象,所以我们在写项目创建Message对象的时候尽量用handle.obtainMessage(),不要直接new Message(),复用会比较好。

知道 msg.target 就是 handler 了,那看下 handler 的 dispatchMessage 方法

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

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

先判断msg.的callback是否为空,接着再判断handler的mCallBack是否为空,
如果都为空,所以执行handleMessage(),这里面是一个空方法,需要我们重写。
上面那个 msg.callback 和 mCallback 怎么用呢?请接着往下看

handler.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
  
handler.post(new Runnable() {
@Override
public void run() {

}
});


public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}



private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}

最后把 Runnable 的callback 赋值成 Message 的 callback
再结合上面的 dispatchMessage 方法 , msg.callback 就不是空了


private static void handleCallback(Message message) {
message.callback.run();
}

调用 callback 的 run 方法。 handler 的 post 方法就这这么简单,
把一个可以可以执行的 runnable 放到 msg 的 callback 中抛到主线程,
调用 run 方法执行,然后这个 run 方法中的内容就是在主线程执行的了。



接下来看 创建Handler 的另一种方式,可以有个 callback 的写法, 这个时候
handler 中的 mCallback 就不为空了, 就可以执行了,直接执行 callback 中的方法即可
这个也是在当前线程(主线程)执行的。
Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
return false;
}
});

MessageQueue

MessageQueue的 next 方法做了什么操作呢

1
2
3
4
5
6
7
8
9
10
11
12
    Message next() {
...
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
//请注意这个方法 这个
nativePollOnce(ptr, nextPollTimeoutMillis);
...
}
...
}

也是死循环取消息 同一线程在同一时间只能处理一个消息,同一线程代码执行是不具有并发性,所以需要队列来保存消息和安排每个消息的处理顺序。

多个其他线程往UI线程发送消息,UI线程必须把这些消息保持到一个列表(它同一时间不能处理那么多任务),然后挨个拿出来处理,每一个Looper线程都会维护这样一个队列,而且仅此一个,这个队列的消息只能由该线程处理。

Message

Message 就没有太多可以说的,它就是一个消息的载体,用来保存消息的。

消息发送

常用的方法

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

public final boolean sendMessage(Message msg)
{
return sendMessageDelayed(msg, 0);
}


public final boolean sendEmptyMessage(int what)
{
return sendEmptyMessageDelayed(what, 0);
}


最后都会调用到

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

接着看 queue.enqueueMessage 是怎么处理的呢



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

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

msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}

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

主要目的就是把 Message 添加到 MessageQueue 中
如果消息在此时queue 中没有消息,就加入队列后 nativeWake 唤醒,通知取消息,
如果队列中有消息,就加入进去。

主线程一直 loop 死循环为什么没有卡死

主线程的死循环一直运行是不是特别消耗 CPU 资源呢? 其实不然,这里就涉及到 Linux pipe/epoll机制,简单说就是在主线程的 MessageQueue 没有消息时,便阻塞在 loop 的 queue.next() 中的 nativePollOnce() 方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。

这里采用的 epoll 机制,是一种 IO 多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步 I/O ,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量 CPU 资源。

其实这也是整个 Android 系统的做法,App 启动,然后就进入死循环,如果没有消息,就阻塞在哪些,AMS, WMS 等等 会通过binder抛过来一些消息,然后执行 onCreate 之类的方法,Activity 的生命周期的方法都是 msg,有消息过来就执行

runOnUiThread

1
2
3
4
5
6
7
8
9

@Override
public final void runOnUiThread(Runnable action) {
if (Thread.currentThread() != mUiThread) {
mHandler.post(action);
} else {
action.run();
}
}

可以看到代码 如果当前线程是主线程, 直接调用 Runnable 的run 方法,去执行, 如果不是主线程, 则调用 mHandler 的 post 方法 ,上面我们已经分析过 post 方法的原理了。 上面这个 mHandler 就是 Activity 的 Handler 也就是主线程的 Handler ,发送到主线程执行这个 Runnable。

子线程间怎么发送消息

根据上面的分析,我们在子线程中要使用handler 发送消息的话, 需要 手动在子线程的Handler 创建之前,调用 Looper.prepare 创建一个looper 来跟当前线程关联, 然后在创建完成 handler之后 调用 Looper.loop() 开启消息循环, 然后其他线程就可以通过这个线程创建出的 handler 往这个线程发送消息了。

总结

Handler 机制是现在各个公司面试必问的问题,掌握 Handler 原理对我们日常开发工作也是非常有帮助的。代码量也不大,比较好懂,作为一个 Android 开发工程师非常有必要掌握这些知识点。 欢迎交流学习。

参考文章

Android异步消息处理机制完全解析

Android 源码

]]>
<p>本文主要分析 Handler机制和源码,线程切换的原理,下面是大致的目录:</p> <ul> <li>子线程可以更新UI吗,为什么</li> <li>常用的更新子线程更新切换方式</li> <li>Handler 源码解析</li> <li>handler.post原理</
OkHttpUtils 源码解析 http://youngerdev.com/OkHttpUtils-源码解析.html 2019-10-20T05:51:35.000Z 2019-10-20T06:43:13.507Z 接上篇的 OkHttp 源码解析,目前项目中更多的用到的是 OkHttpUtilsOkHttp 所以有必要了解它的原理,以便遇到网络相关的问题时,可以及时的定位并解决问题,关于 OkHttp 源码相关的内容请看上篇, 请在阅读过上篇的基础上来看这篇会更好的理解,下面就开始吧。

OkHttpUtils 项目

本文的目录大致是这样:

  • OkHttpUtils 简单使用
  • OkHttpUtils 源码解析(V2.6.2)
  • OkHttp 和OkHttp 的对比

OkHttpUtils 简单使用

首先要在 gradle 中加上

1
compile 'com.zhy:okhttputils:2.6.2'

将直接使用okhttp默认的配置生成OkhttpClient,如果你有任何配置,记得在Application中调用initClient方法进行设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyApplication extends Application
{
@Override
public void onCreate()
{
super.onCreate();

OkHttpClient okHttpClient = new OkHttpClient.Builder()
// .addInterceptor(new LoggerInterceptor("TAG"))
.connectTimeout(10000L, TimeUnit.MILLISECONDS)
.readTimeout(10000L, TimeUnit.MILLISECONDS)
//其他配置
.build();

OkHttpUtils.initClient(okHttpClient);

}
}

在具体的使用过程中

GET 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
String url = "http://www.csdn.net/";
OkHttpUtils
.get()
.url(url)
.addParams("username", "hyman")
.addParams("password", "123")
.build()
.execute(new StringCallback()
{
@Override
public void onError(Request request, Exception e)
{

}

@Override
public void onResponse(String response)
{

}
});

Post JSON

1
2
3
4
5
6
7
OkHttpUtils
.postString()
.url(url)
.content(new Gson().toJson(new User("zhy", "123")))
.mediaType(MediaType.parse("application/json; charset=utf-8"))
.build()
.execute(new MyStringCallback());

能看出常用的GET ,POST 请求写起来非常简单流畅, 并且网络回调直接到主线程中了,可以直接处理数据。

OkHttpUtils 源码解析

我们通过这个get 请求,进去到源码中看下是如何封装的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
OkHttpUtils.get().url("http://www.baidu.com").build().execute(new com.zhy.http.okhttp.callback.Callback() {
@Override
public Object parseNetworkResponse(Response response, int id) throws Exception {
return null;
}

@Override
public void onError(Call call, Exception e, int id) {

}

@Override
public void onResponse(Object response, int id) {

}
});
}

OkHttpUtils

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

public static final long DEFAULT_MILLISECONDS = 10_000L;
private volatile static OkHttpUtils mInstance;
private OkHttpClient mOkHttpClient;
private Platform mPlatform;

public OkHttpUtils(OkHttpClient okHttpClient)
{
if (okHttpClient == null)
{
mOkHttpClient = new OkHttpClient();
} else
{
mOkHttpClient = okHttpClient;
}

mPlatform = Platform.get();
}


public static OkHttpUtils initClient(OkHttpClient okHttpClient)
{
if (mInstance == null)
{
synchronized (OkHttpUtils.class)
{
if (mInstance == null)
{
mInstance = new OkHttpUtils(okHttpClient);
}
}
}
return mInstance;
}

public static OkHttpUtils getInstance()
{
return initClient(null);
}


public Executor getDelivery()
{
return mPlatform.defaultCallbackExecutor();
}

public OkHttpClient getOkHttpClient()
{
return mOkHttpClient;
}

public static GetBuilder get()
{
return new GetBuilder();
}

public static PostStringBuilder postString()
{
return new PostStringBuilder();
}

public static PostFileBuilder postFile()
{
return new PostFileBuilder();
}

public static PostFormBuilder post()
{
return new PostFormBuilder();
}

public static OtherRequestBuilder put()
{
return new OtherRequestBuilder(METHOD.PUT);
}

public static HeadBuilder head()
{
return new HeadBuilder();
}

public static OtherRequestBuilder delete()
{
return new OtherRequestBuilder(METHOD.DELETE);
}

public static OtherRequestBuilder patch()
{
return new OtherRequestBuilder(METHOD.PATCH);
}

...

根据代码 能看出 OkHttpUtils 做了双重锁定的单例处理,因为一个App有一个 OkHttpClient 对象就行了。new 出了一个 OkHttpClient 。

我们在Application中初始化后,就生成这样一个对象, 以后每次用的时候就取得这个,无需重复创建。

实例化 OkHttpUtils 时 会创建 mPlatform = Platform.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
39
40
41
42
43
private static final Platform PLATFORM = findPlatform();

public static Platform get()
{
L.e(PLATFORM.getClass().toString());
return PLATFORM;
}

private static Platform findPlatform()
{
try
{
Class.forName("android.os.Build");
if (Build.VERSION.SDK_INT != 0)
{
return new Android();
}
} catch (ClassNotFoundException ignored)
{
}
return new Platform();
}


static class Android extends Platform
{
@Override
public Executor defaultCallbackExecutor()
{
return new MainThreadExecutor();
}

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

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

此外在OkHttpUtils的构造方法中可以注意到有一个mPlatform的变量,他会根据当前是Android还是其他平台的不同被初始化为Android主线程或者普通线程池。

当是 Android 系统,new 了一个Android ,里面一个内部类,实现了 Executor, 创建了一个 handler 传入 Looper.getMainLooper() 主线程的 Looper,线程池执行时在这把可执行的 runnable 发送到主线程, 然后执行,就实现了线程切换。

它的功能就是实现线程之间的切换的。

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
//GetRequest
@Override
public RequestCall build()
{
if (params != null)
{
url = appendParams(url, params);
}

return new GetRequest(url, tag, params, headers,id).build();
}

//postString

new PostStringBuilder();

public class PostStringBuilder extends OkHttpRequestBuilder<PostStringBuilder>
{
private String content;
private MediaType mediaType;


public PostStringBuilder content(String content)
{
this.content = content;
return this;
}

public PostStringBuilder mediaType(MediaType mediaType)
{
this.mediaType = mediaType;
return this;
}

@Override
public RequestCall build()
{
return new PostStringRequest(url, tag, params, headers, content, mediaType,id).build();
}


}

当我们调用build 方法时做了什么操作呢
public PostStringRequest(String url, Object tag, Map<String, String> params, Map<String, String> headers, String content, MediaType mediaType,int id)
{
super(url, tag, params, headers,id);
this.content = content;
this.mediaType = mediaType;

if (this.content == null)
{
Exceptions.illegalArgument("the content can not be null !");
}
if (this.mediaType == null)
{
this.mediaType = MEDIA_TYPE_PLAIN;
}

}

把我们设置的参数传递过来, 比如我们经常用的 mediaType 和 content 组装起来,
在执行网络请求的时候会把这些参数按照需要配置好,传入。



//OkHttpRequest

protected Request.Builder builder = new Request.Builder();

protected OkHttpRequest(String url, Object tag,
Map<String, String> params, Map<String, String> headers,int id)
{
this.url = url;
this.tag = tag;
this.params = params;
this.headers = headers;
this.id = id ;

if (url == null)
{
Exceptions.illegalArgument("url can not be null.");
}

initBuilder();
}

使用 get请求 new GetBuilder() 创建了一个继承 OkHttpRequest 的 GetRequest,配置需要的参数。

执行网络请求 execute

然后 看下 execute 的具体内容

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
    public void execute(Callback callback)
{
buildCall(callback);

if (callback != null)
{
callback.onBefore(request, getOkHttpRequest().getId());
}

OkHttpUtils.getInstance().execute(this, callback);
}


接着看 execute 的内容
可以看其最后只是将RequestCall和callback传递给了OkHttpUtils类的execute方法,
也就是说,最终还是调用了okhttp3.Call的enqueue()方法,在这里执行了真正的网络请求:


public void execute(final RequestCall requestCall, Callback callback)
{
if (callback == null)
callback = Callback.CALLBACK_DEFAULT;
//如果没有写回调,给了一个默认的
final Callback finalCallback = callback;
final int id = requestCall.getOkHttpRequest().getId();

//requestCall.getCall() 调用了 enqueue进行网络请求,你肯定能猜出来 requestCall.getCall()的内容,
//看下面吧, 我贴出来了,就是 返回了一个OkHttpClient创建的call。
requestCall.getCall().enqueue(new okhttp3.Callback()
{
@Override
public void onFailure(Call call, final IOException e)
{
sendFailResultCallback(call, e, finalCallback, id);
}

@Override
public void onResponse(final Call call, final Response response)
{
try
{
//如果请求被取消 就结束
if (call.isCanceled())
{
sendFailResultCallback(call, new IOException("Canceled!"), finalCallback, id);
return;
}

// 可以看到方法中 code码是 【200,300)的算是成功,其他的是失败
if (!finalCallback.validateReponse(response, id))
{
sendFailResultCallback(call, new IOException("request failed , reponse's code is : " + response.code()), finalCallback, id);
return;
}

Object o = finalCallback.parseNetworkResponse(response, id);
sendSuccessResultCallback(o, finalCallback, id);
} catch (Exception e)
{
sendFailResultCallback(call, e, finalCallback, id);
} finally
{
if (response.body() != null)
response.body().close();
}

}
});
}
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 Call buildCall(Callback callback)
{
request = generateRequest(callback);

if (readTimeOut > 0 || writeTimeOut > 0 || connTimeOut > 0)
{
readTimeOut = readTimeOut > 0 ? readTimeOut : OkHttpUtils.DEFAULT_MILLISECONDS;
writeTimeOut = writeTimeOut > 0 ? writeTimeOut : OkHttpUtils.DEFAULT_MILLISECONDS;
connTimeOut = connTimeOut > 0 ? connTimeOut : OkHttpUtils.DEFAULT_MILLISECONDS;

clone = OkHttpUtils.getInstance().getOkHttpClient().newBuilder()
.readTimeout(readTimeOut, TimeUnit.MILLISECONDS)
.writeTimeout(writeTimeOut, TimeUnit.MILLISECONDS)
.connectTimeout(connTimeOut, TimeUnit.MILLISECONDS)
.build();

call = clone.newCall(request);
} else
{
call = OkHttpUtils.getInstance().getOkHttpClient().newCall(request);
}
return call;
}

来看看处理返回成功的回调方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public void sendSuccessResultCallback(final Object object, final Callback callback, final int id)
{
if (callback == null) return;
mPlatform.execute(new Runnable()
{
@Override
public void run()
{
callback.onResponse(object, id);
callback.onAfter(id);
}
});
}

是不是有点眼熟 mPlatform 其实就是上面的那个 Android , 把这个 runnable 发送到主线程。

在本文中,okhttputils将初始化OkHttpClient的动作提取出来,这样同一个应用只需要在最开始的时候配置一下诸如网络超时、cookie等既可。

在具体的实现中,通过OkHttpRequestBuilder收集网络请求的属性并传递给OkHttpRequest,在其子类中按照不同的需要实现生成Request的方法。

OkHttpRequestBuilder的build()方法会生成RequestCall对象,RequestCall对象的execute()方法会调用OkHttpRequestBuilder对象的generateRequest()方法产生Request,并据此产生Call对象,最后通过该Call对象的enqueue方法执行网络请求。

至此 OkHttpUtils 的源码分析完成了,其实代码相对简单,就是对 OkHttp 的一个封装, 少写了一些代码, 把常用的 get , post ,postString,postFile,head,put, delete 都进行了一个封装,调用起来非常方便。

并且网络请求的返回内容也回调到主线程,方便进行 UI 操作,

这也解决了上一个篇文章中说的 OkHttp 的两个缺点。

]]>
<p>接上篇的 <a href="https://youngerdev.com/OkHttp-源码解析.html">OkHttp 源码解析</a>,目前项目中更多的用到的是 <a href="https://github.com/hongyangAndroid/okhttputi
OkHttp 源码解析 http://youngerdev.com/OkHttp-源码解析.html 2019-10-19T05:49:09.000Z 2019-10-20T12:15:08.908Z 接上篇的 Volley 源码解析,目前项目中更多的用到的是 OkHttpUtilsOkHttp 所以有必要了解它的原理,以便遇到网络相关的问题时,可以及时的定位并解决问题,下面就开始吧。

本文的目录大致是这样:

  • OkHttp 的基本使用
  • OkHttp 的源码解析(V3.5.0)
  • OkHttp 连接池复用
  • OkHttp 的优缺点

OkHttp 的基本使用

在 gradle 中添加依赖

1
compile 'com.squareup.okhttp3:okhttp:3.5.0'

1.首先创建OkHttpClient

1
OkHttpClient client = new OkHttpClient();

2.构造Request对象

1
2
3
4
Request request = new Request.Builder()
.get()
.url("https://www.baidu.com")
.build();

3.将Request封装为Call

1
Call call = client.newCall(request);

4.根据需要调用同步或者异步请求方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//同步调用,返回Response,会抛出IO异常
Response response = call.execute();

//异步调用,并设置回调函数
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {

}

@Override
public void onResponse(Call call, Response response) throws IOException {
Log.e("=====Younger==", "===" + (Looper.myLooper() == Looper.getMainLooper()));
//打印出的结果是false , 可以看出 这个回调并没有回到主线程,需要我们自己处理线程切换的问题
}
});

同步调用会阻塞主线程,一般不用

异步调用的回调函数是在子线程,我们不能在子线程更新UI,需要借助于runOnUiThread()方法或者Handler来处理

post 也是类似的, 相信大家都会用使用,接下来我们来看重头戏-源码。

OkHttp 源码解析

okHttpClient

首先来看,我们进行网络请求时使用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Call call = okHttpClient.newCall(request);

实际调用

@Override public Call newCall(Request request) {
return new RealCall(this, request, false /* for web socket */);
}


new 了一个 RealCall,这是它的构造方法

RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
this.client = client;
this.originalRequest = originalRequest;
this.forWebSocket = forWebSocket;
this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
}

RealCall

实际上的 Call 的 enqueue 调用的是 RealCall的 enqueue方法

1
call.enqueue(new ...);

下面我们看下 RealCall的 enqueue是如何实现的

1
2
3
4
5
6
7
8
@Override public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
captureCallStackTrace();
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}

可以看到最终的请求处理是 dispatcher 来完成的,接下来看下 dispatcher

dispatcher

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
 //最大并发请求书
private int maxRequests = 64;
//每个主机的最大请求数
private int maxRequestsPerHost = 5;
private Runnable idleCallback;

/** 执行的线程池. Created lazily. */
private ExecutorService executorService;

//将要运行的异步请求队列
/** Ready async calls in the order they'll be run. */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();

//正在执行的异步请求队列
/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
//正在执行的同步请求队列
/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();


public Dispatcher(ExecutorService executorService) {
this.executorService = executorService;
}

public Dispatcher() {
}

public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}

Dispatcher 有两个构造方法,可以自己指定线程池, 如果没有指定, 则会默认创建默认线程池,可以看到核心数为0,缓存数可以是很大, 比较适合执行大量的耗时比较少的任务。

接着看 enqueue是如何实现的

1
2
3
4
5
6
7
8
9

synchronized void enqueue(AsyncCall call) {
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}

当正在运行的异步请求队列中的数量小于64, 并且 正在运行的请求主机数小于5,把请求加载到runningAsyncCalls 中并在线程池中执行, 否则就加入到 readyAsyncCalls 进行缓存等待。

runningCallsForHost是如何实现的呢

1
2
3
4
5
6
7
8
9

/** Returns the number of running calls that share a host with {@code call}. */
private int runningCallsForHost(AsyncCall call) {
int result = 0;
for (AsyncCall c : runningAsyncCalls) {
if (c.host().equals(call.host())) result++;
}
return result;
}

正在执行的网络请求中 同一个host最多只能是5个。

上面可以看到传递进来的是 AsyncCall 然后 execute 那我们看下 AsyncCall方法

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
final class AsyncCall extends NamedRunnable {
private final Callback responseCallback;

AsyncCall(Callback responseCallback) {
super("OkHttp %s", redactedUrl());
this.responseCallback = responseCallback;
}

String host() {
return originalRequest.url().host();
}

Request request() {
return originalRequest;
}

RealCall get() {
return RealCall.this;
}

@Override protected void execute() {
boolean signalledCallback = false;
try {
//获取请求报文
Response response = getResponseWithInterceptorChain();
if (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
responseCallback.onFailure(RealCall.this, e);
}
} finally {
client.dispatcher().finished(this);
}
}
}

看到 NamedRunnable 实现了 Runnable,AsyncCall 中的 execute 是对网络请求的具体处理。

1
Response response = getResponseWithInterceptorChain();

能明显看出这就是对请求的处理,在看它的具体实现之前先看下 client.dispatcher().finished 的方法实现。

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
  /** Used by {@code AsyncCall#run} to signal completion. */
void finished(AsyncCall call) {
finished(runningAsyncCalls, call, true);
}

/** Used by {@code Call#execute} to signal completion. */
void finished(RealCall call) {
finished(runningSyncCalls, call, false);
}

// 最后调用这个
private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
int runningCallsCount;
Runnable idleCallback;
synchronized (this) {
if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
if (promoteCalls) promoteCalls();
runningCallsCount = runningCallsCount();
idleCallback = this.idleCallback;
}

if (runningCallsCount == 0 && idleCallback != null) {
idleCallback.run();
}
}

由于 promoteCalls 是true 我们看下 promoteCalls 的方法实现

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

private void promoteCalls() {
if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall call = i.next();

if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
runningAsyncCalls.add(call);
executorService().execute(call);
}

if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
}
}

根据代码可以明显看出 , 当一个请求结束了调用 finished 方法,最终到promoteCalls就是把 异步等待队列中的请求,取出放到 异步执行队列中。

  • 如果异步执行队列已经是满的状态就不加了,return
  • 如果 异步等待队列中 没有需要执行的网络请求 也就没有必要进行下一步了 return
  • 上面的两条都没遇到,遍历 异步等待队列,取出队首的请求,如果这个请求的 host 符合 (正在执行的网络请求中 同一个host最多只能是5个)的这个条件, 把 等待队列的这个请求移除, 加入到 正在执行的队列中, 线程开始执行。 如果不符合继续 遍历操作。

interceptors 拦截器

接着看 RealCall 的 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
27
28
29
30
31

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()));
//处理 缓存配置 根据条件(存在响应缓存并被设置为不变的或者响应在有效期内)返回缓存响应
//设置请求头(If-None-Match、If-Modified-Since等) 服务器可能返回304(未修改)
//可配置用户自己设置的缓存拦截器
interceptors.add(new CacheInterceptor(client.internalCache()));
//连接拦截器 这里才是真正的请求网络
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
//配置okhttpClient 时设置的networkInterceptors
//返回观察单个网络请求和响应的不可变拦截器列表。
interceptors.addAll(client.networkInterceptors());
}
//执行流操作(写出请求体、获得响应数据) 负责向服务器发送请求数据、从服务器读取响应数据
//进行http请求报文的封装与请求报文的解析
interceptors.add(new CallServerInterceptor(forWebSocket));
//创建责任链
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
//执行 责任链
return chain.proceed(originalRequest);
}

看下 RealInterceptorChain 的实现

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


public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
Connection connection) throws IOException {

if (index >= interceptors.size()) throw new AssertionError();

calls++;
//创建新的拦截链,链中的拦截器集合index+1
RealInterceptorChain next = new RealInterceptorChain(
interceptors, streamAllocation, httpCodec, connection, index + 1, request);
// 执行当前的拦截器
Interceptor interceptor = interceptors.get(index);
// 执行拦截器
Response response = interceptor.intercept(next);

if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
throw new IllegalStateException("network interceptor " + interceptor
+ " must call proceed() exactly once");
}

// Confirm that the intercepted response isn't null.
if (response == null) {
throw new NullPointerException("interceptor " + interceptor + " returned null");
}

return response;
}

根据上面的代码 我们可以看出,新建了一个RealInterceptorChain 责任链 并且 index+1,然后 执行interceptors.get(index); 返回Response。

责任链中每个拦截器都会执行chain.proceed()方法之前的代码,等责任链最后一个拦截器执行完毕后会返回最终的响应数据,而chain.proceed() 方法会得到最终的响应数据,这时就会执行每个拦截器的chain.proceed()方法之后的代码,其实就是对响应数据的一些操作。

接下来看下各个拦截器的具体代码

RetryAndFollowUpInterceptor

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
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();

streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(request.url()), callStackTrace);

int followUpCount = 0;
Response priorResponse = null;
while (true) {
if (canceled) {
streamAllocation.release();
throw new IOException("Canceled");
}

Response response = null;
boolean releaseConnection = true;
try {
response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
// The attempt to connect via a route failed. The request will not have been sent.
if (!recover(e.getLastConnectException(), false, request)) {
throw e.getLastConnectException();
}
//如果出现异常 不释放连接, 继续重试
releaseConnection = false;
continue;
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
//如果出现异常 不释放连接, 继续重试
if (!recover(e, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}

// Attach the prior response if it exists. Such responses never have a body.
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}

Request followUp = followUpRequest(response);

if (followUp == null) {
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}

closeQuietly(response.body());

//重试次数大于20次 ,不再试了,释放连接,
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}

if (followUp.body() instanceof UnrepeatableRequestBody) {
streamAllocation.release();
throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
}

if (!sameConnection(response, followUp.url())) {
streamAllocation.release();
streamAllocation = new StreamAllocation(
client.connectionPool(), createAddress(followUp.url()), callStackTrace);
} else if (streamAllocation.codec() != null) {
throw new IllegalStateException("Closing the body of " + response
+ " didn't close its backing stream. Bad interceptor?");
}

request = followUp;
priorResponse = response;
}
}

当发生 RouteException 和 IOException 都会进行 recover 重试。

BridgeInterceptor

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
@Override public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();

RequestBody body = userRequest.body();
if (body != null) {
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}

long contentLength = body.contentLength();
if (contentLength != -1) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
}

if (userRequest.header("Host") == null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}

if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}

// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
// the transfer stream.
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}

List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies));
}

if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent());
}

Response networkResponse = chain.proceed(requestBuilder.build());

HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);

if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
GzipSource responseBody = new GzipSource(networkResponse.body().source());
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
}

return responseBuilder.build();
}

能看出 BridgeInterceptor 主要做的就是
在请求发出之前 把请求的 信息拿出来处理成Request.Builder.header 发送出去
当请求结果回来之后,处理header 信息。处理返回的信息。

缓存拦截器

CacheInterceptor

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
 @Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;

long now = System.currentTimeMillis();

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
//如果networkRequest == null 则说明不使用网络请求
//获取缓存中(CacheStrategy)的Response
if (cache != null) {
cache.trackResponse(strategy);
}

//缓存无效 关闭资源
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}

// If we're forbidden from using the network and the cache is insufficient, fail.
//networkRequest == null 不使用网络请求 且没有缓存 cacheResponse == null 返回失败
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}

// If we don't need the network, we're done.
//如果无需网络请求, 把缓存中的结果取出来组装成返回体 返回
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());
}
}

//网络请求结果回来了,根据情况更新缓存结果
// If we have a cache response too, then we're doing a conditional get.
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();

// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}

Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();

if (HttpHeaders.hasBody(response)) {
CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
response = cacheWritingResponse(cacheRequest, response);
}

return response;
}

如果用户自己配置了缓存拦截器,cacheCandidate = cache.Response 获取用户自己存储的Response,否则 cacheCandidate = null;同时从CacheStrategy 获取cacheResponse 和 networkRequest

如果cacheCandidate != null 而 cacheResponse == null 说明缓存无效清除cacheCandidate缓存。

如果networkRequest == null 说明没有网络,cacheResponse == null 没有缓存,返回失败的信息,责任链此时也就终止,不会在往下继续执行。

如果networkRequest == null 说明没有网络,cacheResponse != null 有缓存,返回缓存的信息,责任链此时也就终止,不会在往下继续执行。

然后

执行下一个拦截器,也就是请求网络

责任链执行完毕后,会返回最终响应数据,如果缓存存在更新缓存,如果缓存不存在加入到缓存中去。

ConnectInterceptor

1
2
3
4
5
6
7
8
9
10
11
12
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();

// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();

return realChain.proceed(request, streamAllocation, httpCodec, connection);
}

连接复用的逻辑就是这里面, 寻找可用的链接, 复用, 这个待会分析。

networkInterceptors

这个是自定义的网络拦截器

CallServerInterceptor

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
@Override public Response intercept(Chain chain) throws IOException {
//HttpStream 就是先前在 ConnectInterceptor 创建出来的
HttpCodec httpCodec = ((RealInterceptorChain) chain).httpStream();
StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();
Request request = chain.request();
/发送请求的时间戳
long sentRequestMillis = System.currentTimeMillis();
//写入请求头信息
httpCodec.writeRequestHeaders(request);
//写入请求体信息(有请求体的情况)
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
}
//结束请求
httpCodec.finishRequest();
//读取响应头信息
Response response = httpCodec.readResponseHeaders()
.request(request)
//握手?
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
//openResponseBody 获取响应体信息
int code = response.code();
//app 不走这个
if (forWebSocket && code == 101) {
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}

if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();
}

if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}

return response;
}

OkhttpClient 实现了Call.Fctory,负责为Request 创建 Call;

RealCall 为Call的具体实现,其enqueue() 异步请求接口通过Dispatcher()调度器利用ExcutorService实现,而最终进行网络请求时和同步的execute()接口一致,都是通过 getResponseWithInterceptorChain() 函数实现

getResponseWithInterceptorChain() 中利用 Interceptor 链条,责任链模式 分层实现缓存、透明压缩、网络 IO 等功能;最终将响应数据返回给用户。

OkHttp 连接池复用

我们知道 OkHttp 支持5个并发 socket 连接,默认keepAlive 时间为5分钟。 那究竟是怎么做到的呢

在 ConnectInterceptor 中我们知道 newStream

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
public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
...

try {
找“健康的”RealConnection
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);

HttpCodec resultCodec;
if (resultConnection.http2Connection != null) {
resultCodec = new Http2Codec(client, this, resultConnection.http2Connection);
} else {
// 通过RealConnection创建HttpCodec
resultConnection.socket().setSoTimeout(readTimeout);
resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);
resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);
resultCodec = new Http1Codec(
client, this, resultConnection.source, resultConnection.sink);
}

synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}


private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
throws IOException {
while (true) {
// while循环直到找到return
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
connectionRetryEnabled);

...

return candidate;
}
}




private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
boolean connectionRetryEnabled) throws IOException {
Route selectedRoute;
synchronized (connectionPool) {
...
// 如果为空,尝试从连接池中获取,这个方法的关键点,如果获取到connection不为空(第三个参数为this,找到合适的RealConnection赋值到connection

// Attempt to get a connection from the pool.
RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this);
if (pooledConnection != null) {
this.connection = pooledConnection;
return pooledConnection;
}

selectedRoute = route;
}


if (selectedRoute == null) {
selectedRoute = routeSelector.next();
synchronized (connectionPool) {
route = selectedRoute;
refusedStreamCount = 0;
}
}
// 如果没有找到,则创建一个
RealConnection newConnection = new RealConnection(selectedRoute);

synchronized (connectionPool) {
acquire(newConnection);
Internal.instance.put(connectionPool, newConnection);
this.connection = newConnection;
if (canceled) throw new IOException("Canceled");
}
// 进行实际的的网络连接
newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.connectionSpecs(),
connectionRetryEnabled);
routeDatabase().connected(newConnection.route());

return newConnection;
}

从上面的分析,获取RealConnection的流程,总结如下:

在ConnectInterceptor中获取StreamAllocation的引用,通过StreamAllocation去寻找RealConnection

如果RealConnection不为空,那么直接返回。否则去连接池中寻找并返回,如果找不到直接创建并设置到连接池中,然后再进一步判断是否重复释放到Socket。

在实际网络连接connect中,选择不同的链接方式(有隧道链接(Tunnel)和管道链接(Socket))
把RealConnection和HttpCodec传递给下一个拦截器

在从连接池中获取一个连接的时候,使用了 Internal 的 get() 方法。Internal 有一个静态的实例,会在 OkHttpClient 的静态代码快中被初始化。我们会在 Internal 的 get() 中调用连接池的 get() 方法来得到一个连接。并且,从中我们明白了连接复用的一个好处就是省去了进行 TCP 和 TLS 握手的一个过程。因为建立连接本身也是需要消耗一些时间的,连接被复用之后可以提升我们网络访问的效率。

ConnectionPool

连接池的位于 ConnectionPool 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** 空闲 socket 最大连接数 */
private final int maxIdleConnections;
socket 的 keepAlive 时间
private final long keepAliveDurationNs;
private final Deque<RealConnection> connections = new ArrayDeque<>();
final RouteDatabase routeDatabase = new RouteDatabase();
boolean cleanupRunning;


public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);
}

public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
this.maxIdleConnections = maxIdleConnections;
this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);

// Put a floor on the keep alive duration, otherwise cleanup will spin loop.
if (keepAliveDuration <= 0) {
throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
}
}

构造方法可以看到,空闲socket的最大连接数为5个,ConnectionPool是在 OkHttpClient 实例化时创建的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
RealConnection get(Address address, StreamAllocation streamAllocation) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.allocations.size() < connection.allocationLimit
&& address.equals(connection.route().address)
&& !connection.noNewStreams) {
streamAllocation.acquire(connection);
return connection;
}
}
return null;
}

void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
添加到 Deque 之前需要清理空闲的线程,
connections.add(connection);
}

看下 put,get 方法,get 方法会遍历 connection 缓存列表, 当某个连接计数小于限制的大小,并且 request 的地址和缓存列表中此链接的地址完全匹配时, 则直接复用缓存列表中的 connection 作为request 的连接。

上面可以看到 put 方法会调用清理线程。

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
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
long waitMillis = waitNanos / 1000000L;
waitNanos -= (waitMillis * 1000000L);
synchronized (ConnectionPool.this) {
try {
ConnectionPool.this.wait(waitMillis, (int) waitNanos);
} catch (InterruptedException ignored) {
}
}
}
}
}
};
线程会不停的调用cleanup方法进行清理, 并返回下次需要清理的间隔时间, 然后调用 wait方法进行等待,当时间到了之后再次进行清理。 一直这样下去。

会调用 cleanup方法,下面是cleanup方法


long cleanup(long now) {
int inUseConnectionCount = 0;
int idleConnectionCount = 0;
RealConnection longestIdleConnection = null;
long longestIdleDurationNs = Long.MIN_VALUE;

// Find either a connection to evict, or the time that the next eviction is due.
synchronized (this) {
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();

// If the connection is in use, keep searching.
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++;
continue;
}

idleConnectionCount++;

// If the connection is ready to be evicted, we're done.
long idleDurationNs = now - connection.idleAtNanos;
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}

if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it below (outside
// of the synchronized block).
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// A connection will be ready to evict soon.
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// All connections are in use. It'll be at least the keep alive duration 'til we run again.
return keepAliveDurationNs;
} else {
// No connections, idle or in use.
cleanupRunning = false;
return -1;
}
}

closeQuietly(longestIdleConnection.socket());

// Cleanup again immediately.
return 0;
}

cleanup方法的过程是 根据连接中的引用计数来计算空闲连接数和活跃连接数,,然后标记出空闲连接数。
如果空闲连接keepAlive 时间超过5分钟,或者空闲连接数超过5个,则从Deque 中移除次连接,
如果空闲连接数大于0,则返回此连接即将到期的时间,如果都是活跃连接,并大于0,则返回5分钟。 如果没有任何连接,则返回-1,

清除算法,使用类似GC中的引用计算算法,如果弱引用StreamAllocation列表为0,则表示空闲需要进行回收。

可以看出连接池复用的核心就是用 Deque 来存储连接, 通过 put,get 等来对 Deque 进行操作, 另外通过判断连接中的技术对象 StreamAllocation 来进行自动回收连接。

OkHttp 的优缺点

优点:

  • 1、支持 HTTP/2,允许连接同一主机的所有请求分享一个 socket。 如果 HTTP/2 不可用,会使用连接池减少请求延迟。
  • 2、使用GZIP压缩下载内容,且压缩操作对用户是透明的。
  • 3、利用响应缓存来避免重复的网络请求。
  • 4、如果你的服务端有多个IP地址,当第一个地址连接失败时,OKHttp会尝试连接其他的地址,这对IPV4和IPV6以及寄宿在多个数据中心的服务而言,是非常有必要的。
  • 5、用户可自主定制拦截器,实现自己想要的网络拦截。
  • 6、支持大文件的上传和下载。
  • 7、支持cookie持久化。
  • 8、支持自签名的https链接,配置有效证书即可。
  • 9、支持Headers的缓存策略减少重复的网络请求。

缺点:

  • 1、网络请求的回调是子线程,需要用户手动操作发送到主线程。
  • 2、参数较多,配置起来复杂。

所以综合上面的缺点,OkHttpUtils 及类似的 封装应用而生。下一篇我们来通过 OkHttpUtils源码解析 看下是如何封装并解决这些问题的。

参考来源

]]>
<p>接上篇的 <a href="https://youngerdev.com/Volley-源码解析.html">Volley 源码</a>解析,目前项目中更多的用到的是 <a href="https://github.com/hongyangAndroid/okhttputi
Volley 源码解析 http://youngerdev.com/Volley-源码解析.html 2019-10-07T04:30:44.000Z 2019-10-08T00:39:29.595Z Volley 源码解析

开始之前

目前已经有很多文章写volley源码解析的了,

为什么这么多呢? 可能是因为volley 的源码相对来说比较少, 逻辑相对简单,好读。

为什么我还要写volley源码解析呢? 和上面的问题的原因一样, 还有就是我也读了好几遍volley的源码了,每次读完后过段时间就会忘记一些,应了那句老话,“好记性不如烂笔头”,现在也不用纸笔了,直接电脑上敲出来,更方便。 我不一定写的比其他同学的高明,但是我会尽量写出我的理解,和现实工作内容联系起来书写。

注意本文是基于volley最新版本 1.1版本源码

本文的目录结构是:

  • volley 的基本使用
  • volley 源码解析
  • volley 的优缺点
  • volley 的扩展

volley 基本使用

Volley 是 Google 推出的 Android 异步网络请求框架和图片加载框架。在 Google I/O 2013 大会上发布。

特别适合数据量小,通信频繁的网络操作

1
2
3
4
5
在项目的gradle 文件中添加依赖
dependencies {
...
implementation 'com.android.volley:volley:1.1.1'
}

使用起来很简单,举一个StringRequest例子,其他的类似

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

//构建一个请求队列
RequestQueue queue = Volley.newRequestQueue(this);
String url ="http://www.baidu.com";
//创建一个request
StringRequest stringRequest = new StringRequest(com.android.volley.Request.Method.GET,url, new com.android.volley.Response.Listener<String>() {
@Override
public void onResponse(String response) {

}
}, new com.android.volley.Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {

}
});

//把request 添加到 队列中
queue.add(stringRequest);

volley源码架构图

volley 源码解析

首先我们看下请求队列的创建。

Volley

1
RequestQueue queue = Volley.newRequestQueue(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

public static RequestQueue newRequestQueue(Context context) {
return newRequestQueue(context, (BaseHttpStack) null);
}

重载 newRequestQueue 方法

public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) {
BasicNetwork network;

if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
network = new BasicNetwork(new HurlStack());
} else {
String userAgent = "volley/0";
try {
String packageName = context.getPackageName();
PackageInfo info =
context.getPackageManager().getPackageInfo(packageName, /* flags= */ 0);
userAgent = packageName + "/" + info.versionCode;
} catch (NameNotFoundException e) {
}

network =
new BasicNetwork(
new HttpClientStack(AndroidHttpClient.newInstance(userAgent)));
}
} else {
network = new BasicNetwork(stack);
}

return newRequestQueue(context, network);
}



最终调用这个方法start
private static RequestQueue newRequestQueue(Context context, Network network) {
//创建缓存目录
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
//创建任务队列
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
//启动队列
queue.start();
return queue;
}

先判断是否有指定 stack 也就是实际请求的方式,如果有传进来,就用传的来指定的方式,
如果为空, 就按照sdk 版本创建相应的请求方式, sdk版本<9的使用 httpClient 请求

大于等于9的使用 httpUrlconnection 来请求,这是因为,httpUrlconnection在Android 2.2之前有bug。但是httpClient 没有 httpUrlconnection 性能好, Api简单,体积较小,压缩和缓存机制也可有效减少网络访问的流量, 而且httpclient 请求的方式 在Android6.0之后也直接从sdk 中直接去掉了。

RequestQueue

1
2
3
4
5
6
7
public RequestQueue(Cache cache, Network network, int threadPoolSize) {
this(
cache,
network,
threadPoolSize,
new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}

可以重点关注 ExecutorDelivery(new Handler(Looper.getMainLooper())))
接下来在线程切换地方法讲到它,这个是成功把子线程数据发送到主线程的关键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
任务队列的启动方法
/** Starts the dispatchers in this queue. */
public void start() {
stop(); // Make sure any currently running dispatchers are stopped.
// Create the cache dispatcher and start it.
//创建了一个缓存调度线程 并启动
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
mCacheDispatcher.start();

// Create network dispatchers (and corresponding threads) up to the pool size.
//创建了4个网络调度线程,并启动
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher =
new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}

任务队列创建完成,等到request 加入,request的创建比较简单,可以根据自己的需求创建相应的request ,系统提供了 String ,Image, JsonObject ,jsonArray 等request,他们的区别主要是在 parseNetworkResponse 方法中根据不同的数据类型,进行相应数据类型的解析。

再看网络请求的 add 方法,就是把request 添加到 任务队列中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public <T> Request<T> add(Request<T> request) {
// Tag the request as belonging to this queue and add it to the set of current requests.
request.setRequestQueue(this);
synchronized (mCurrentRequests) {
mCurrentRequests.add(request);
}

// Process requests in the order they are added.
request.setSequence(getSequenceNumber());
request.addMarker("add-to-queue");

// 判断这个 request 是否支持缓存, 如果不支持就直接 把这个request加入到网络调度线程中
//否则就加入到缓存 调度线程。
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
mCacheQueue.add(request);
return request;
}

这个 mNetworkQueue 和 mCacheQueue 都是 PriorityBlockingQueue 优先级阻塞队列 现在根据情况分别加入到相应的调度线程等待执行

CacheDispatcher

上面已经把request加入到缓存队列中了,接下来看下 CacheDispatcher 是如何处理这些request的。

1
2
3
4

public class CacheDispatcher extends Thread {
...
}

能看到这个 CacheDispatcher 继承 Thread ,前面我们也看到start 方法中已经 调用了线程的 start() ,那我们看下run方法

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

@Override
public void run() {
if (DEBUG) VolleyLog.v("start new dispatcher");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

// Make a blocking call to initialize the cache.
mCache.initialize();

while (true) {
try {
processRequest();
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
Thread.currentThread().interrupt();
return;
}
VolleyLog.e(
"Ignoring spurious interrupt of CacheDispatcher thread; "
+ "use quit() to terminate it");
}
}
}

能看到代码相当简洁, 就是一个 while (true) 死循环, 里面执行 processRequest方法,

1
2
3
4
5
6
private void processRequest() throws InterruptedException {
// Get a request from the cache triage queue, blocking until
// at least one is available.
final Request<?> request = mCacheQueue.take();
processRequest(request);
}

英文注释写的也特别清楚,就是从 BlockingQueue阻塞队列中取出,如果有数据就处理,没有数据就阻塞在这。

BlockingQueue

如果BlockQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态,直到BlockingQueue进了东西才会被唤醒。

同样,如果BlockingQueue是满的,任何试图往里存东西的操作也会被阻断进入等待状态,直到BlockingQueue里有空间才会被唤醒继续操作。

了解了 BlockingQueue 我们看下真正处理request的详细方法

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

void processRequest(final Request<?> request) throws InterruptedException {
request.addMarker("cache-queue-take");

// 先判断这个request是否已经被取消,需求就不继续了,直接调用finish 方法
if (request.isCanceled()) {
request.finish("cache-discard-canceled");
return;
}

// 尝试从缓存中查找是否有同样的request在
Cache.Entry entry = mCache.get(request.getCacheKey());
if (entry == null) {
request.addMarker("cache-miss");
// 如果没有缓存,就加入到网络请求的队列
if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
mNetworkQueue.put(request);
}
return;
}

//如果缓存已经过期,也加入到网络请求的队列
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
request.setCacheEntry(entry);
if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
mNetworkQueue.put(request);
}
return;
}

// 在缓存中找到了这个请求,且没有过期,把 服务端返回的数据解析后取出,传回。

request.addMarker("cache-hit");
Response<?> response =
request.parseNetworkResponse(
new NetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");

if (!entry.refreshNeeded()) {
// 直接发送
mDelivery.postResponse(request, response);
} else {
// 发送缓存的同时 也把request加入到网络请求线程,刷新数据
request.addMarker("cache-hit-refresh-needed");
request.setCacheEntry(entry);
// Mark the response as intermediate.
response.intermediate = true;

if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
// Post the intermediate response back to the user and have
// the delivery then forward the request along to the network.
mDelivery.postResponse(
request,
response,
new Runnable() {
@Override
public void run() {
try {
mNetworkQueue.put(request);
} catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
}
});
} else {
// request has been added to list of waiting requests
// to receive the network response from the first request once it returns.
mDelivery.postResponse(request, response);
}
}
}

NetworkDispatcher

看完了 缓存队列的执行方式, 再看下网络请求线程的执行,跟上面的方法方式类似,我们直奔processRequest看它是如何实现的。

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
void processRequest(Request<?> request) {
long startTimeMs = SystemClock.elapsedRealtime();
try {
request.addMarker("network-queue-take");

// 是否取消,取消的话就结束

if (request.isCanceled()) {
request.finish("network-discard-cancelled");
request.notifyListenerResponseNotUsable();
return;
}

addTrafficStatsTag(request);

// 进行网络请求, performRequest 这个方法中是具体的网络请求的内容
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");


// 如果返回304 同时已经回传过了返回值,就不再回传内容了,结束
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
request.notifyListenerResponseNotUsable();
return;
}

// 在工作线程中解析返回内容
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");


//如果需要缓存, 就把内容放到缓存中
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}

// 把解析出的内容回传。
request.markDelivered();
mDelivery.postResponse(request, response);
request.notifyListenerResponseReceived(response);
} catch (VolleyError volleyError) {
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
parseAndDeliverNetworkError(request, volleyError);
request.notifyListenerResponseNotUsable();
} catch (Exception e) {
VolleyLog.e(e, "Unhandled exception %s", e.toString());
VolleyError volleyError = new VolleyError(e);
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
mDelivery.postError(request, volleyError);
request.notifyListenerResponseNotUsable();
}
}

具体执行网络请求的是 BasicNetwork 的 performRequest 方法

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

public NetworkResponse performRequest(Request<?> request) throws VolleyError {
long requestStart = SystemClock.elapsedRealtime();
while (true) {
HttpResponse httpResponse = null;
byte[] responseContents = null;
List<Header> responseHeaders = Collections.emptyList();
try {
// 把所有的header 信息拿到
Map<String, String> additionalRequestHeaders =
getCacheHeaders(request.getCacheEntry());
// 具体的根据不同的情况,看Android SDK 版本号 是否小于9 来选择使用
// httpClient 还是httpUrlconnection 进行创建网络链接
httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders);
int statusCode = httpResponse.getStatusCode();

responseHeaders = httpResponse.getHeaders();
// 处理304问题
if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
Entry entry = request.getCacheEntry();
if (entry == null) {
return new NetworkResponse(
HttpURLConnection.HTTP_NOT_MODIFIED,
/* data= */ null,
/* notModified= */ true,
SystemClock.elapsedRealtime() - requestStart,
responseHeaders);
}
// Combine cached and response headers so the response will be complete.
List<Header> combinedHeaders = combineHeaders(responseHeaders, entry);
return new NetworkResponse(
HttpURLConnection.HTTP_NOT_MODIFIED,
entry.data,
/* notModified= */ true,
SystemClock.elapsedRealtime() - requestStart,
combinedHeaders);
}

// 处理异常情况
InputStream inputStream = httpResponse.getContent();
if (inputStream != null) {
responseContents =
inputStreamToBytes(inputStream, httpResponse.getContentLength());
} else {
// Add 0 byte response as a way of honestly representing a
// no-content request.
responseContents = new byte[0];
}

// if the request is slow, log it.
long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
logSlowRequests(requestLifetime, request, responseContents, statusCode);

if (statusCode < 200 || statusCode > 299) {
throw new IOException();
}
//最后new 了一个NetworkResponse 把返回的内容包装后返回前一个方法,继续处理
return new NetworkResponse(
statusCode,
responseContents,
/* notModified= */ false,
SystemClock.elapsedRealtime() - requestStart,
responseHeaders);
} catch (SocketTimeoutException e) {
//重试机制
attemptRetryOnException("socket", request, new TimeoutError());
} catch (MalformedURLException e) {
throw new RuntimeException("Bad URL " + request.getUrl(), e);
} catch (IOException e) {
int statusCode;
if (httpResponse != null) {
statusCode = httpResponse.getStatusCode();
} else {
throw new NoConnectionError(e);
}
VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
NetworkResponse networkResponse;
if (responseContents != null) {
networkResponse =
new NetworkResponse(
statusCode,
responseContents,
/* notModified= */ false,
SystemClock.elapsedRealtime() - requestStart,
responseHeaders);
if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED
|| statusCode == HttpURLConnection.HTTP_FORBIDDEN) {
//重试机制
attemptRetryOnException(
"auth", request, new AuthFailureError(networkResponse));
} else if (statusCode >= 400 && statusCode <= 499) {
// Don't retry other client errors.
throw new ClientError(networkResponse);
} else if (statusCode >= 500 && statusCode <= 599) {
if (request.shouldRetryServerErrors()) {
attemptRetryOnException(
"server", request, new ServerError(networkResponse));
} else {
throw new ServerError(networkResponse);
}
} else {
// 3xx? No reason to retry.
throw new ServerError(networkResponse);
}
} else {
//重试机制
attemptRetryOnException("network", request, new NetworkError());
}
}
}
}

HurlStack

我们可以再看下建立网络链接的部分,这个是httpUrlcinnection,

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

@Override
public HttpResponse executeRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
String url = request.getUrl();
HashMap<String, String> map = new HashMap<>();
map.putAll(additionalHeaders);
// Request.getHeaders() takes precedence over the given additional (cache) headers).
map.putAll(request.getHeaders());
if (mUrlRewriter != null) {
String rewritten = mUrlRewriter.rewriteUrl(url);
if (rewritten == null) {
throw new IOException("URL blocked by rewriter: " + url);
}
url = rewritten;
}
URL parsedUrl = new URL(url);
HttpURLConnection connection = openConnection(parsedUrl, request);
boolean keepConnectionOpen = false;
try {
for (String headerName : map.keySet()) {
connection.setRequestProperty(headerName, map.get(headerName));
}
setConnectionParametersForRequest(connection, request);
// Initialize HttpResponse with data from the HttpURLConnection.
int responseCode = connection.getResponseCode();
if (responseCode == -1) {
// -1 is returned by getResponseCode() if the response code could not be retrieved.
// Signal to the caller that something was wrong with the connection.
throw new IOException("Could not retrieve response code from HttpUrlConnection.");
}

if (!hasResponseBody(request.getMethod(), responseCode)) {
return new HttpResponse(responseCode, convertHeaders(connection.getHeaderFields()));
}

// Need to keep the connection open until the stream is consumed by the caller. Wrap the
// stream such that close() will disconnect the connection.
keepConnectionOpen = true;
return new HttpResponse(
responseCode,
convertHeaders(connection.getHeaderFields()),
connection.getContentLength(),
new UrlConnectionInputStream(connection));
} finally {
if (!keepConnectionOpen) {
connection.disconnect();
}
}
}

总结上面这个方法就是 把所有的请求头信息发送出去,如果有请求体,把请求体也发送过去, 等待服务端相应,响应后把返回值回传到上一个方法中处理。

volley 是如何实现线程切换的

  1. 主线程上开启子线程执行网络请求在上面分析中能看到。 创建了1个缓存线程,4个网络请求线程,执行相应的网络请求。
  2. 子线程在处理完耗时操作,处理完数据,怎么发送到主线程的呢?
ExecutorDelivery

接下来看下我们上面多次看到的 mDelivery.postResponse(request, response); 是怎么处理的

先看下构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    public ExecutorDelivery(final Handler handler) {
//注意这个 Handler 这是RequestQueue 构造方法中初始化的,传入的是 Looper.getMainLooper(), 它就是主线程的handler
//创建了一个线程池,它的作用就是在线程中调用 handler,发送 runnable,通过这种形式使得 runnable 在 UI 线程中执行。
mResponsePoster =
new Executor() {
@Override
public void execute(Runnable command) {
handler.post(command);
}
};
}


//看下具体方法
@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
request.markDelivered();
request.addMarker("post-response");
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}

通过这种方式,mResponsePoster.execute 是得里面的runnable能够在主线程中得到执行,

ResponseDeliveryRunnable
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
    // If this request has canceled, finish it and don't deliver.
if (mRequest.isCanceled()) {
mRequest.finish("canceled-at-delivery");
return;
}

// Deliver a normal response or error, depending.
if (mResponse.isSuccess()) {
mRequest.deliverResponse(mResponse.result);
} else {
mRequest.deliverError(mResponse.error);
}

// If this is an intermediate response, add a marker, otherwise we're done
// and the request can be finished.
if (mResponse.intermediate) {
mRequest.addMarker("intermediate-response");
} else {
mRequest.finish("done");
}

// 发送之后,都会调用 Runnable 的 run() ,这个已经可执行的 Runnable 已经切换到主线程了。
if (mRunnable != null) {
mRunnable.run();
}
}

最后调用 request 的 finish 方法,表示该请求已经执行结束了,同时,如果 ResponseDeliveryRunnable 的构造方法中的第三个参数 runnable 不为空,立即执行该 runnable 的 run 方法。

再看这个Stringrequest中的

1
2
3
4
5
6
7
8
9
10
11

@Override
protected void deliverResponse(String response) {
Response.Listener<String> listener;
synchronized (mLock) {
listener = mListener;
}
if (listener != null) {
listener.onResponse(response);
}
}

在主线程 通过接口回调,回调到发起请求的位置,进行相应的处理。

volley 的优缺点

优点:

  • 特别适合数据量小,通信频繁的网络操作
  • 轻量,jar包相对较小
  • 扩展性强。Volley 中大多是基于接口的设计,可配置性强。
  • 一定程度符合 Http 规范,包括返回 ResponseCode(2xx、3xx、4xx、5xx)的处理,请求头的处理,缓存机制的支持等。并支持重试及优先级定义。
  • 默认 Android2.3 及以上基于 HttpURLConnection,2.3 以下基于 HttpClient 实现,
  • 提供简便的图片加载工具。
  • 网络请求线程NetworkDispatcher默认开启了4个,可以优化,通过手机CPU数量

缺点:

  • 在BasicNetwork中判断了statusCode(statusCode < 200 || statusCode > 299),如何符合条件直接抛出IOException(),不够合理。
  • 图片加载性能一般
  • 导致401等其他状态抛出IOException
  • 对大文件下载 Volley的表现非常糟糕
  • 使用的是httpclient,HttpURLConnection。不过在android 6.0不支持httpclient了,如果想支持得添加org.apache.http.legacy.jar

为什么volley 不适合下载上传大文件?为什么适合数据量小的频率高的请求?

  1. Volley的网络请求线程池默认大小为4。意味着可以并发进行4个请求,大于4个,会排在队列中。
  2. Volley将整个response加载到内存并进行操作(可以是解析等操作)大文件可能会引起OOM

volley 的扩展

有时候我们整个项目在使用的volley 如果要替换成okhttp 的成本就比较高了, 那能不能方便简单的时候okhttp 的优秀的功能呢, 答案当然是可以的。

还记得这个吗? 最开始分析源码时,

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

public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) {
BasicNetwork network;

if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
network = new BasicNetwork(new HurlStack());
} else {
// Prior to Gingerbread, HttpUrlConnection was unreliable.
// See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
// At some point in the future we'll move our minSdkVersion past Froyo and can
// delete this fallback (along with all Apache HTTP code).
String userAgent = "volley/0";
try {
String packageName = context.getPackageName();
PackageInfo info =
context.getPackageManager().getPackageInfo(packageName, /* flags= */ 0);
userAgent = packageName + "/" + info.versionCode;
} catch (NameNotFoundException e) {
}

network =
new BasicNetwork(
new HttpClientStack(AndroidHttpClient.newInstance(userAgent)));
}
} else {
network = new BasicNetwork(stack);
}

return newRequestQueue(context, network);
}

如果我们传stack 进来,这个 stack 就是null 了,所以,我们只需要实现这个HttpStack 然后传进来就行了。

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
137
138
139
140
141
142
143
144
145


public class OkHttpStack implements HttpStack {
private final OkHttpClient mClient;
//提供一个构造函数,传入OkHttpClient 对象
public OkHttpStack(OkHttpClient client) {
this.mClient = client;
}

private static HttpEntity entityFromOkHttpResponse(Response response) throws IOException {
BasicHttpEntity entity = new BasicHttpEntity();
ResponseBody body = response.body();

entity.setContent(body.byteStream());
entity.setContentLength(body.contentLength());
entity.setContentEncoding(response.header("Content-Encoding"));

if (body.contentType() != null) {
entity.setContentType(body.contentType().type());
}
return entity;
}

//请求方法和请求体
@SuppressWarnings("deprecation")
private static void setConnectionParametersForRequest
(okhttp3.Request.Builder builder, Request<?> request)
throws IOException, AuthFailureError {
switch (request.getMethod()) {
case Request.Method.DEPRECATED_GET_OR_POST:
byte[] postBody = request.getPostBody();
if (postBody != null) {
builder.post(RequestBody.create
(MediaType.parse(request.getPostBodyContentType()), postBody));
}
break;

case Request.Method.GET:
builder.get();
break;

case Request.Method.DELETE:
builder.delete();
break;

case Request.Method.POST:
builder.post(createRequestBody(request));
break;

case Request.Method.PUT:
builder.put(createRequestBody(request));
break;

case Request.Method.HEAD:
builder.head();
break;

case Request.Method.OPTIONS:
builder.method("OPTIONS", null);
break;

case Request.Method.TRACE:
builder.method("TRACE", null);
break;

case Request.Method.PATCH:
builder.patch(createRequestBody(request));
break;

default:
throw new IllegalStateException("Unknown method type.");
}
}

private static RequestBody createRequestBody(Request request) throws AuthFailureError {
final byte[] body = request.getBody();
if (body == null) return null;

return RequestBody.create(MediaType.parse(request.getBodyContentType()), body);
}

private static ProtocolVersion parseProtocol(final Protocol protocol) {
switch (protocol) {
case HTTP_1_0:
return new ProtocolVersion("HTTP", 1, 0);
case HTTP_1_1:
return new ProtocolVersion("HTTP", 1, 1);
case SPDY_3:
return new ProtocolVersion("SPDY", 3, 1);
case HTTP_2:
return new ProtocolVersion("HTTP", 2, 0);
}

throw new IllegalAccessError("Unkwown protocol");
}
//performRequest方法,首先我们设置一下超时时间
@Override
public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
throws IOException, AuthFailureError {
int timeoutMs = request.getTimeoutMs();
OkHttpClient client = mClient.newBuilder()
.readTimeout(timeoutMs, TimeUnit.MILLISECONDS)
.connectTimeout(timeoutMs, TimeUnit.MILLISECONDS)
.writeTimeout(timeoutMs, TimeUnit.MILLISECONDS)
.build();

okhttp3.Request.Builder okHttpRequestBuilder = new okhttp3.Request.Builder();
//设置请求头,请求头的来源有两个
Map<String, String> headers = request.getHeaders();
for (final String name : headers.keySet()) {
okHttpRequestBuilder.addHeader(name, headers.get(name));
}

for (final String name : additionalHeaders.keySet()) {
okHttpRequestBuilder.addHeader(name, additionalHeaders.get(name));
}

setConnectionParametersForRequest(okHttpRequestBuilder, request);

okhttp3.Request okhttp3Request = okHttpRequestBuilder.url(request.getUrl()).build();
//开始请求
Response okHttpResponse = client.newCall(okhttp3Request).execute();

StatusLine responseStatus = new BasicStatusLine
(
parseProtocol(okHttpResponse.protocol()),
okHttpResponse.code(),
okHttpResponse.message()
);

BasicHttpResponse response = new BasicHttpResponse(responseStatus);
response.setEntity(entityFromOkHttpResponse(okHttpResponse));

Headers responseHeaders = okHttpResponse.headers();
for (int i = 0, len = responseHeaders.size(); i < len; i++) {
final String name = responseHeaders.name(i), value = responseHeaders.value(i);
if (name != null) {
response.addHeader(new BasicHeader(name, value));
}
}
//最后返回HttpResponse对象
return response;
}

}

总结

总体来看,volley 源码还是比较简单的,结构清晰,代码量少,对于想读源码的同学还是一个比较不错的选择。有兴趣的可以尝试下。 也希望看这篇文章的同学能从本文中受益,也欢迎与我交流学习。

下一篇是分析 okHttp 的源码,敬请期待。

]]>
<h2 id="Volley-源码解析"><a href="#Volley-源码解析" class="headerlink" title="Volley 源码解析"></a>Volley 源码解析</h2><h3 id="开始之前"><a href="#开始之前" class="
网络基础知识 http://youngerdev.com/网络基础知识.html 2019-10-03T07:45:34.000Z 2019-10-08T00:33:18.705Z 计划写两篇关于 解析 Volley源码 和 Okhttp源码的文章 先了解下基础的网络知识。

目前存在的两种网络分层模型:OSI模型和TCP/IP模型。OSI模型一共分为七层,TCP/IP模型和OSI模型类似,但是只分为四层。

OSI模型

  • 应用层(Application)
  • 表示层(Presentation)
  • 会话层(Session)
  • 传输层(Transport)
  • 网络层(Network)
  • 数据链路层(Data Link)
  • 物理层(Physical)

TCP/IP模型

TCP/IP模型分为四层:应用层(Application)、传输层(Host-to-Host Transport)、互联网层(Internet)、网络接口层(Network Interface)。

在TCP/IP模型中并不包含物理层。另外,两个重要的协议ARP(Address Resolution Protocol,地址解析协议)和RARP(Reverse Address Resolution Protocol,反向地址转换协议),在OSI模型中一般被认为是在位于第二层数据链路层和第三层网络层之间,而在TCP/IP模型中则位于网络接口层。

  • 应用层:TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet 等等

  • 传输层:TCP,UDP

  • 网络层:IP,ICMP,OSPF,EIGRP,IGMP

  • 数据链路层:SLIP,CSLIP,PPP,MTU

每一抽象层建立在低一层提供的服务上,并且为高一层提供服务

TCP 三次握手与四次挥手

TCP (Transmission Control Protocol) 传输控制协议

  • TCP 提供一种面向连接的、可靠的字节流服务
  • 在一个 TCP 连接中,仅有两方进行彼此通信。广播和多播不能用于 TCP
  • TCP 使用校验,确认和重传机制来保证可靠传输
  • TCP 给数据分节进行排序,并使用累积确认保证数据的顺序不变和非重复
  • TCP 使用滑动窗口机制来实现流量控制,通过动态改变窗口的大小进行拥塞控制

注意:TCP 并不能保证数据一定会被对方接收到,因为这是不可能的。TCP 能够做到的是,如果有可能,就把数据递送到接收方,否则就(通过放弃重传并且中断连接这一手段)通知用户。因此准确说 TCP 也不是 100% 可靠的协议,它所能提供的是数据的可靠递送或故障的可靠通知。

三次握手

所谓三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送3个包。

三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect() 时。将触发三次握手。

  • 第一次握手(SYN=1, seq=x):

客户端发送一个 TCP 的 SYN 标志位置1的包,指明客户端打算连接的服务器的端口,以及初始序号 X,保存在包头的序列号(Sequence Number)字段里。

发送完毕后,客户端进入 SYN_SEND 状态。

  • 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1):

服务器发回确认包(ACK)应答。即 SYN 标志位和 ACK 标志位均为1。服务器端选择自己 ISN 序列号,放到 Seq 域里,同时将确认序号(Acknowledgement Number)设置为客户的 ISN 加1,即X+1。 发送完毕后,服务器端进入 SYN_RCVD 状态。

  • 第三次握手(ACK=1,ACKnum=y+1)

客户端再次发送确认包(ACK),SYN 标志位为0,ACK 标志位为1,并且把服务器发来 ACK 的序号字段+1,放在确定字段中发送给对方,并且在数据段放写ISN的+1

发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手结束。

四次挥手

客户端或服务器均可主动发起挥手动作,在 socket 编程中,任何一方执行 close() 操作即可产生挥手操作。

  • 第一次挥手(FIN=1,seq=x)

假设客户端想要关闭连接,客户端发送一个 FIN 标志位置为1的包,表示自己已经没有数据可以发送了,但是仍然可以接受数据。

发送完毕后,客户端进入 FIN_WAIT_1 状态。

  • 第二次挥手(ACK=1,ACKnum=x+1)

服务器端确认客户端的 FIN 包,发送一个确认包,表明自己接受到了客户端关闭连接的请求,但还没有准备好关闭连接。

发送完毕后,服务器端进入 CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态,等待服务器端关闭连接。

  • 第三次挥手(FIN=1,seq=y)

服务器端准备好关闭连接时,向客户端发送结束连接请求,FIN 置为1。

发送完毕后,服务器端进入 LAST_ACK 状态,等待来自客户端的最后一个ACK。

  • 第四次挥手(ACK=1,ACKnum=y+1)

客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待可能出现的要求重传的 ACK 包。

服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态。

客户端等待了某个固定时间(两个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。

UDP (User Datagram Protoco)用户数据报协议

UDP 是一个简单的传输层协议。和 TCP 相比,UDP 有下面几个显著特性:

  • UDP 缺乏可靠性。UDP 本身不提供确认,序列号,超时重传等机制。UDP 数据报可能在网络中被复制,被重新排序。即 UDP 不保证数据报会到达其最终目的地,也不保证各个数据报的先后顺序,也不保证每个数据报只到达一次
  • UDP 数据报是有长度的。每个 UDP 数据报都有长度,如果一个数据报正确地到达目的地,那么该数据报的长度将随数据一起传递给接收方。而 TCP 是一个字节流协议,没有任何(协议上的)记录边界。
  • UDP 是无连接的。UDP 客户和服务器之前不必存在长期的关系。UDP 发送数据报之前也不需要经过握手创建连接的过程。
  • UDP 支持多播和广播。

Socket

Socket 是对 TCP/IP 协议族的一种封装,是应用层与TCP/IP协议族通信的中间软件抽象层。从设计模式的角度看来,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

socket起源于UNIX,在Unix一切皆文件哲学的思想下,socket是一种”打开—读/写—关闭”模式的实现,服务器和客户端各自维护一个”文件”,在建立连接打开后,可以向自己文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。

Http 协议

  • HTTP 协议构建于 TCP/IP 协议之上,是一个应用层协议,默认端口号是 80
  • HTTP 是无连接无状态的
]]>
<p>计划写两篇关于 解析 Volley源码 和 Okhttp源码的文章 先了解下基础的网络知识。</p> <p>目前存在的两种网络分层模型:OSI模型和TCP/IP模型。OSI模型一共分为七层,TCP/IP模型和OSI模型类似,但是只分为四层。</p> <h3 id="OSI模
ListView 优化及相关问题总结 http://youngerdev.com/ListView-优化及相关问题总结.html 2019-08-11T02:56:37.000Z 2019-08-11T08:57:11.478Z ListView 优化

最近在做项目过程中频繁使用列表,今天抽空总结下过程中遇到的问题,下面会有具体对应的解决办法;

  • 1.listView 外套一层ScrollView 的问题,就是listView 只显示一行或者两行, 我发现这个问题在 RecycleView 上也存在, 在RecycleView外套一层 ScrollView也会出现只显示一行的问题,滑动只能在 一行的高度内滑动。这问题下面有具体解决方案,说下我是怎么解决的吧,因为我要实现的是9宫格的图片展示,本来是用的RecycleView 来实现的,出现了这个问题,我就尝试把recyclerView 换成了GridView 了,因为GridView 本身就支持多行展示,发现就没有这个问题了,套了ScrollView 也正常展示;
  • 2.也是listView 在嵌套ScrollView 中的问题,在某款三星手机上,这个手机的虚拟按键,可以锁定和解锁定, 发现在切换锁定和解锁定中,会导致原来的多行的图片变成单行, 在别的手机上就没有这个问题, 这个奇怪的现象,最终还是以GridView 替换掉listView 解决的。
  • 3.listView 添加 headerView的问题, 需要注意的是 添加进去的 HeaderView 就占据了第0位,所以在使用的 onItemClick 的时候,需要-1, addfooterView的时候可以用 view.getFooterViewsCount() 是否等于0来判断,避免多次添加。
  • 4.由于listView 会复用viewHolder ,所以我们在getView 中 显示隐藏的设置一定要配套使用,if里有显示,在else 中就要 隐藏。

优化步骤:

使用 RecycleView 代替listview

  • 1.重用ConvertView;
  • 2.使用View Holder模式;
  • 3.使用异步线程加载图片(一般都是直接使用图片库加载,如Glide, Picasso);
    建议:
  • 1.在adapter的getView方法中尽可能的减少逻辑判断,特别是耗时的判断;
  • 2.避免GC
  • 3.在快速滑动时不要加载图片;
  • 4.将ListView的scrollingCache和animateCache这两个属性设置为false(默认是true);
  • 5.尽可能减少List Item的Layout层次(如可以使用RelativeLayout替换LinearLayout,或使用自定的View代替组合嵌套使用的Layout)

ListView 嵌套 ScrollView

Listview不能显示正常的条目,只显示一条或二条

这是因为:由于listView在scrollView中无法正确计算它的大小, 故只显示一行。

解决方案:

1. 方法一:重写ListView, 覆盖onMeasure()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
WrapperListView.java:
public class WrapperListView extends ListView {
public WrapperListView(Context context) {
super(context);
}
public WrapperListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public WrapperListView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public WrapperListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
/**
* 重写该方法,达到使ListView适应ScrollView的效果
*/

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}
2.方法二:动态设置listview的高度,不需要重写ListView

只需要在setAdapter之后调用如下方法即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void setListViewHeightBasedOnChildren(ListView listView) {
// 获取ListView对应的Adapter
ListAdapter listAdapter = listView.getAdapter();
if (listAdapter == null) {
return;
}
int totalHeight = 0;
for (int i = 0, len = listAdapter.getCount(); i < len; i++) {
// listAdapter.getCount()返回数据项的数目
View listItem = listAdapter.getView(i, null, listView);
// 计算子项View 的宽高
listItem.measure(0, 0);
// 统计所有子项的总高度
totalHeight += listItem.getMeasuredHeight();
}
ViewGroup.LayoutParams params = listView.getLayoutParams();
params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
// listView.getDividerHeight()获取子项间分隔符占用的高度
// params.height最后得到整个ListView完整显示需要的高度
listView.setLayoutParams(params);
}

这时最好给ListView之外嵌套一层LinearLayout,不然有时候这种方法会失效

3.方法三:在xml文件中,直接将Listview的高度写死

可以确定的是:这种方式可以改变ListView的高度,但是,还有一个严重的问题就是listview的数据是可变动的,除非你能正确的写出listview的高度,否则这种方式就是个鸡肋。

4. addHeadView()

如果只有数据 layout 完全没有必要嵌套 ScrollView 的.之说以需要嵌套,很多是在listView 头部或者底部多了一部分Layout ,这个时候可以尝试 listView 的 addHeadView() 尝试解决

在一次显示ListView的界面时,getView会被执行几次?

比如有5组数据要填充到listView。listView会先调用onMeasure,此时会调用5次getView。然后才调用onLayout,此时又会调用5次getView,这样就重复了。所以导致多次调用getView方法

的确调用了三遍

解决办法:

ListView 的高度 从 wrap_content 改成 match_patent 或者一个固定值,就能减少getview 调用次数

或者重写 listView 的onMasure onLayout 方法 赋值 一个变量 检测 是测量还是layout 测量时不加载自己写的那段逻辑

listview失去焦点怎么处理?

在listview子布局里面写,可以解决焦点失去的问题
android:descendantFocusability=”blocksDescendants”

ListView 优化

  • 1.首先,虽然大家都知道,还是提一下,利用好 convertView 来重用 View,切忌每次 getView() 都新建。ListView 的核心原理就是重用 View。ListView 中有一个回收器,Item 滑出界面的时候 View 会回收到这里,需要显示新的 Item 的时候,就尽量重用回收器里面的 View。

  • 2.利用好 View Type,例如你的 ListView 中有几个类型的 Item,需要给每个类型创建不同的 View,这样有利于 ListView 的回收,当然类型不能太多

  • 3.尽量让 ItemView 的 Layout 层次结构简单,这是所有 Layout 都必须遵循的
  • 4.善用自定义 View,自定义 View 可以有效的减小 Layout 的层级,而且对绘制过程可以很好的控制;
  • 5.尽量能保证 Adapter 的 hasStableIds() 返回 true,这样在 notifyDataSetChanged() 的时候,如果 id 不变,ListView 将不会重新绘制这个 View,达到优化的目的;
  • 6.每个 Item 不能太高,特别是不要超过屏幕的高度,可以参考 Facebook 的优化方法,把特别复杂的 Item 分解成若干小的 Item,
  • 7.为了保证 ListView 滑动的流畅性,getView() 中要做尽量少的事情,不要有耗时的操作。特别是滑动的时候不要加载图片,停下来再加载,这个库可以帮助你 Glide:https://github.com/bumptech/glide
  • 8.使用 RecycleView 代替。 ListView 每次更新数据都要 notifyDataSetChanged(),有些太暴力了。RecycleView 在性能和可定制性上都有很大的改善,推荐使用。
]]>
<h2 id="ListView-优化"><a href="#ListView-优化" class="headerlink" title="ListView 优化"></a>ListView 优化</h2><p>最近在做项目过程中频繁使用列表,今天抽空总结下过程中遇到的问题,下面
网站优化 http://youngerdev.com/网站优化.html 2019-05-30T00:11:39.000Z 2019-05-30T00:24:17.130Z 网站在今年2月份就创建了,但是一直没有怎么优化。最近几天,简单美化了一下
现在就把相关的优化网站列一下,希望对读者有点帮助。

Github Pages + Hexo 博客搭建,Next主题个性化修改 有很多链接,包括从创建到优化,还有视频教程。

Hexo博客主题安装及Next主题个性化修改 不得不说,这个对我帮助很大。

hexo - Next 主题添加评论功能 这个是给我们的网站加评论的, 我选择 Valine 评论系统 ,我希望可以匿名评论。

后续还有 seo 优化还没有做,这个会继续更新的。

]]>
<p>网站在今年2月份就创建了,但是一直没有怎么优化。最近几天,简单美化了一下<br>现在就把相关的优化网站列一下,希望对读者有点帮助。</p> <p><a href="https://www.lixint.me/hexo-blog.html" target="_blank" r
Android中公共代码仓库与私服的使用 http://youngerdev.com/Android中公共代码仓库与私服的使用.html 2019-05-27T15:18:43.000Z 2019-05-29T15:34:30.358Z 上篇文章 从ARM暂停与华为合作谈起 Android中SO兼容的那些事 中提到了我们是用自己私服上的weex sdk 出的问题继而解决的问题,今天就来介绍下我们的私服, 并手把手教会你怎么创建并使用私服.

背景

随着公司业务发展,项目越来越大,项目有好几个,项目虽然具体业务不同,但是其中也有很多相同的功能, 比如都有 weex 需求,网络请求、 图片处理、视频处理等等共同的需求,如果每个项目都重新写一遍,实现一遍,势必影响进度,重复造轮子的问题,项目进展缓慢,所以 就需要一种方式来避免这种重复劳动,彻底解决这种问题。

解决办法 : 使用组件化的思想, 把公共的组件抽离出来,和主项目的关系是依赖主项目的关系。module 是以 library的形式存在于整个project 中,依赖于主工程。在需要使用的时候,引入即可。
类似我们使用 recyclerview butterknife 之类的这些的功能, 这些内容有的是 Android官方提供的支持,有的是国内外优秀开发者开源出来的控件,我们可以很方便的集成到项目中。如下图:

仓库1.png

知名仓库介绍

目前有 jcenter() ,mavenCentral(), google() 等等仓库中心,这些仓库是用来保存代码的,组织或者个人开发者 开源出很多优秀的代码,工具,控件,上传到这些仓库中,我们可以通过 在 gradle 中配置相应的仓库地址 ,就可以方便使用相应的代码,

而内部使用的组件,不适合开源的可以自己搭建 私有仓库,内网使用,
还有一些需要借助代理访问,国内访问起来特别慢或者无法访问的内容,也可以上传到我们的私服上,能够很好地提高效率,减少同步等待时间
类似下图 我们在其中一个项目中使用的 仓库地址:

仓库2.png

我们通过在 gradle 中配置相应的依赖,项目在 build 完成后就会将相应的 依赖工程代码 下载到 项目中, 可以在project 模式下 查看 External Libraries 看到实际下载下来的代码。

仓库3.png

下面分别介绍下 这几个仓库,和私有仓库的搭建

jcenter

网页地址 http://jcenter.bintray.com/

mavenCentral

网页地址http://central.maven.org/maven2/

google

网页地址 https://dl.google.com/dl/android/maven2/

其他仓库

以阿里云为例
网页地址 http://maven.aliyun.com/nexus/content/repositories/jcenter/

上图中已经展示了 在项目中的配置这些仓库的方式。

这些仓库又有什么区别呢?

maven中央仓库http://repo1.maven.org/maven2/)是由Sonatype公司提供的服务,它是Apache Maven、SBT和其他构建系统的默认仓库,并能很容易被Apache Ant/Ivy、Gradle和其他工具所使用。开源组织例如Apache软件基金会、Eclipse基金会、JBoss和很多个人开源项目都将构件发布到中央仓库。 maven中央仓库已经将内容浏览功能禁掉了。

jcenter仓库https://jcenter.bintray.com )是由JFrog公司提供的Bintray中的Java仓库。它是当前世界上最大的Java和Android开源软件构件仓库。 所有内容都通过内容分发网络(CDN)使用加密https连接获取。JCenter是Goovy Grape内的默认仓库,Gradle内建支持(jcenter()仓库),非常易于在(可能除了Maven之外的)其他构建工具内进行配置。

JCenter相比mavenCenter构件更多,性能也更好。但还是有些构件仅存在mavenCenter中。

google()存储库是Google maven资源库的快捷方式。 它是在Gradle 4.x +中引入的。 使用的实际存储库URL是“https://dl.google.com/dl/android/maven2/”, 也可以使用 maven { url ‘https://maven.google.com' }
但是 google(),则需要Gradle 4.x +,Android Studio 3.x +和Android 3.x +的Gradle插件。

maven (阿里云) maven.aliyun.com代理了很多公共的maven仓库。使用maven.aliyun.com中的仓库地址作为下载源,速度更快更稳定。
https://maven.aliyun.com/repository/public 是 central仓和jcenter仓的聚合仓 经过测试发现,可以代替 central 和 jcenter ,把阿里云的这个放到第一位会加快同步速度。

仓库4.png

阿里云除了代理了 mavenCentral, jcenter还有 上面的 google() 基本上主流的 仓库都有, 在国内这个网络环境下, 使用阿里云的代理仓库是一个挺好的选择.

什么是Maven?

Maven 是一个项目管理和自动构建工具。是一个软件(特别是Java软件)项目管理以及自动构建工具,由Apache软件基金会所提供。是基于项目对象模型(缩写:POM)概念,Maven利用一个中央信息片断能管理一个项目的构建、报告和文档等步骤。
Maven也可以被利用与构建和管理各种项目,例如:C#、Ruby、Scala和其他语言编写的项目。
Maven项目使用项目对象模型(Project Object Modle,POM)来配置项目,对象模型存储在名为pom.xm的文件中。

什么是Gradle?

Gradle是一个基于Apache Ant和Apache Maven概念的项目自动化构建工具,是一款通用灵活的构建工具,支持maven, Ivy仓库,支持传递性依赖管理,而不需要远程仓库或者是pom.xml和ivy.xml配置文件,基于Groovy(DSL语言,所谓的DSL是指这个语言应用在特定的领域,而类似Java这样是DCL语言,可以运用在普通的各个领域),而不是传统的xml语言,build脚本使用Groovy编写。当前支持的语言仅限于Java、Groovy、Scala、Kotlin。计划未来支持更多的语言。
Gradle改良了过去Maven、Ant带给开发者的问题,也已经成为Android Studio内置封装部署工具

Android支持的Maven仓库:

  • 1.mavenCentral 是最早的 maven 中央仓库
  • 2.jcenter 是 Android Studio 0.8 版本起的默认 maven 中央仓库
  • 3.本机的仓库
  • 4.部署在内网服务器的私有仓库

上面也说了,我们可以把自己写的优秀代码共享到这些代码仓库中供全世界 开发者使用,但是由于公司的项目很多不能公开,只可在公司范围使用,又需要在不同项目中可以方便接入,在后续功能修改时 又能通过版本控制 使用修改前后的功能均可正常使用, 这个时候可以 搭建私有 maven 仓库,部署我们的公共组件代码,就可以方便使用了。

使用Nexus搭建 maven 私服

下面演示在window上的安装过程

1.下载Nexus

下载地址:http://www.sonatype.com/download-oss-sonatype
仓库5.png

2.解压zip文件,配置nexus下bin目录全局变量

仓库6.png

然后打开我的电脑->属性->高级系统设置->高级->环境变量

在path 中配置 上面的bin 文件夹的路径进去 F:\Downloads\nexus-3.16.1-02-win64\nexus-3.16.1-02\bin

3.安装nexus服务,

以管理员身份运行 cmd 进入 上面bin的文件夹下, 运行nexus.exe/install Nexus Service命令
查看服务里就能看到

仓库7.png

4.启动服务,

启动这个服务,稍等一会时间,在浏览器中访问 http://localhost:8081/。就能够看到这个页面

仓库8.png

默认的用户名和密码登录(admin/admin123) 目前为止 服务搭建成功。

仓库的详细配置过程,请移步到官方网站
http://books.sonatype.com/nexus-book/reference3/admin.html#admin-repositories

下面来看怎么使用的问题;

上传 aar 到我们搭建的私服上 步骤

1. Nexus创建自己的仓库

访问:http://localhost:8081/nexus,
先创建一个你要上传的仓库信息:

仓库9.png

点击create repository

仓库10.png

仓库11.png

name那输入你想创建的名称,如 younger
然后点创建,就可以看到了自己的仓库 http://10.1.1.147:8081/repository/younger/ (此ip为我自己电脑局域网ip)

2. Android Studio 配置

新建一个module,这个module就是你要编译的aar
module下gradle配置如下,
添加代码:
apply plugin:’maven’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
uploadArchives {
configuration = configurations.archives
repositories {
mavenDeployer {
repository(url: 'http://10.1.1.147:8081/repository/younger/') {
authentication(userName: 'admin', password: 'admin123')
}
pom.project {
version '0.0.1'
artifactId 'testmaven'
groupId 'demo.younger.com'
packaging 'aar'
description 'update version 0.0.1'
}
}
}
}

groupId,version要注意填好。
然后,在Gradle projects列表中可以看到upload的Task
点击后即可编译上传,success后就可以了。

仓库12.png

仓库13.png

可以看到编译成功 ,在Nexus后台可以看到:
仓库14.png

3. Android Studio 中引用

//添加仓库
在根gradle 中添加

1
2
3
4
5
6
7
8
9
10
11
allprojects {
repositories {
google()
jcenter()
//新增
maven {
url 'http://10.1.1.147:8081/repository/younger/'
}

}
}

在项目的gradle 中使用

1
implementation 'demo.younger.com:testmaven:0.0.1'

就可以正常引用了, 可以看到上面的顺序是 groupId :artifactId: version

也可以在 External Libraries 查看到

仓库15.png

项目中也可以引用啦

目前项目中weex就是采用这种方式, 搭建一个nexus 私服,把weex 官方的sdk 下载下来,我们根据项目需要,自行实现很多不同的功能,
而这些功能在不同的项目都可以用到,就统一放到私服上,在我们具体的项目中,只需要简单设置,就可以非常方便的使用这些功能,
如果新增了什么功能,可以直接把代码上传到公共组件中,升级版本, 项目中用的时候只需要改成相应的版本号就可以方便使用到最新的功能。

总结

可以发现,创建私服,上传aar文件, 引用都非常简单, 具体有没有必要使用还是需要根据自身实际情况来看,那些公共仓库上的内容可以优先使用阿里云的代理仓库,这样会加快同步速度,自己内部使用或者比较特殊的内容就可以放到私服上了,同样是特别方便。

]]>
<p>上篇文章 <a href="https://youngerdev.com/Android中SO文件使用.html">从ARM暂停与华为合作谈起 Android中SO兼容的那些事</a> 中提到了我们是用自己私服上的weex sdk 出的问题继而解决的问题,今天就来介绍下我们
从ARM暂停与华为合作谈起 Android中SO兼容的那些事 http://youngerdev.com/Android中SO文件使用.html 2019-05-08T15:18:03.000Z 2019-05-30T00:24:29.407Z

因为包含来自美国的技术,ARM(英国)已经要求员工“停止所有与华为及其子公司正在生效的合约、支持及未决约定”

从最近的新闻来看,美国的贸易禁令使得华为公司腹背受敌,ARM彻底暂停与华为合作已成定局,最新款的芯片技术肯定是用不上了,不过华为已经获得了ARMv8的永久授权。但是ARM是什么样的存在? 为什么对华为有这么大的影响力呢,事实上 ARM处理器 已经一统移动端了。今天就来谈谈 ARM 与 Android SO 的那些事。

背景:

公司某项目新上功能部分页面使用 weex 来做的,测试过程中发现 weex 页面在某些手机上出现异常,经过排查定位到是 SO 库的问题,下面是这个问题的解决过程的一个记录,希望能对遇到类似问题的同学提供一些帮助。

什么问题?

公司某项目 V1.2.0版本最初上线时,使用的 weex 是官方的 SDK 的接入方式, 接下来 App版本需要把官方的 SDK 替换成我们自己私服上的 SDK(因为私服上有我们针对自己项目需要,自定义了许多控件和公共组件) ,替换后,出现 weex 页面加载异常的问题(白屏)

报错信息如下:

so1.jpeg

我们注意到,上面有一行错误是 invokeInitFramework java.lang.UnsatisfiedLinkError: 见过该错误的开发者都知道,这个是 JNI 相关的错误信息,根据错误信息能看到是 weex 相关的 JNI 调用出了问题,由JNI 又想到了 SO 库文件, 那就引出今天的话题。

什么是 SO 文件及SO 应用?

SO(shared object,共享库)是机器可以直接运行的二进制代码

  • SO 机制让开发者最大化利用已有的 C 和 C++ 代码,达到复用的效果,利用软件世界积累了几十年的优秀代码;

  • SO 是二进制,没有解释编译的开销,用SO实现的功能比纯java实现的功能要快;

  • SO 内存分配不受 Dalivik/ART 的单个应用限制,减少 OOM;

  • 相对于java代码,二进制代码的反编译难度更大,一些核心代码可以考虑放在 SO 中。

在Android 中 提到 SO 就不能不提 ABI

ABI

应用程序二进制接口(Application Binary Interface)

定义了其所对应的CPU架构能够执行的二进制文件(特别是.so文件)的格式规范。在 Android 系统上,不同 Android 手机使用不同的 CPU,因此支持不同的指令集。CPU 与指令集的每种组合都有其相应的应用二进制界面(或 ABI)。

ABI 可以非常精确地定义应用的机器代码在运行时如何与系统交互。 您必须为应用要使用的每个 CPU 架构指定 ABI:armeabi,armeabi-v7a,arm64-v8a,x86,x86_64,mips,mips64;

目前 Android 共支持七种不同类型的 CPU 架构,分别是:ARMv5,ARMv7 (从2010年起),x86 (从2011年起),MIPS (从2012年起),ARMv8,MIPS64和x86_64 (从2014年起)

so11.png

SO(CPU)的兼容

每一个 CPU 架构对应一个 ABI,一个 CPU 属于某一种架构,多核 CPU 属于相同架构才能一起工作,很多设备仅支持一种 CPU 架构。

如果你要完美兼容所有类型的机型,理论上是要在的 libs 目录下放置各个架构平台的 SO 文件。

但项目体积也会变得非常庞大。是否一定需要带入这么多SO文件去兼容呢?答案是否定的。

根据目前Android共支持七种不同类型的CPU架构,其兼容特点可总结如下:

armeabi设备只兼容armeabi;

armeabi-v7a设备兼容armeabi-v7a、armeabi;

arm64-v8a设备兼容arm64-v8a、armeabi-v7a、armeabi;

X86设备兼容X86、armeabi;

X86_64设备兼容X86_64、X86、armeabi;

mips64设备兼容mips64、mips;

mips只兼容mips;

armeabi 的 SO 文件基本上可以说是兼容目前市面上的大部分手机,它能运行在除了mips和mips64的设备上,但在非 armeabi 设备上运行性能会有所损耗;

64位的CPU架构总能向下兼容其对应的32位指令集,如:x86_64兼容X86,arm64-v8a兼容armeabi-v7a,mips64兼容mips;

总结成一句话就是, 新的CPU 架构总能向下兼容;

所以知道这些信息后,查看公司其他项目怎么用的, 发现其他项目, 在 build.gradle 中关于 ndk.abiFilters 都是这样写的:

so2.png

而这个项目的写法是这样的:

so3.png

区别就是这个 ndk.abiFilters , 既然这样写,必定有它的原因, 那就继续找,为什么要这么写,咨询相关的同事,了解到相关路径下的文件是 视频聊天、语音相关的 SO 库。

那能不能改成只支持 armeabi 或者 他们其中的部分呢。那就模拟这些情况,然后分别打出了这么多包(包的命名有点随意,这不是重点),分别在不同的手机上验证是否能正常加载 weex 。

so4.png

so5.png

经测试发现 armeabi 的和 armeabi +x86 的都可以正常加载 weex 页面,感觉问题这么简单就解决了,但是!!!

想到去掉这么多东西 会不会影响其他正常功能呢, 那就验证下吧,试了下视频相关功能,果不其然的 app 崩溃了。

说明上面的方向肯定是有问题,使用工具 Native libs Monitor 查看下Apk 中具体的SO 文件

首先查看下那些我们常用的App 是怎么做的,微信、QQ、支付宝等等

其他公司如何适配的

  • 微信(只适配armeabi,有少量 v7a);

  • qq(只适配armeabi,文件夹下有少量x86);

  • 百度地图(只适配armeabi);

  • 大众点评(只适配armeabi);

  • google 家 (基本都是 arm64);

  • 支付宝 (基本都是 armeabi 的,2个x86的);

而且 公司别的项目App 中 用到的 face++ 的人脸识别,身份证识别 也是只提供了armeabi 的SO 文件

下图是微信的Apk 解压出的情况, 微信的lib下虽然只有armeabi-v7a一个目录,但目录内的文件仍放着v7a和 armeabi架构的SO文件,用于处理兼容带来的某些性能运算问题。

so12.png

看完大公司的适配情况,然后看下我们自己 公司这几个项目,看看到底有什么区别, 还有上面打出的不同的包,相应的 SO 引入情况。

经过对比发现, 指定不同的 abi 会 在apk 中 打入相应 的文件(前提是 你有这些文件),这就是一个过滤器,只在包中引入指定的cpu 架构的 SO 文件。

so6.png

既然重点是 weex 加载的出了问题,然后就重点查看 weex 相关的 SO 引入情况, 既然官方的可以正常使用, 我们私服上却出了问题,然后对比使用官方的 sdk 集成方式,和我们私服上的集成方式的 SO 库有什么不同;

如图是集成weex 官方sdk 的 APK
so7.png

而集成私服上的 sdk 后,只在 armv5中有 libweexjsc.so 而 armv7 中是没有这个问题的。 区别找到了,那就去私服的项目中找原因
so8.png

并没有 armeabi-v7a 的文件,相应的 lib下也没有相应的文件夹和文件,按照一样的写法,在这加入 armeabi-v7a 相应的路径,相应的 lib 下,也新建了相应的文件夹,放入相应的文件。重新编译

so9.png

再次尝试, 查看 lib 在 arm v7 和 v8a 的手机上,查看 lib 情况,发现, 在都能看到相应的 SO 文件了,

多部手机尝试, 视频聊天、语音、 weex 都正常、至此 问题得到解决。

在排查问题的过程中发现,其实这个项目 本来的 build.gradle 文件中的 arm64-v8a 写错了,以前写成了 armeabi-v8a 其实这样是无法使用到 arm64-v8a下的 so 的。

但是会向下兼容, 使用了 armeabi-v7a 中的文件,也没有报错,其实也是因祸得福,如果写正确的话,weex 在 arm64-v8a 也会加载失败,因为 官方也没有提供这样的一个文件夹和文件。

但是我们要怎么配置呢?

从目前移动端CPU市场的份额数据看,ARM架构几乎垄断,所以,除非你的用户很特殊,否则几乎可以不考虑单独编译带入X86、X86_64、mips、mips64架构SO文件。除去这四个架构之后,还要带入armeabi、armeabi-v7a、arm64-v8a 这三个不同类型,这对于一个拥有大量SO文件的应用来说,安装包的体积将会增大不少。

针对不同平台,如何去适配,如何抉择

目前主流的Android设备主要是 armeabi-v7a ARMv8 架构的,程序在运行的时候去加载不同平台对应的so,这是较为完美的一种解决方案,但是有时候为了减少包体积的大小,

不会同时设置 armeabi, armeabi-v7a 和 x86。根据不同的情况,可以进行不同的适配,

1.只适配 armeabi-v7a,因为目前主流机型是 ARMv7,并且 ARMv8 设备也向下兼容了armeabi-v7a, Facebook、WhatsApp、王者荣耀等就是只适配了armeabi-v7a。(Google play store下载 Native libs Monitor 进行查看)。

2.只适配 armeabi,因为 ARMv7 、ARMv8 还是 x86 都兼容 armeabi,但是性能都会有些损耗,例如ARMv7 支持硬件浮点运算等没法体现,x86 支持 armeabi 同样具有相应的损耗。

3.同时适配 armeabi-v7a 和 armeabi,既能够支持所有 ARM 架构,同时又能具有 ARMv7 支持硬件浮点运算等特性,例如Line等应用。

4.同时适配 x86 和 armeabi,既能支持所有 ARM 架构,又能支持x86架构,唯一的缺点就是没有了ARMv7 支持硬件浮点运算等一系列特性,例如QQ。

5.同时适配 armeabi, armeabi-v7a 和 x86,在性能方面来说是较为完美的方案,只是APK的大小也会随之变大。

6.还有其他的一些方案,例如微信只适配了armeabi,但是对于某些需要利用 ARMv7 支持硬件浮点运算等一系列特性的操作,在armeabi目录下存在v7对应的so文件,通过代码判断加载不同的so文件。即达到了减少APK大小的目的,又能达到适配ARMv7等架构以便使用其架构的一些新特性的目的。

就目前市场份额而言,绝大部分的设备都已经是armeabi-v7a、arm64-v8a,可以考虑只保留armeabi-v7a架构的SO文件,这样能获得更好的性能效果。所以我们的 这个项目选择采用第3种方案;
so10.png

现在把这个项目中的 ndk.abiFilters 配置成上图所示,几乎覆盖所有机型,weex 能够正常加载,暂时没有发现影响其他功能,这个问题也算是得到解决。在不影响太多性能的情况下,也可以明显减少包的体积。

总结下常见的 引入.so文件的错误

1.使用android高版本平台版本编译的.so文件运行在android低版本的设备上

使用NDK时,你可能会倾向于使用最新的编译平台,但事实上这是错误的,因为NDK平台不是向下兼容的,而是向上兼容的。推荐使用app的minSdkVersion对应的编译平台。

这也意味着当你引入一个预编译好的.so文件时,你需要检查它编译时所用的平台版本。

2.没有为每个支持的CPU架构提供对应的.so文件

arm64-v8a是可以向下兼容的,但前提是你的项目里面没有arm64-v8a的文件夹,如果你有两个文件夹armeabi和arm64-v8a,两个文件夹,armeabi里面有a.so 和 b.so,arm64-v8a里面只有a.so,

那么arm64-v8a的手机在用到b的时候发现有arm64-v8a的文件夹,发现里面没有b.so,就报错了,

所以这个时候删掉arm64-v8a文件夹,这个时候手机发现没有适配arm64-v8a,就会直接去找armeabi的so库,或者把 arm64-v8a文件夹 的 b.so补齐。

参考文章:

https://zhuanlan.zhihu.com/p/21302804

https://www.zhihu.com/question/36893314/answer/78467097

https://blog.csdn.net/zophar_development/article/details/84329054

https://www.cnblogs.com/janehlp/p/7473240.html

https://www.jianshu.com/p/cb05698a1968

https://www.jianshu.com/p/cb15ba69fa89

]]>
<blockquote> <p>因为包含来自美国的技术,ARM(英国)已经要求员工“停止所有与华为及其子公司正在生效的合约、支持及未决约定”</p> </blockquote> <p>从最近的新闻来看,美国的贸易禁令使得华为公司腹背受敌,ARM彻底暂停与华为合作已成定局,最新款的
开站顺利 http://youngerdev.com/开站顺利.html 2019-02-18T14:12:37.000Z 2019-05-29T14:33:33.642Z 这是Younger 的第一篇内容

终于我也有自己的个人网站了

昨天是周日,下午开始弄这个,申请域名, 搭建网站,整个过程真的好简单,以前一直以为很难的,就一直没有动手的去弄; 目前来看, 困难都是自己想象出来的, 真正动手去做,就会发现,也没有那么难,真正需要做的就是行动, 勇敢踏出第一步

购买域名

域名是在 namesilo 买的, 用了优惠码后一年也就是不到50元,支持支付宝支付,方便至极 ,如果有需要可以寻找网上的优惠码, 可以节省1美元, 我这个域名买下来一年5.99美元,也就是不到45块钱,可谓是相当的划算了.

关于域名商的选择我也是查了下资料, 之所以没有选择国内的服务商, 主要是众所周知的原因, 国内的需要备案, 且你的域名随时可能被关掉等不安全因素, 还是在国际知名的大的域名商买比较靠谱, namesilo 这个打的广告比较少, 说是让利给消费者, 的确是很便宜, 我是对比了多家, 发现这家便宜的, 还有 namecheap 也比较便宜. 这个还是见仁见智了, 想买哪个都行, 续费便宜, 服务好就行.

主机

使用的是 github.io + hexo 搭建的,
过程非常的简单, 感谢各位大佬的教程

hexo官网的文档 也很详细,还有视频教程,
另外还有好多主题供选择; 傻瓜式安装.每一步都有详细的教程, 然后就成了我现在弄的这个样子, 后续我肯定还会继续更新的.

今天简单弄了一下,记录一下, 虽然用的也都是别人的轮子,但还是很开心呀!

]]>
<h2 id="这是Younger-的第一篇内容"><a href="#这是Younger-的第一篇内容" class="headerlink" title="这是Younger 的第一篇内容"></a>这是Younger 的第一篇内容</h2><p><strong>终于我也有自