今天要分享的是Android基礎(chǔ)知識(shí)篇,往往我們都是拿來主義,知道怎么用卻不知道原理,今天就來講講Android線程相關(guān)的知識(shí)點(diǎn)吧。一如既往的,寫文初心,便于追溯,總結(jié)知識(shí),如果能給看到這篇文章的你提供幫助,那價(jià)值就更大了。
Android中線程分為主線程和子線程,主線程主要用于UI相關(guān)的事務(wù),是進(jìn)程默認(rèn)情況下?lián)碛械木€程,looper是main looper,在主線程中不能做耗時(shí)操作,因?yàn)橹骶€程對(duì)于響應(yīng)速度要求很高,如果五秒不響應(yīng)系統(tǒng)就會(huì)出現(xiàn)ANR報(bào)錯(cuò),就算不超過五秒耗時(shí)操作也會(huì)造成界面的卡頓,嚴(yán)重影響用戶體驗(yàn),所以一定要記住耗時(shí)操作不要在主線程中進(jìn)行,比如數(shù)據(jù)庫訪問,網(wǎng)絡(luò)訪問等,當(dāng)然在Android3.0以上如果在主線程中進(jìn)行網(wǎng)絡(luò)訪問就會(huì)報(bào)NetworkOnMainThreadExcepetion異常。而子線程可以說是工作線程的,可用于耗時(shí)操作,網(wǎng)絡(luò)請求等。
子線程我們最熟悉的實(shí)現(xiàn)方式就是Thread,但是Thread的大量new可不是一件好事情,在阿里巴巴編程規(guī)范里面就有提到建議不要直接new Thread 應(yīng)該用線程池來代替,可以管理和復(fù)用線程。在Android中除了Thread的呢,還有HandlerThread,AsyncTask以及IntentService,當(dāng)然啦,還有線程池。AsyncTask相信很多人都有用到過,用于在主線程創(chuàng)建并且進(jìn)行異步操作,在回調(diào)方法中可進(jìn)行UI的刷新和操作進(jìn)程的監(jiān)聽等。 而IntentService呢,內(nèi)部其實(shí)也是Thread和Handler實(shí)現(xiàn)的,它的優(yōu)勢在于它是一個(gè)service可以在后臺(tái)運(yùn)行,相對(duì)來說優(yōu)先級(jí)比其他線程來說更好,不容易被系統(tǒng)殺死。
HandlerThread是具有消息循環(huán)的線程,我們可以利用HnandlerThread在其中運(yùn)行Handler從而將一個(gè)Handler建立在子線程中運(yùn)行。而AsyncTask則是內(nèi)部封裝了線程池和Handler,接下來我們會(huì)分別分析AsyncTask的源碼看看它是如何完成整個(gè)過程的,以及IntentService,HandlerThread相關(guān)原理和使用。
AsyncTask
AsyncTask 用于異步任務(wù)執(zhí)行,利用線程池在后臺(tái)執(zhí)行任務(wù),然后借助于Handler將任務(wù)進(jìn)程以及任務(wù)結(jié)果返回到UI,從而實(shí)現(xiàn)在子線程中進(jìn)行耗時(shí)操作,而在主線程更新UI的功能,對(duì)于我們程序中請求網(wǎng)絡(luò)并且刷新UI來說是個(gè)很好的選擇,但是呢,從下面這段從源碼摘抄過來的類注釋可以看出,AsyncTask不適用于特別耗時(shí)的后臺(tái)任務(wù),只適用于幾秒的操作,對(duì)于特別耗時(shí)的后臺(tái)任務(wù)建議使用線程池或者FutureTask。
syncTask is designed to be a helper class around {@link Thread} and {@link Handler}
* and does not constitute a generic threading framework. AsyncTasks should ideally be
* used for short operations (a few seconds at the most.) If you need to keep threads
* running for long periods of time, it is highly recommended you use the various APIs
* provided by the <code>java.util.concurrent</code> package such as {@link Executor},
* {@link ThreadPoolExecutor} and {@link FutureTask}.</p>
疑問: 為什么AsyncTask不適用于特別長時(shí)間的耗時(shí)操作呢?
- AsyncTask的生命周期和Activity不一致,如果是操作時(shí)間太長的話,當(dāng)Activity由于旋轉(zhuǎn)屏幕或者其他原因銷毀了的時(shí)候,當(dāng)操作執(zhí)行完會(huì)找不到要更新的UI從而報(bào)錯(cuò)。
java.lang.IllegalArgumentException: View not attached to window manager. 比如你想關(guān)于一個(gè)dialog,你并沒有在onstop中去dimiss掉這個(gè)dialog。 - 因?yàn)锳syncTask在執(zhí)行長時(shí)間的耗時(shí)任務(wù)時(shí)也會(huì)持有一個(gè)Activity對(duì)象,即使這個(gè)Activity已經(jīng)不可見了,Android也無法對(duì)這個(gè)Activity進(jìn)行回收,導(dǎo)致內(nèi)存泄露。
- 當(dāng)然你可能會(huì)問,難道AsyncTask不能手動(dòng)cancel,答案當(dāng)然是可以啦, 但是AsyncTask的cancel方法有一個(gè)弊端,那就是當(dāng)doInBackground()正在執(zhí)行一個(gè)不可打斷的工作的方法會(huì)失效,比如BitmapFactory.decodeStream()的IO操作,當(dāng)然只要你想cancel成功,你也可以在cancel之前強(qiáng)制停止IO操作,捕捉異常,保證AsyncTask準(zhǔn)確的被cancel,關(guān)于AsyncTask的Cancel的使用等會(huì)會(huì)簡單介紹一下,是有一點(diǎn)小差別的。
下面我們一起來看看AsyncTask的源碼然后看看實(shí)現(xiàn)原理.
public abstract class AsyncTask<Params, Progress, Result> {
首先AsyncTask是一個(gè)抽象類,它有三個(gè)泛型參數(shù),從字面意思可知,第一個(gè)是參數(shù),第二個(gè)是任務(wù)進(jìn)度,第三個(gè)是返回結(jié)果類型。
要使用AsyncTask的時(shí)候必須繼承實(shí)現(xiàn)抽象方法。AsyncTask有4個(gè)核心的方法。 如下:
注: 此段源碼摘抄自android-26
從注解可知: onPreExecute是工作在主線程中的一個(gè)方法,主要用于在開始執(zhí)行異步任務(wù)之前做一些前期準(zhǔn)備工作。
@MainThread
protected void onPreExecute() {
}
doInBackground是在工作線程即子線程執(zhí)行后臺(tái)任務(wù)的方法,在這里你可以實(shí)現(xiàn)你要執(zhí)行的后臺(tái)任務(wù),數(shù)據(jù)庫請求網(wǎng)絡(luò)請求等。 參數(shù)Params和創(chuàng)建AsyncTask子類的時(shí)候Params同類型的參數(shù),用于給后臺(tái)任務(wù)提供一些信息。在這個(gè)方法,可以通過調(diào)用publicProgress來更新任務(wù)進(jìn)度,publicProgress會(huì)調(diào)用onProgressUpdate方法。
并且在這個(gè)方法將返回任務(wù)執(zhí)行的返回結(jié)果,結(jié)果會(huì)被onPostExecute接收并處理
@WorkerThread
protected abstract Result doInBackground(Params... params);
這個(gè)方法執(zhí)行在主線程中用于監(jiān)聽當(dāng)前任務(wù)的進(jìn)度,可以根據(jù)進(jìn)度更新主線程UI告知用戶任務(wù)執(zhí)行的進(jìn)度的。
@MainThread
protected void onProgressUpdate(Progress... values) {
}
同樣此方法運(yùn)行在主線程中,用于接收任務(wù)的結(jié)果,
@MainThread
protected void onPostExecute(Result result) {
}
這四個(gè)方法的執(zhí)行順序是: 1.onPreExecute 2. doInBackground 最后是onPostExecute。在doInBackground中如果有調(diào)用publicProgress方法被調(diào)用的話就會(huì)執(zhí)行onProgressUpdate方法。
AsyncTask還提供了cancel方法如下:
當(dāng)mayInterruptIfRunning is true 則中斷當(dāng)前正在執(zhí)行的任務(wù),false則允許當(dāng)前正在執(zhí)行的任務(wù)執(zhí)行完才結(jié)束。調(diào)用此方法之后會(huì)回調(diào)onCanceled()方法,不會(huì)調(diào)用onPostExecute方法、
public final boolean cancel(boolean mayInterruptIfRunning) {
mCancelled.set(true);
return mFuture.cancel(mayInterruptIfRunning);
}
,但是要注意的是,AsyncTask中的cancel()方法并不是真正去取消任務(wù),只是設(shè)置這個(gè)任務(wù)為取消狀態(tài),我們需要在doInBackground()判斷終止任務(wù)。
下面是一個(gè)簡單的應(yīng)用實(shí)例:
/**
* URL 下載地址
* Integer 下載進(jìn)度
* Integer 下載結(jié)果 總共下載的文件長度
*/
public static class DownLoadingFileAsyncTask extends AsyncTask<URL, Integer, Integer>{
private static final String TAG = DownLoadingFileAsyncTask.class.getSimpleName();
@Override
protected void onPreExecute() {
Log.i(TAG, "onPostExecute");
}
@Override
protected void onPostExecute(Integer integer) {
Log.i(TAG, "onPostExecute");
}
@Override
protected void onProgressUpdate(Integer... values) {
Log.i(TAG, "onProgressUpdate" + values[0]);
}
@Override
protected Integer doInBackground(URL... urls) {
int length = 0;
for (URL url : urls) {
length = downloadFile(url);
publishProgress(length);
if (isCancelled()){
break;
}
}
return length;
}
}
使用方法:
new DownLoadingFileAsyncTask().execute(url1, url2, url3);
這里我只是示意一下所以實(shí)現(xiàn)都比較簡單,我執(zhí)行了一個(gè)下載文件的操作,并且調(diào)用publicProgress方法更新下載進(jìn)度,在onProgressUpdate里面打印了當(dāng)前下載的進(jìn)度.
從我們調(diào)用的方法的入口我們來看看AsyncTask是如何實(shí)現(xiàn)的:
首先看看execute方法:
@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
從注解可以知道首先execute必須在主線程執(zhí)行,可以看到這個(gè)方法只是調(diào)用了executeOnExecutor方法。 sDefaultExecutor是一個(gè)串行的線程池d.定義如下:
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static class SerialExecutor implements Executor {
final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
Runnable mActive;
public synchronized void execute(final Runnable r) {
//向隊(duì)列尾部插入一個(gè)新的runable對(duì)象
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}
//取出隊(duì)列頭部第一個(gè)任務(wù)并且不為null的執(zhí)行
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
//用于執(zhí)行任務(wù)的線程池 THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
從上面可以知道sDefaultExecutor是一個(gè)靜態(tài)的SerialExecutor對(duì)象,從SerialExecutor的實(shí)現(xiàn)可以看出它是一個(gè)一個(gè)執(zhí)行任務(wù)的,當(dāng)前沒有active的任務(wù)的時(shí)候,就會(huì)調(diào)用scheduleNext()執(zhí)行下一個(gè)任務(wù)。并且是串行執(zhí)行。
需要注意的是:這個(gè)方法是執(zhí)行的時(shí)候AsyncTask是串行執(zhí)行還是并行執(zhí)行取決于Android版本,在一開始的Android1.6之前AsyncTask是串行執(zhí)行的,但Android1.6之后AsyncTask變成了并行執(zhí)行,不過為了避免的并行帶來的麻煩,Android3.0又開始使用單線程串行執(zhí)行,這個(gè)在源碼中的方法的注解中都有明確的說明的,不過不是說Android3.0方法以后就不能執(zhí)行并行操作了,你可以用這個(gè)方法實(shí)現(xiàn)AsyncTask的的并行操作。executeOnExecutor 如下:
new DownLoadingFileAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url1, url2, url3);
new DownLoadingFileAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url1, url2, url3);
new DownLoadingFileAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url1, url2, url3);
new DownLoadingFileAsyncTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url1, url2, url3);
一起來看看executeOnExecutor的實(shí)現(xiàn):
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;
//調(diào)用了onPreExecute()方法s
onPreExecute();
mWorker.mParams = params;
exec.execute(mFuture);
executeOnExecutor首先判斷一下當(dāng)前AsyncTask的狀態(tài)是否正在運(yùn)行或者已經(jīng)執(zhí)行完了,然后是就拋出異常,所以這也決定了AsyncTask的execute必須也只能調(diào)用一次 . 可以看到在這個(gè)方法中首先調(diào)用了onPreExecute方法。 然后執(zhí)行線程池執(zhí)行了mFuture這個(gè)RunableTask,而mFuture呢就是執(zhí)行的mWorker這個(gè)Runable的call方法。
mWorker和mFuture的定義如下:
private final WorkerRunnable<Params, Result> mWorker;
private final FutureTask<Result> mFuture;
private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
Params[] mParams;
}
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
//調(diào)用了doInBackground方法
result = doInBackground(mParams);
Binder.flushPendingCommands();
} catch (Throwable tr) {
mCancelled.set(true);
throw tr;
} finally {
//發(fā)送result到Handler
postResult(result);
}
return result;
}
};
mFuture = new FutureTaskd<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);
}
}
};
executeOnExecutor方法中將params賦值給了mWorker的params對(duì)象,mWorker的call方法中執(zhí)行了doInBackground并且將結(jié)果通過Hanlder發(fā)送出去。
Handler如下:
private static class InternalHandler extends Handler {
public InternalHandler(Looper looper) {
super(looper);
}
@SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
@Override
public void handleMessage(Message msg) {
AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
switch (msg.what) {
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;
}
}
}
在InternalHandler接收到MESSAGE_POST_RESULT之后調(diào)用了AsyncTask的finish方法如下:
private void finish(Result result) {
if (isCancelled()) {
onCancelled(result);
} else {
onPostExecute(result);
}
mStatus = Status.FINISHED;
}
可以看到finish方法首先判斷了是否被取消,如果取消了就調(diào)用onCanceled方法把結(jié)果返回,否則回調(diào)onPostExecute,將結(jié)果返回,至此我們將 一個(gè)任務(wù)的執(zhí)行到返回結(jié)果的路徑都跟蹤完了。這就是任務(wù)異步執(zhí)行的全過程, InternalHandler是一個(gè)運(yùn)行在主線程中的Handler,new的語句如下:
private static Handler getMainHandler() {
synchronized (AsyncTask.class) {
if (sHandler == null) {
sHandler = new InternalHandler(Looper.getMainLooper());
}
return sHandler;
}
}
下面我們一起看看更新進(jìn)度的方法publishProgress:
@WorkerThread
protected final void publishProgress(Progress... values) {
if (!isCancelled()) {
getHandler().obtainMessage(MESSAGE_POST_PROGRESS,
new AsyncTaskResult<Progress>(this, values)).sendToTarget();
}
}
顯而易見,該方法向handler發(fā)送了MESSAGE_POST_PROGRESS消息,Handler回調(diào)了onProgressUpdate(result.mData)方法。
現(xiàn)在終于清楚了整個(gè)調(diào)用的過程了吧。 其實(shí)AsyncTask的源碼注釋說明里面也給出了很清楚的解說,所以多看看源碼也有利于我們更加了解類的實(shí)現(xiàn)和原理。
這里需要注意的是: 在Android5.0以下AsyncTask必須在主線程中加載,至于為什么很簡單,因?yàn)镮nternalHandler是一個(gè)靜態(tài)內(nèi)部類,而它又必須有主線程的looper,靜態(tài)內(nèi)部類在類加載的時(shí)候就完成初始化,所以這就要求AsyncTask必須在主線程中執(zhí)行。
HandlerThread
我們直接看HandlerThread的源碼:
public class HandlerThread extends Thread {
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
}
此處只貼出關(guān)鍵的部分,感興趣的可以自己去看完整的源碼哦,我就不全貼出來湊字?jǐn)?shù)了。^_^
HandlerThread繼承自Thread,在run里面利用Looper實(shí)現(xiàn)了消息隊(duì)列功能,我們都知道Handler的原理中就包含了Looper,Looper負(fù)責(zé)消息的循環(huán),這里也是一樣的,HandlerThread借助Looper無線循環(huán)的輪詢,從而執(zhí)行對(duì)應(yīng)的Message。同時(shí)HandlerThread提供了quit和quitSafely方法,因?yàn)閘ooper是無線循環(huán)的,所以不需要時(shí),記得養(yǎng)成良好的習(xí)慣停止HandlerThread,應(yīng)用場景如下:
一般我們在程序中借助handlerThread來開啟一個(gè)非主線程的Handler進(jìn)行消息處理做一些耗時(shí)的操作。
HandlerThread handlerThread = new HandlerThread("worker thread");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper()){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
HandlerThread相對(duì)簡單一點(diǎn),所以我們就將這么多了。
IntentService
顧名思義,IntentService是一個(gè)繼承與service的類,內(nèi)部利用HandlerThread和handler實(shí)現(xiàn)了對(duì)消息的傳遞。IntentService是一個(gè)抽象類,子類必須繼承onHandlerIntent方法用于對(duì)消息的處理。
/**
* This method is invoked on the worker thread with a request to process.
* Only one Intent is processed at a time, but the processing happens on a
* worker thread that runs independently from other application logic.
* So, if this code takes a long time, it will hold up other requests to
* the same IntentService, but it will not hold up anything else.
* When all requests have been handled, the IntentService stops itself,
* so you should not call {@link #stopSelf}.
*
* @param intent The value passed to {@link
* android.content.Context#startService(Intent)}.
* This may be null if the service is being restarted after
* its process has gone away; see
* {@link android.app.Service#onStartCommand}
* for details.
*/
@WorkerThread
protected abstract void onHandleIntent(@Nullable Intent intent);
從注釋可以看到這個(gè)方法也是串行的,所以當(dāng)有很多請求的時(shí)候回堵塞當(dāng)前IntentService的其他請求,當(dāng)所有請求都被執(zhí)行完了之后,IntentService會(huì)調(diào)用stopSelf停止他自己。
IntentSerivce是如何對(duì)外界Intent進(jìn)行處理的, 每次調(diào)用Service的時(shí)候雖然只會(huì)調(diào)用一次onCreate,但是會(huì)重復(fù)調(diào)用onStartCommond方法,onStartCommond里面調(diào)用了onStart方法,看看onStart做了什么。
@Override
public void onStart(@Nullable Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}
onStart將startId以及Intent包裹在msg里面發(fā)送到了ServiceHandler,在看看ServiceHandler做了些什么。
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}
不管接收到了什么消息,ServiceHandler都是直接回調(diào)onHandlerIntent方法。然后停止自己。而上面我們說了onHandlerIntent的方法是有子類實(shí)現(xiàn)的,所以子類自己實(shí)現(xiàn)然后處理相應(yīng)的Intent消息。
最后一下有關(guān)于線程池的知識(shí):
線程池的優(yōu)點(diǎn): 實(shí)現(xiàn)線程的復(fù)用,控制線程池最大的并發(fā)數(shù),對(duì)線程進(jìn)行管理。
這里我只提及一下ThreadPoolExecutor的構(gòu)造方法。
通過配置相關(guān)的參數(shù)創(chuàng)建一個(gè)線程池。
參數(shù)解說如下:
1.corePoolSize 線程池中核心線程數(shù)
2、maximumPoolSize 線程池中允許的最大線程數(shù)
3.keepAliveTime 非核心線程閑置時(shí)的超時(shí)時(shí)間,超過就會(huì)被回收掉,如果allowCoreThreadTimeOut為true,核心線程也會(huì)被回收掉。
4.unit 超時(shí)的時(shí)間單位
5. 線程池中的任務(wù)隊(duì)列
6.創(chuàng)建線程工廠
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
核心線程會(huì)一直在線程池中不會(huì)被回收,除非allowCoreThreadTimeOut 為true。那么當(dāng)核心線程閑置時(shí)間超過keepAliveTime的時(shí)候就會(huì)回收掉。
非核心線程當(dāng)閑置的時(shí)候就會(huì)被回收掉。當(dāng)活動(dòng)線程達(dá)到最大線程池的時(shí)候,后面的線程就會(huì)被阻塞。
ThreadPoolExecutor執(zhí)行任務(wù)的規(guī)則:
- 如果線程池中的線程數(shù)量未達(dá)到核心線程的數(shù)量,那么會(huì)直接啟動(dòng)一個(gè)核心線程來執(zhí)行任務(wù)
- 如果線程池中的線程數(shù)量已經(jīng)超過了核心線程數(shù)量則插入任務(wù)隊(duì)列里面等待
- 當(dāng)任務(wù)隊(duì)列滿了的時(shí)候,并且線程沒有達(dá)到規(guī)定的最大線程的數(shù)量的時(shí)候則啟動(dòng)一個(gè)非核心線程執(zhí)行任務(wù)。
- 如果線程數(shù)量已經(jīng)超過了最大線程池?cái)?shù)量,則拒絕任務(wù)調(diào)用RejectedExecutionHandler的rejectedExecution來通知調(diào)用者。
我們常見的線程池子類有: FixedThreadPool, CachedThreadPool,ScheduledThreadPool,SingleThreadExecutor。這些只是配置了不同參數(shù)的線程池而已,所以感興趣的可以自己百度看看哦。
好了 碼了這么久,終于學(xué)習(xí)筆記總結(jié)完了,總結(jié)了一下之后,感覺把之前看的知識(shí)點(diǎn)又重新溫習(xí)了一遍,影響更加深刻了,受益匪淺。 不知道 看到這里,你學(xué)會(huì)了多少。有總結(jié)的不對(duì)的地方,請不吝指出,歡迎討論, 謝謝。