Java併發程式設計(5)-- AsyncTask
一、使用
1、定義
在Android中實現非同步任務機制有兩種方式,Handler和AsyncTask。Handler模式需要為每一個任務建立一個新的執行緒,任務完成後通過Handler例項向UI執行緒傳送訊息,完成介面的更新,這種方式對於整個過程的控制比較精細,但也是有缺點的,例如程式碼相對臃腫,在多個任務同時執行時,不易對執行緒進行精確的控制。為了簡化操作,Android1.5提供了工具類android.os.AsyncTask, 它使建立非同步任務變得更加簡單,不再需要編寫任務執行緒和Handler例項即可完成相同的任務。
2、AsyncTask的定義:
public abstract class AsyncTask<Params, Progress, Result> {}
三種泛型型別分別代表“啟動任務執行的輸入引數”、“後臺任務執行的進度”、“後臺計算結果的型別”。
在特定場合下,並不是所有型別都被使用,如果沒有被使用,可以用java.lang.Void型別代替。
3、一個非同步任務的執行一般包括以下幾個步驟:
1)、execute(Params… params),執行一個非同步任務,需要我們在程式碼中呼叫此方法,觸發非同步任務的執行。
2)、onPreExecute(),在execute(Params… params)被呼叫後立即執行,一般用來在執行後臺任務前對UI做一些標記。
3)、doInBackground(Params… params),在onPreExecute()完成後立即執行,用於執行較為費時的操作,此方法將接收輸入引數和返回計算結果。在執行過程中可以呼叫publishProgress(Progress… values)來更新進度資訊。
4)、onProgressUpdate(Progress… values),在呼叫publishProgress(Progress… values)時,此方法被執行,直接將進度資訊更新到UI元件上。
5)、onPostExecute(Result result),當後臺操作結束時,此方法將會被呼叫,計算結果將做為引數傳遞到此方法中,直接將結果顯示到UI元件上。
在使用的時候,有幾點需要格外注意:
1)、非同步任務的例項必須在UI執行緒中建立。
2)、execute(Params… params)方法必須在UI執行緒中呼叫。
3)、不要手動呼叫onPreExecute(),doInBackground(Params… params),onProgressUpdate(Progress… values),onPostExecute(Result result)這幾個方法。
4)、不能在doInBackground(Params… params)中更改UI元件的資訊。
5)、一個任務例項只能執行一次,如果執行第二次將會丟擲異常。
4、樣例
public void onNewClick(View v) {
//注意每次需new一個例項,新建的任務只能執行一次,否則會出現異常
mTask = new MyTask();
mTask.execute("http://www.baidu.com");
}
public void onCancelClick(View v) {
//取消一個正在執行的任務,onCancelled方法將會被呼叫
mTask.cancel(true);
}
private class MyTask extends AsyncTask<String, Integer, String> {
//onPreExecute方法用於在執行後臺任務前做一些UI操作
@Override
protected void onPreExecute() {
Log.i(TAG, "onPreExecute() called");
textView.setText("loading...");
}
//doInBackground方法內部執行後臺任務,不可在此方法內修改UI
@Override
protected String doInBackground(String... params) {
Log.i(TAG, "doInBackground(Params... params) called");
try {
HttpClient client = new DefaultHttpClient();
HttpGet get = new HttpGet(params[0]);
HttpResponse response = client.execute(get);
if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
HttpEntity entity = response.getEntity();
InputStream is = entity.getContent();
long total = entity.getContentLength();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int count = 0;
int length = -1;
while ((length = is.read(buf)) != -1) {
baos.write(buf, 0, length);
count += length;
//呼叫publishProgress公佈進度,最後onProgressUpdate方法將被執行
publishProgress((int) ((count / (float) total) * 100));
//為了演示進度,休眠500毫秒
Thread.sleep(500);
}
return new String(baos.toByteArray(), "gb2312");
}
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
return null;
}
//onProgressUpdate方法用於更新進度資訊
@Override
protected void onProgressUpdate(Integer... progresses) {
Log.i(TAG, "onProgressUpdate(Progress... progresses) called");
progressBar.setProgress(progresses[0]);
textView.setText("loading..." + progresses[0] + "%");
}
//onPostExecute方法用於在執行完後臺任務後更新UI,顯示結果
@Override
protected void onPostExecute(String result) {
Log.i(TAG, "onPostExecute(Result result) called");
textView.setText(result);
execute.setEnabled(true);
cancel.setEnabled(false);
}
//onCancelled方法用於在取消執行中的任務時更改UI
@Override
protected void onCancelled() {
Log.i(TAG, "onCancelled() called");
textView.setText("cancelled");
progressBar.setProgress(0);
execute.setEnabled(true);
cancel.setEnabled(false);
}
}
二、原始碼分析
AsyncTask可以選擇序列或並行執行任務。
當我們new了一個AsyncTask的物件,並執行execute方法後,
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
實際上執行的是executeOnExecutor方法,裡面傳了一個Executor物件,
在AsyncTask類中,定義了2個Executor物件。
第一個即上述的 sDefaultExecutor,
第二個是 THREAD_POOL_EXECUTOR
我們先看第二個THREAD_POOL_EXECUTOR
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
這是一個執行緒池,物件裡的引數定義如下:
//CPU_COUNT為手機中的CPU核數
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//將手機中的CPU核數加1作為AsyncTask所使用的執行緒池的核心執行緒數的大小
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
//將CPU_COUNT * 2 + 1作為AsyncTask所使用的執行緒池的最大執行緒數的大小
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 1;
//例項化執行緒工廠ThreadFactory,sThreadFactory用於在後面建立執行緒池
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
//mCount為AtomicInteger型別,AtomicInteger是一個提供原子操作的Integer類,
//確保了其getAndIncrement方法是執行緒安全的
private final AtomicInteger mCount = new AtomicInteger(1);
//重寫newThread方法的目的是為了將新增執行緒的名字以"AsyncTask #"標識
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
//例項化阻塞式佇列BlockingQueue,佇列中存放Runnable,容量為128
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
執行緒池當然是支援並行操作的。
再看第一個Executor物件 sDefaultExecutor:
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) {
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內部維護了一個存放Runnable的雙端佇列mTasks。當執行SerialExecutor的execute方法時,會傳入一個Runnable變數r,但是mTasks並不直接儲存r,而是又新new了一個匿名Runnable物件,其內部會呼叫r,這樣就對r進行了封裝,將該封裝後的Runnable物件通過佇列的offer方法入隊,新增到mTasks的隊尾。
SerialExecutor內部通過mActive儲存著當前正在執行的任務Runnable。當執行SerialExecutor的execute方法時,首先會向mTasks的隊尾新增進一個Runnable。然後判斷如果mActive為null,即當前沒有任務Runnable正在執行,那麼就會執行scheduleNext()方法。當執行scheduleNext方法的時候,會首先從mTasks中通過poll方法出隊,刪除並返回隊頭的Runnable,將返回的Runnable賦值給mActive,如果不為空,那麼就讓將其作為引數傳遞給THREAD_POOL_EXECUTOR的execute方法進行執行。由此,我們可以看出SerialExecutor實際上是通過之前定義的執行緒池THREAD_POOL_EXECUTOR進行實際的處理的。
當將mTasks中的Runnable作為引數傳遞給THREAD_POOL_EXECUTOR執行execute方法時,會線上程池的工作執行緒中執行匿名內部類Runnable中的try-finally程式碼段,即先在工作執行緒中執行r.run()方法去執行任務,無論任務r正常完成還是丟擲異常,都會在finally中執行scheduleNext方法,用於執行mTasks中的下一個任務。從而在此處我們可以看出SerialExecutor是一個接一個執行任務,是序列執行任務,而不是並行執行。
所以當我們啟動AsyncTask的execute方法時,預設是序列執行任務,如果想並行執行任務,可以直接呼叫AsyncTask的executeOnExecutor方法,傳入其內建的執行緒池THREAD_POOL_EXECUTOR,或自己定義一個執行緒池。
接下來看下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();
mWorker.mParams = params;
exec.execute(mFuture);
return this;
}
首先,
AsyncTask內部定義了一個Status列舉型別,如下所示:
public enum Status {
//PENDING表示還沒有開始執行任務
PENDING,
//RUNNING表示已經開始執行任務
RUNNING,
//FINISHED表示任務已經執行完成或被取消了,總之onPostExecute方法已經被呼叫了
FINISHED,
}
一個AsyncTask正常情況下會經歷PENDING->RUNNING->FINISHED三個狀態。
所以,
一個AsyncTask例項只能執行一次任務,當第二次執行任務時就會丟擲異常。executeOnExecutor方法一開始就檢查AsyncTask的狀態是不是PENDING,只有PENDING狀態才往下執行,如果是其他狀態表明現在正在執行另一個已有的任務或者已經執行完成了一個任務,這種情況下都會丟擲異常。
然後,
執行了onPreExecute()方法,這個是要在UI執行緒執行的,所以execute方法要在UI執行緒中執行。
最後,
執行緒池執行任務。
這裡多了兩個物件,mWorker和mFuture
private final WorkerRunnable<Params, Result> mWorker;
private final FutureTask<Result> mFuture;
WorkerRunnable表示一個任務,實現了Callable介面
private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
Params[] mParams;
}
FutureTask實現了future接口裡的方法,所以可以用來獲取任務的執行情況,同時它也是一個Runnable物件,可以作為任務提交給執行緒池。
public class FutureTask<V> implements RunnableFuture<V> {}
public interface RunnableFuture<V> extends Runnable, Future<V> {}
接著看下這兩個物件的初始化:
public AsyncTask() {
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
mTaskInvoked.set(true);
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
return postResult(doInBackground(mParams));
}
};
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 occured while executing doInBackground()",
e.getCause());
} catch (CancellationException e) {
postResultIfNotInvoked(null);
}
}
};
}
第一, 這兩個物件是在AsyncTask的構造器裡初始化的;
第二, mWorker作為引數例項化了mFuture;
第三, mWorker的call方法是線上程池的某個執行緒中執行的,而不是執行在主執行緒中。線上程池的工作執行緒中執行doInBackground方法,執行實際的任務,並返回結果。當doInBackground執行完畢後,將執行完的結果傳遞給postResult方法。postResult方法我們後面會再講解。
第四, call方法執行,mTaskInvoked標誌位被置為true,標識當前任務是真正開始執行了
private final AtomicBoolean mTaskInvoked = new AtomicBoolean();
第五, FutureTask的任務執行完成或任務取消的時候會執行FutureTask的done方法。done方法裡面的邏輯我們稍後再將。
第六, 在初始化構造器之前會先初始化靜態塊和例項塊,比如這個:
//用於通過Handler釋出result的Message Code
private static final int MESSAGE_POST_RESULT = 0x1;
//用於通過Handler釋出progress的Message Code
private static final int MESSAGE_POST_PROGRESS = 0x2;
//這個Handler綁定了主執行緒的Looper,用於向主執行緒傳遞訊息
private static final InternalHandler sHandler = new InternalHandler();
private static class InternalHandler extends Handler {
@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;
}
}
}
因為這個Handler要繫結主執行緒的Looper,要用於向主執行緒傳遞訊息
所以,構造AsyncTask的例項也要在UI執行緒中執行。
當mWorker的call方法執行到最後會將執行完的結果傳遞給postResult方法
private Result postResult(Result result) {
@SuppressWarnings("unchecked")
Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
new AsyncTaskResult<Result>(this, result));
message.sendToTarget();
return result;
}
此處將doInBackground返回的result封裝成了AsyncTaskResult物件
private static class AsyncTaskResult<Data> {
final AsyncTask mTask;
final Data[] mData;
AsyncTaskResult(AsyncTask task, Data... data) {
mTask = task;
mData = data;
}
}
在postResult這個方法裡通過Handler將結果傳到了UI執行緒,
//釋出最後結果
case MESSAGE_POST_RESULT:
// There is only one result
result.mTask.finish(result.mData[0]);
break;
private void finish(Result result) {
if (isCancelled()) {
onCancelled(result);
} else {
onPostExecute(result);
}
mStatus = Status.FINISHED;
}
如果任務被取消,執行onCancelled方法,否則執行onPostExecute方法。這兩個方法是實現類可以選擇去實現的方法。
protected void onCancelled() {
}
protected void onPostExecute(Result result) {
}
那怎麼判斷一個任務是否被取消?
private final AtomicBoolean mCancelled = new AtomicBoolean();
mCancelled標識當前任務是否被取消了
我們可以主動去cancel一個任務,
public final boolean cancel(boolean mayInterruptIfRunning) {
mCancelled.set(true);
return mFuture.cancel(mayInterruptIfRunning);
}
也可以判斷一個任務是否被取消
public final boolean isCancelled() {
return mCancelled.get();
}
我們知道,doInBackground方法是在工作執行緒中執行比較耗時的操作,這個操作時間可能比較長,而我們的任務有可能分成多個部分,每當我完成其中的一部分任務時,我們可以在doInBackground中多次呼叫AsyncTask的publishProgress方法,將階段性資料釋出出去。
protected final void publishProgress(Progress... values) {
if (!isCancelled()) {
sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
new AsyncTaskResult<Progress>(this, values)).sendToTarget();
}
}
通過Handler傳到UI執行緒去做更新操作
//釋出階段性處理結果
case MESSAGE_POST_PROGRESS:
result.mTask.onProgressUpdate(result.mData);
break;
前面提到,FutureTask的任務執行完成或任務取消的時候會執行FutureTask的done方法,
protected void done() {
try {
//任務正常執行完成
postResultIfNotInvoked(get());
} catch (InterruptedException e) {
android.util.Log.w(LOG_TAG, e);
} catch (ExecutionException e) {
throw new RuntimeException("An error occured 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);
}
}
如果AsyncTask正常執行完成的時候,call方法都執行完了,mTaskInvoked設定為true,並且在call方法中最後執行了postResult方法,然後進入mFuture的done方法,然後進入postResultIfNotInvoked方法,由於mTaskInvoked已經執行,所以不會再執行postResult方法。
如果在呼叫了AsyncTask的execute方法後立馬就執行了AsyncTask的cancel方法(實際執行mFuture的cancel方法),那麼會執行done方法,且捕獲到CancellationException異常,從而執行語句postResultIfNotInvoked(null),由於此時還沒有來得及執行mWorker的call方法,所以mTaskInvoked還未false,這樣就可以把null傳遞給postResult方法。
最後附上AsyncTask的原始碼:
值得一提的是這個靜態方法:
public static void execute(Runnable runnable) {
sDefaultExecutor.execute(runnable);
}
所以可以直接向AsyncTask.execute(runnable)新增子任務序列執行。
原始碼:
public abstract class AsyncTask<Params, Progress, Result> {
private static final String LOG_TAG = "AsyncTask";
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE = 1;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
private static final BlockingQueue<Runnable> sPoolWorkQueue =
new LinkedBlockingQueue<Runnable>(128);
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static final int MESSAGE_POST_RESULT = 0x1;
private static final int MESSAGE_POST_PROGRESS = 0x2;
private static final InternalHandler sHandler = new InternalHandler();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
private final WorkerRunnable<Params, Result> mWorker;
private final FutureTask<Result> mFuture;
private volatile Status mStatus = Status.PENDING;
private final AtomicBoolean mCancelled = new AtomicBoolean();
private final AtomicBoolean mTaskInvoked = new AtomicBoolean();
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);
}
}
}
public enum Status {
PENDING,
RUNNING,
FINISHED,
}
/** @hide Used to force static handler to be created. */
public static void init() {
sHandler.getLooper();
}
/** @hide */
public static void setDefaultExecutor(Executor exec) {
sDefaultExecutor = exec;
}
public AsyncTask() {
mWorker = new WorkerRunnable<Params, Result>() {
public Result call() throws Exception {
mTaskInvoked.set(true);
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
return postResult(doInBackground(mParams));
}
};
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 occured while executing doInBackground()",
e.getCause());
} catch (CancellationException e) {
postResultIfNotInvoked(null);
}
}
};
}
private void postResultIfNotInvoked(Result result) {
final boolean wasTaskInvoked = mTaskInvoked.get();
if (!wasTaskInvoked) {
postResult(result);
}
}
private Result postResult(Result result) {
@SuppressWarnings("unchecked")
Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
new AsyncTaskResult<Result>(this, result));
message.sendToTarget();
return result;
}
public final Status getStatus() {
return mStatus;
}
protected abstract Result doInBackground(Params... params);
protected void onPreExecute() {
}
@SuppressWarnings({"UnusedDeclaration"})
protected void onPostExecute(Result result) {
}
@SuppressWarnings({"UnusedDeclaration"})
protected void onProgressUpdate(Progress... values) {
}
@SuppressWarnings({"UnusedParameters"})
protected void onCancelled(Result result) {
onCancelled();
}
protected void onCancelled() {
}
public final boolean isCancelled() {
return mCancelled.get();
}
public final boolean cancel(boolean mayInterruptIfRunning) {
mCancelled.set(true);
return mFuture.cancel(mayInterruptIfRunning);
}
public final Result get() throws InterruptedException, ExecutionException {
return mFuture.get();
}
public final Result get(long timeout, TimeUnit unit) throws InterruptedException,
ExecutionException, TimeoutException {
return mFuture.get(timeout, unit);
}
public final AsyncTask<Params, Progress, Result> execute(Params... params) {
return executeOnExecutor(sDefaultExecutor, params);
}
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();
mWorker.mParams = params;
exec.execute(mFuture);
return this;
}
public static void execute(Runnable runnable) {
sDefaultExecutor.execute(runnable);
}
protected final void publishProgress(Progress... values) {
if (!isCancelled()) {
sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
new AsyncTaskResult<Progress>(this, values)).sendToTarget();
}
}
private void finish(Result result) {
if (isCancelled()) {
onCancelled(result);
} else {
onPostExecute(result);
}
mStatus = Status.FINISHED;
}
private static class InternalHandler extends Handler {
@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;
}
}
}
private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
Params[] mParams;
}
@SuppressWarnings({"RawUseOfParameterizedType"})
private static class AsyncTaskResult<Data> {
final AsyncTask mTask;
final Data[] mData;
AsyncTaskResult(AsyncTask task, Data... data) {
mTask = task;
mData = data;
}
}
}