AsyncTask使用及实现原理
讲解顺序:
1.AsyncTask简单介绍
2.主要方法及作用
3.应用与使用
4.实现原理分析
1.AsyncTask简单介绍
AsyncTask是开发中常用的异步实现工具,又因为其无需再通过Handler 更新ui ,所以使用起来比较方便,在开发中使用频率较高。内部主要由Handler ,线程池 实现类ThreadPoolExecutor 等构成,主要实现了异步执行任务并且可执行ui线程任务,下面我们先从使用在研究如何实现的。
2.主要方法及作用
AsyncTask是一个抽象类,所以要使用必须要继承它
public abstract class AsyncTask<Params, Progress, Result>
泛型1:代表传入参数的可变数组类型,既定义 doInBackground 的参数类型
泛型2:代表异步任务的执行进度的可变变长度数据类型,既定义onProgressUpdate 参数类型
泛型3:代表异步任务执行结果的返回值类型及 异步执行结束onPostExecute 参数类型
private class MyAsyncTask extends AsyncTask<String, Integer, Boolean>{
/**
* 任务即将开始
*/
@Override
protected void onPreExecute() {
super.onPreExecute();
}
/**
* 任务已经开始执行了,此处执行耗时任务
* @param params
* @return
*/
@Override
protected Boolean doInBackground(String... params) {
return null;
}
/**
* 任务执行结束,返回异步执行结果
* @param aBoolean
*/
@Override
protected void onPostExecute(Boolean aBoolean) {
super.onPostExecute(aBoolean);
}
/**
* 任务的执行进度
* @param values
*/
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
}
/**
* 将异步任务设置为:取消状态
* @param aBoolean
*/
@Override
protected void onCancelled(Boolean aBoolean) {
super.onCancelled(aBoolean);
}
}
这里单独说下 publishProgress 方法
protected final void publishProgress(Progress... values) {
if (!isCancelled()) {
getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
new AsyncTaskResult<Progress>(this, values)).sendToTarget();
}
}
它的作用是用来更新当前异步任务执行的进度,在doInBackground 中调用,其参数类型为一个可变数组,类型就是新建类时泛型3的类型。当调用 publishProgress 方法后会执行 onProgressUpdate 方法,此方法在ui线程中执行
3.应用与使用
上面讲了方法的使用,当我们将耗时操作在 doInBackground 处理完毕并且给出返回值,在onPostExecute中处理ui逻辑后,开始调用
MyAsyncTask myAsyncTask=new MyAsyncTask();
myAsyncTask.execute();
这里调用的是无参数的execute()方法,也可以
myAsyncTask.execute("1"); 这里简单传递了一个参数1,这里的参数类型就是泛型1所定义的可变长度数组类型了。
使用我们简单聊这么多,如果你想要详细了解使用方法可以参考: AsyncTask使用
下面我们将对AsyncTask进行详细的原理分析
4.实现原理分析
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
开始执行任务调用了execute 方法并且参数是一个可变数组,调用 executeOnExecutor 方法。
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
Params... params) {
if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw new IllegalStateException("Cannot execute task:"
+ " the task is already running.");
case FINISHED:
throw new IllegalStateException("Cannot execute task:"
+ " the task has already been executed "
+ "(a task can be executed only once)");
}
}
//将当前线程状态设为运行
mStatus = Status.RUNNING;
//调用onPreExecute 方法
onPreExecute();
//将参数赋值给mParams
mWorker.mParams = params;
//调用Executor接口的execute方法
exec.execute(mFuture);
return this;
}
这里看到调用了onPreExecute 方法用于即将开始任务的一些数据处理,将参数存储在实现了Callable接口的
WorkerRunnable抽象类中,然后调用Executor的execute方法。这里插入看下AsyncTask的构造函数。
public AsyncTask() {
this((Looper) null);
}
实际上调用了public AsyncTask(@Nullable Looper callbackLooper)
public AsyncTask(@Nullable Looper callbackLooper) {
mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
? getMainHandler()
: new Handler(callbackLooper);
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
mTaskInvoked.set(true);
Result result = null;
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
result = doInBackground(mParams);
Binder.flushPendingCommands();
} catch (Throwable tr) {
mCancelled.set(true);
throw tr;
} finally {
postResult(result);
}
return result;
}
};
mFuture = new FutureTask<Result>(mWorker) {
@Override
protected void done() {
try {
postResultIfNotInvoked(get());
} catch (InterruptedException e) {
android.util.Log.w(LOG_TAG, e);
} catch (ExecutionException e) {
throw new RuntimeException("An error occurred while executing doInBackground()",
e.getCause());
} catch (CancellationException e) {
postResultIfNotInvoked(null);
}
}
};
}
首先获Looper 对象,如果你不了解handler的原理建议先看写下Handler 的实现原理,否则可能有点蒙,意思就是获取主线程looper对象,并且绑定handler.
WorkerRunnable 方法是实现了Callable 接口的静态类内部只有一个数组。
private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
Params[] mParams;
}
这里mWorker 既(WorkerRunnable)初始化,call()方法啥时执行我们后边分析,接着mFuture既(FutureTask)被初始化了,FutureTask是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果的。现在我们重新回到
executeOnExecutor方法。 看这行 exec.execute(mFuture); 那么这个execute在哪里执行的呢?
首先 executeOnExecutor 方法第一个参数是Executor 这段我们上边看到过
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
sDefaultExecutor 是个啥呢?
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
他只不过是一个静态变量SERIAL_EXECUTOR 赋值的。
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
SERIAL_EXECUTOR 是一个静态类变量,且一开始就初始化了,看下SerialExecutor方法
private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;
public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
因为AsyncTask 本身是一个抽象类, SerialExecutor 是抽象类中静态类初始化,无论AsyncTask被多少类继承 其属性决定了SerialExecutor 及 ArrayDeque 只会被new 一次 。所以每次调用线程池方法mTasks都是同一个变量。ArrayDeque双端队列的实现类。
offer方法是将此Runnable 加入到队列的末尾,然后逐个执行。run方法中执行了r.run();这个方法很重要,那么这个run是在哪里执行的呢。
我们回到 executeOnExecutor 方法,exec.execute(mFuture)其实就是 SerialExecutor 中的execute方法,这里的r 变量就是 mFuture,mFuture是 FutureTask 的变量名,所以r.run()调用的是FutureTask中的run()方法。这里比较绕,要仔细理一理。
public void run() {
if (state != NEW ||
!U.compareAndSwapObject(this, RUNNER, null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
现在我们看下 FutureTask 类
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
这个方法是Futuretask 的一个构造函数,就是将传进来的参数callable 赋值给自身的变量callable 并且将状态设为new。上边我们在AsyncTask的构造函数中有两个类的初始化其中Futretask 类的初始化调用的就是这个构造函数。再看下
public AsyncTask(@Nullable Looper callbackLooper)方法。
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
mTaskInvoked.set(true);
Result result = null;
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
result = doInBackground(mParams);
Binder.flushPendingCommands();
} catch (Throwable tr) {
mCancelled.set(true);
throw tr;
} finally {
postResult(result);
}
return result;
}
};
mFuture = new FutureTask<Result>(mWorker) {
@Override
protected void done() {
原来传进来的是mWorker 既 实现了Callable 接口的抽象类WorkerRunnable 类。
现在回到FutureTask 类的run()方法。
public void run() {
if (state != NEW ||
!U.compareAndSwapObject(this, RUNNER, null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
Callable<V> c = callable; 将callable(上边刚讲过) 赋值给变量c ,c不是null 并且 state 状态是NEW,执行WorkerRunnable 的call()方法
紧接着,如果调用成功ran==true 执行set方法。
call方法回调执行内容
public AsyncTask(@Nullable Looper callbackLooper) 方法
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
mTaskInvoked.set(true);
Result result = null;
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
result = doInBackground(mParams);
Binder.flushPendingCommands();
} catch (Throwable tr) {
mCancelled.set(true);
throw tr;
} finally {
postResult(result);
}
return result;
}
};
在这里执行了doInBackground方法,并且将mParams 参数传递给它。
然后看FutureTask 中set方法
protected void set(V v) {
if (U.compareAndSwapInt(this, STATE, NEW, COMPLETING)) {
outcome = v;
U.putOrderedInt(this, STATE, NORMAL); // final state
finishCompletion();
}
}
执行了finishCompletion
private void finishCompletion() {
// assert state > COMPLETING;
for (WaitNode q; (q = waiters) != null;) {
if (U.compareAndSwapObject(this, WAITERS, q, null)) {
for (;;) {
Thread t = q.thread;
if (t != null) {
q.thread = null;
LockSupport.unpark(t);
}
WaitNode next = q.next;
if (next == null)
break;
q.next = null; // unlink to help gc
q = next;
}
break;
}
}
done();
callable = null; // to reduce footprint
}
主要看下done()方法,mFuture 重写了done 方法,所以会调用到
mFuture = new FutureTask<Result>(mWorker) {
@Override
protected void done() {
try {
postResultIfNotInvoked(get());
} catch (InterruptedException e) {
android.util.Log.w(LOG_TAG, e);
} catch (ExecutionException e) {
throw new RuntimeException("An error occurred while executing doInBackground()",
e.getCause());
} catch (CancellationException e) {
postResultIfNotInvoked(null);
}
}
};
看下postResultIfNotInvoked方法
private void postResultIfNotInvoked(Result result) {
final boolean wasTaskInvoked = mTaskInvoked.get();
if (!wasTaskInvoked) {
postResult(result);
}
}
postResult方法
private Result postResult(Result result) {
@SuppressWarnings("unchecked")
Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
new AsyncTaskResult<Result>(this, result));
message.sendToTarget();
return result;
}
通过handler发送消息 ,所以下边的操作都在ui线程中执行了
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
调用了finish方法
private void finish(Result result) {
if (isCancelled()) {
onCancelled(result);
} else {
onPostExecute(result);
}
mStatus = Status.FINISHED;
}
是否取消,如果没有停止就调用 onPostExecute 并且将状态设为 FINISHED
这时 重写的onPostExecute执行了,并且参数为我们定义AsyncTask 泛型的参数三的参数类型。既 返回 子线程中
doInBackground的返回结果。
这里可能仔细看会有疑问,在call()方法中的 finally 中也执行了 postResult方法 ,在done 中最后也执行了postResult方法,不是说最后onPostExecute 会调用两遍了,不对啊,如果你看的够仔细你会找到答案,在call()方法中
mTaskInvoked.set(true); 方法 ,将value设为1;
public final void set(boolean newValue) {
value = newValue ? 1 : 0;
}
当调用done 方法中的 postResultIfNotInvoked 时首先会去获取该值,并且是0才会向下执行
private void postResultIfNotInvoked(Result result) {
final boolean wasTaskInvoked = mTaskInvoked.get();
if (!wasTaskInvoked) {
postResult(result);
}
}
所以如果call成功执行,done 中的onPostExecute 就不执行了。
到现在已经讲了主要三个方法 的执行过程
onPreExecute 主线程中
doInBackground 子线程中
onPostExecute 主线程中
这是我们使用AsyncTask最常用的三个方法,上边为了快速理清流程,有些地方一笔带过了,现在我们重新认识下SerialExecutor类,
这个类我们上边提过,现在我觉得有必要仔细看下,因为它决定了AsyncTask中线程池的执行过程及时机。
private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;
public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
首先SerialExecutor实现了Executor接口,可以看到其内部只有一个方法execute方法参数是Runnable 接口
public interface Executor {
void execute(Runnable command);
}
开始执行会调用到executeOnExecutor中的exec.execute(mFuture); exec就是SerialExecutor类的变量,会执行SerialExecutor内的
execute方法,然后执行offer方法,但是Runnable 接口的Run方法不会被执行,只是new 了一个Runnable 放入mTasks数组中,因为没人去调用run方法,所以不会被调用,接着判断mActive是否为空,第一次调用当然是空了,所以会执行scheduleNext()方法。
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
从队列首部获取一个元素给mActive赋值,然后调用
THREAD_POOL_EXECUTOR的execute 方法,这个获取到的mActive就是刚才new 的Runnable接口了,所以,run方法的执行要看THREAD_POOL_EXECUTOR的execute具体如何实现。这个暂且先放下,后边再仔细分析,先把流程理通。直接看run方法被调用执行了,上边我们已经分析过r.run(),实际上是调用了FutureTask中的run方法,这里就不讲了,它使用了try finally 语法,我们知道finally中的语法无论如何是都会执行的。所以当一个任务执行完成以后,都会去调用下scheduleNext方法,用于查找队列中是否还有未执行的任务,如果有,就又开始执行。这是一个有序队列按照先后顺序执行。所以,如果你有多个类继承自AsyncTask 并且调用了它的execute方法,那么他会按照顺序一个一个的去执行,并不是并发执行。看到这里,这点我们要明白。
现在我们来看下THREAD_POOL_EXECUTOR的execute 方法,THREAD_POOL_EXECUTOR其实是间接实现了Executor接口的子类ThreadPoolExecutor所以看下其内部execute 实现,这里要看的仔细些,要看的问题是:
mTasks.offer 所new的那个Runnable 接口的run方法何时,在哪里执行?
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
/*1.获取当前正在运行线程数是否小于核心线程池,是则新创建一个线程执行任务,否则将任务放到任务队列中*/
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))//在addWorker中创建工作线程执行任务
return;
c = ctl.get();
}
/*2.当前核心线程池中全部线程都在运行workerCountOf(c) >= corePoolSize,所以此时将线程放到任务队列中*/
if (isRunning(c) && workQueue.offer(command)) {//线程池是否处于运行状态,且是否任务插入任务队列成功
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))//线程池是否处于运行状态,如果不是则使刚刚的任务出队
reject(command);//抛出RejectedExceptionException异常
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
/*3.插入队列不成功,且当前线程数数量小于最大线程池数量,此时则创建新线程执行任务,创建失败抛出异常*/
else if (!addWorker(command, false))
reject(command);//抛出RejectedExceptionException异常
}
我们上边说了,Runnable 接口的run方法调用没有看到,这里我们就仔细分析下,主要方法addWorker
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
//如果当前线程数已经大于最大线程数直接返回
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//将正在运行的线程数+1,数量自增成功则跳出循环,自增失败则继续从头继续循环
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//正在运行的线程数自增成功后则将线程封装成工作线程Worker
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
//将线程封装为Worker工作线程
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
//全局锁
final ReentrantLock mainLock = this.mainLock;
//获取全局锁
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
/*当持有了全局锁的时候,还需要再次检查线程池的运行状态等*/
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
////线程处于活跃状态,即线程已经开始执行或者还未死亡,正确的应线程在这里应该是还未开始执行的
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
//包含线程池中所有的工作线程,只有在获取了全局的时候才能访问它。将新构造的工作线程加入到工作线程集合中
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
//新构造的工作线程加入成功
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
//在被构造为Worker工作线程,且被加入到工作线程集合中后,执行线程任务,注意这里的start实际上执行Worker中run方法,所以接下来分析Worker的run方法
t.start();
workerStarted = true;
}
}
} finally {
//未能成功创建执行工作线程
if (! workerStarted)
//在启动工作线程失败后,将工作线程从集合中移除
addWorkerFailed(w);
}
return workerStarted;
}
主要的方法已经做过备注。其中主要Worker工作线程类;
w = new Worker(firstTask); 将传进来的Runnable 接口传给Worker
看下Worker 类
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* This class will never be serialized, but we provide a
* serialVersionUID to suppress a javac warning.
*/
private static final long serialVersionUID = 6138294804551838833L;
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker. */
public void run() {
runWorker(this);
}
// Lock methods
//
// The value 0 represents the unlocked state.
// The value 1 represents the locked state.
protected boolean isHeldExclusively() {
return getState() != 0;
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
首先它实现了Runnable 接口,构造函数内,将传进来的Runnable 接口赋值给自身的firstTask变量,同时从线程工厂获取一个线程,并将实现的Runnable 绑定
this.thread = getThreadFactory().newThread(this);
所以在addWorker方法中调用 t.start() 既调用了 Worker 中获取到的thread 对象的start,所以此时Worker中的Run()方法被执行了。
public void run() {
runWorker(this);
}
这里调用了 final void runWorker(Worker w) 方法
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
//获取Worker 中的firstTask构造参数
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
//调用Runnable 接口的run方法
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
这里看到最终调用的run方法,这个run方法就是我们上边提的问题的答案:
mTasks.offer 所new的那个Runnable 接口的run方法何时,在哪里执行? ,是的,就是在这里执行的。
所以,在这个run方法中运行的方法都是在子线程中运行的,所以我们可以在doInBackground 方法中执行异步任务。
还有更新进度相关的两个方法没有介绍,由于篇幅有点长,也搞了几天了,就暂时结束,后边会专门针对这两个方法再写一遍补充文章。
如果你看到文章中有错误,或者有更好的理解,欢迎留言交流,或者加qq 1301749314
参考:
https://www.cnblogs.com/yulinfeng/p/7021293.html
https://www.cnblogs.com/sg9527/p/8004502.html
https://zhidao.baidu.com/question/1639323053250406060.html
https://blog.csdn.net/zmx729618/article/details/52767736
https://blog.csdn.net/dove_knowledge/article/details/71077512