參考
https://www.baidu.com/link?url=QNRznJEBT25k0bpgVD3bOniOia2W85eiPIWrS93YFknyrHoFDGrJVtoax2ZYpiiErtRW7VD-sNEgCRNespIhSK&wd=&eqid=9a27957100030dcd000000065aac77c0
https://www.jianshu.com/p/79cc3c5fc9a3
三流之路有沒有第二部,http://blog.csdn.net/majihua817/article/details/51658465
以下用法和源碼分析基于android-27(即版本android 8.1.0)
1.進程所有AsyncTask默認并不是并行執行的,通常默認的?使用execute執行?的AsyncTask是排隊按順序執行的,同一時間內只有一個AsyncTask會運行;
2.進程中使用executeOnExecutor(Executor exec,Params...params)根據傳入的線程池效果,可以實現同一時間多個AsyncTask異步執行的效果;
android源碼,3.常用的回調方法有:onPreExecute()、doInBackground(Params... params)、onProgressUpdate(Progress... values)、onPostExecute(Result result)
a. 執行順序為onPreExecute -> doInBackground--> (onProgressUpdate)->onPostExecute
b. onPreExecute: 運行在ui線程,當代碼執行AsyncTask.execute(Params...params)后會被調用,一般可用于界面上一些初始化操作,比如彈出下載對話框等;
c. doInBackground:運行在后臺非ui線程,用來處理耗時的任務,如下載、數據處理等;
Android 編譯、d. onProgressUpdate:運行在ui線程,可用來處理界面上的一些進度更新相關操作;在doInBackground中調用publishProgress后,可觸發onProgressUpdate;
e. 當然onPreExecute也可以使用publishProgress觸發onProgressUpdate,只是二者都是ui線程,所以并沒有實際意義;
f. 注意的是publishProgress也是使用的handler發送消息來觸發的onProgressUpdate,所以注意的是
doInBackground沒人為干預的話并不會因此堵塞等待onProgressUpdate執行完畢
g. onPostExecute:運行在ui線程,可用于界面提示操作完成,在doInBackground執行完使用return后調用;
安卓源碼。h.源碼中對這些重要回調方法都有對應的@MainThread、@WorkerThread線程注解,但這個注解目前主要用于IDE編譯檢查錯誤用的,實際上execute等依舊可以在子線程中調用,當然onPreExecute此時就不能處理ui界面,否則會拋出異常;但建議調用的地方和注解一致
4.執行過一個次或者正在執行的AsyncTask不能再次通過execute或者executeOnExecutor進行重復執行
5.使用executeOnExecutor并行執行的話,根據傳入的線程池的設置會有隊列大小的限制,通常直接使用AsyncTask中AsyncTask.THREAD_POOL_EXECUTOR這個線程池的話:
//cpu核數,假設4
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
//核心線程數,3
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
//最大同時執行的線程數9
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
//空閑線程超過30秒被回收
private static final int KEEP_ALIVE_SECONDS = 30;
//等待隊列允許的最大數量為128
private static final BlockingQueue<Runnable> sPoolWorkQueue =new LinkedBlockingQueue<Runnable>(128);
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,sPoolWorkQueue, sThreadFactory);
threadPoolExecutor.allowCoreThreadTimeOut(true);
THREAD_POOL_EXECUTOR = threadPoolExecutor;
(假設終端cpu數目為4)因為該線程池運行同時運行的最大線程數為9,等待隊列最大為128,如果每一個asynctask的doInBackground中耗時非常久,一次性添加超過9+128=137個task的話(比如listview中一次性創建),那么將會引起異常java.util.concurrent.RejectedExecutionException
中二之路。6.使用execute的話,因為有隊列緩存(沒大小限制,除非內存爆掉),串行執行,所以不存在線程池中任務超出大小的情況
7.AsyncTaskd的cancel方法只是簡單把標志位改為true,最后使用Thread.interrupt去中斷線程執行,但是這并不保證會馬上停止執行,所以可以在doInBackground中使用isCancelled來判斷任務是否被取消
要點解釋
(來自參考資料https://www.jianshu.com/p/79cc3c5fc9a3)
三流之路的女兒?在Android 1.5剛引入的時候,AsyncTask的execute是串行執行的;到了Android 1.6直到Android 2.3.2,又被修改為并行執行了,這個執行任務的線程池就是THREAD_POOL_EXECUTOR,因此在一個進程內,所有的AsyncTask都是并行執行的;但是在Android 3.0以后,如果你使用execute函數直接執行AsyncTask,那么這些任務是串行執行的。因此結論就來了:Android 3.0以上,AsyncTask默認并不是并行執行的。
由于同一個進程的AsyncTask都是在一個線程池中,如果默認是并行的,那么多個線程同時執行多個task中的doInBackground操作,而doInBackground又訪問修改相同的資源,這邊往往會忽略對資源的同步處理問題,經常會導致不可預料的錯誤結果,為此默認情況下使用execute執行的AsyncTask默認為串行執行;
如果處理好資源同步問題,可以使用executeOnExecutor傳入合適的線程池,來達到AsyncTask并行執行的效果;
優點:
簡述android源代碼編譯過程?缺點(參考http://blog.csdn.net/goodlixueyong/article/details/45895997):
1.需要注意串行還是并行執行,在1.5是串行,1.6-2.3.2是并行,3.0之后默認使用execute時串行,如果使用executeOnExecutor則是并行;
2.所謂的串行,實際上主要是針對doInBackground方法里面的操作而言的,串行時并不會等到task中onPostExecute執行完才觸發下一份asynctask,只要doInBackground執行完成就會促發下一個asynctask
3.可能導致內存泄露或者ui更新異常問題,如果是activity中非靜態內部asynctask子類,默認隱式持有一個activity的引用,在activity銷毀之前,注意要停止asynctask;不然后臺線程一直運行,導致了內存泄露,還可能在要更新ui時,該view已經不存在而導致異常;
android應用源碼、4.如果activity因為轉屏重建或者被后臺異常終結,之前運行的asynctask還會持有一個之前activity的引用,更新這個無效的引用上的view需要注意對view有效性的判斷
5.并行執行的時候,需要考慮下線程池能夠處理的任務大小
聲明MyAsyncTask繼承并實現AsyncTask相關回調:
public class MyAsyncTask extends AsyncTask<String, Integer, Boolean>{String TAG="MyAsyncTask";public MyAsyncTask(String tag) {super();this.TAG=tag;}//doInBackground開始之前調用,ui線程,可用于界面上的一些初始化操作,比如彈出下載對話框@Overrideprotected void onPreExecute() {Log.d(TAG,"onPreExecute "+Thread.currentThread());Toast.makeText(context,"onPreExecute",Toast.LENGTH_SHORT).show();}//用于執行后臺任務,非ui線程,用于處理耗時任務,下載,數據處理等@Overrideprotected Boolean doInBackground(String... params) {Log.d(TAG,"doInBackground "+Thread.currentThread());int count = params.length;for(int i=0;i<count;i++){//判斷任務是否取消,取消則停止,用戶調用AsyncTask.cancel()后設置該標志位//調用AsyncTask.cancel()并不能直接停止一個asynctask,// 需要在doInBackground/onProgressUpdate/onPreExecute手動判斷該標志位if(isCancelled()){return false}String p = params[i];Log.d(TAG,"doInBackground:"+p);publishProgress(i);}return true;}//當后臺任務doInBackground調用了publishProgress(Progress...)方法后,這個方法會被調用//用于執行處理過程中的進度更新之類,ui線程,可用于更新下載對話框的進度條@Overrideprotected void onProgressUpdate(Integer... values) {Log.d(TAG,"onProgressUpdate:"+values[0]+" "+Thread.currentThread());Toast.makeText(context,"onProgressUpdate:"+values[0],Toast.LENGTH_SHORT).show();}//當后臺任務doInBackground執行完畢并通過return語句進行返回時,這個方法就會被調用//ui線程,可用于界面更新處理完成@Overrideprotected void onPostExecute(Boolean aBoolean) {Log.d(TAG,"onPostExecute:"+aBoolean+" "+Thread.currentThread());Toast.makeText(context,"onPostExecute",Toast.LENGTH_SHORT).show();}//AsyncTaskd的cancel方法只是簡單把標志位改為true,最后使用Thread.interrupt去中斷線程執行,但是這并不保證會馬上停止執行,所以可以在doInBackground中使用isCancelled來判斷任務是否被取消//ui線程,可用于取消后關閉ui界面提示等@Overrideprotected void onCancelled(Boolean aBoolean) {Log.d(TAG,"onCancelled:"+aBoolean+" "+Thread.currentThread());}}
創建相關實例并執行AsyncTask:
測試一:普通使用
Log.d(TAG,"btn1 start");MyAsyncTask myAsyncTask =new MyAsyncTask("task1"); //創建AsyncTask子類實例myAsyncTask.execute("參數1","參數2"); //開始執行Log.d(TAG,"btn1 end");**日志輸出**:btn1 start onPreExecute Thread[main,5,main] //可以看見調用execute后,onPreExecute立馬執行btn1 enddoInBackground Thread[AsyncTask #1,5,main] doInBackground:參數1doInBackground:參數2//doInBackground調用publishProgress后,由handler觸發onProgressUpdate,與doInBackground為2個線程異步執行onProgressUpdate:0 Thread[main,5,main] onProgressUpdate:1 Thread[main,5,main]onPostExecute:true Thread[main,5,main] //全部操作完成后調用
測試二:多個asyncTask使用驗證串行執行
Log.d(TAG,"btn1 start");MyAsyncTask myAsyncTask =new MyAsyncTask("task1");MyAsyncTask myAsyncTask2 =new MyAsyncTask("task2");myAsyncTask.execute("參數1","參數2");myAsyncTask2.execute("參數1","參數2");Log.d(TAG,"btn1 end");日志輸出:btn1 start//可以看見各自調用execute后,task1,task2的onPreExecute立馬執行,互不影響task1: onPreExecute Thread[main,5,main] task2: onPreExecute Thread[main,5,main]btn1 endtask1: doInBackground Thread[AsyncTask #1,5,main] //doInBackground是按序執行的task1: doInBackground:參數1task1: doInBackground:參數2task2: doInBackground Thread[AsyncTask #2,5,main]task2: doInBackground:參數1task2: doInBackground:參數2//onProgressUpdate和onProgressUpdate也是按序執行的//這邊看到可能存在后續asyncTask執行doInBackground過程中,前面的task才剛觸發onProgressUpdate和onPostExecutetask1: onProgressUpdate:0 Thread[main,5,main] task1: onProgressUpdate:1 Thread[main,5,main] task1: onPostExecute:true Thread[main,5,main]task2: onProgressUpdate:0 Thread[main,5,main]task2: onProgressUpdate:1 Thread[main,5,main]task2: onPostExecute:true Thread[main,5,main]
測試三:并行執行:
MyAsyncTask myAsyncTask =new MyAsyncTask("task1");MyAsyncTask myAsyncTask2 =new MyAsyncTask("task2");myAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,"參數1","參數2");myAsyncTask2.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,"參數1","參數2");日志輸出(doInBackground 添加Thread.sleep方便模擬):btn1 starttask1: onPreExecute Thread[main,5,main]task2: onPreExecute Thread[main,5,main]//task1與task2的doInBackground部分交替執行task1: doInBackground Thread[AsyncTask #1,5,main]task1: doInBackground:參數1 task2: doInBackground Thread[AsyncTask #2,5,main]task2: doInBackground:參數1 btn1 endtask1: onProgressUpdate:0 Thread[main,5,main]task2: onProgressUpdate:0 Thread[main,5,main]task1: doInBackground:參數2task2: doInBackground:參數2task1: onProgressUpdate:1 Thread[main,5,main]task2: onProgressUpdate:1 Thread[main,5,main]task1: onPostExecute:true Thread[main,5,main]task2: onPostExecute:true Thread[main,5,main]
從測試中可以看出onPreExecute在各自AsyncTask調用execute后立即執行,后續源碼分析可知道onPreExecute不由handler觸發,否則可能與doInBackground執行時序有差別;
測試二中:
后續從源碼分析可以知道使用execute觸發的AsyncTask中的doInBackground部分實際上是在線程池中排隊執行的,所以只有一個任務的doInBackground執行完,才會獲取下一個任務的doInBackground進行執行;
從源碼分析可以知道onProgressUpdate、onPostExecute是通過handler消息控制的,本身也是按序執行的;多個AsyncTask的不同的生命周期步驟其實會有穿插;
從日志輸出和源碼原理可知,AsyncTask的按序執行相對來講有一定的不足,不會等待一個AsyncTask完整執行完后onPreExecute -> doInBackground--> (onProgressUpdate)->onPostExecute,后下一個AsyncTask才允許開始它的生命周期;
測試三中:
驗證了執行線程池并行執行的效果
AsyncTask開始執行工作的入口函數為AsyncTask.execute,所以從該方法入手查看
AsyncTask源碼
execute函數
@MainThread
public final AsyncTask<Params, Progress, Result> execute(Params... params) {return executeOnExecutor(sDefaultExecutor, params);
}注解上也表明該方法運行需要在ui線程運行,為什么需要呢?此為問題一;這邊使用execute觸發會使用默認的線程池SerialExecutor sDefaultExecutor,
該線程池實際上只起到一個隊列控制,保存task依次按順序執行;
假設需要并發執行AsyncTask,則可以直接使用executeOnExecutor方法
傳入合適的線程池就可以達到效果;接著查看調用的executeOnExecutor所執行的操作
executeOnExecutor函數
@MainThread
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狀態的判斷,如果該AsyncTask已經運行過或者正在運行則拋出異常,
每個AsyncTask只允許直接或者間接調用一次executeOnExecutor;
狀態判斷AsyncTask尚處于等待狀態Status.PENDING,則將該狀態標識為Status.RUNNING正在運行狀態等待后續線程池排隊執行;
注意,接著立即執行onPreExecute()方法,也就是用于初始化ui控件準備工作的方法;
這邊說明了前面2個問題:
mWorker.mParams = params;exec.execute(mFuture);
然后將傳入的params參數賦值給mWorker.mParams(params后續會傳遞給doInBackground進行處理),調用線程池exec處理mFuture;
這邊需要對mWorker、mFuture和線程池exec進行分析,首先對于exec可以看出傳入的是線程池SerialExecutor
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
對于線程池SerialExecutor
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// We want at least 2 threads and at most 4 threads in the core pool,
// preferring to have 1 less than the CPU count to avoid saturating
// the CPU with background work
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final int KEEP_ALIVE_SECONDS = 30;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());}
};//實際執行任務的線程池,注意是靜態的,所以AsyncTask共用
public static final Executor THREAD_POOL_EXECUTOR;
static {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,sPoolWorkQueue, sThreadFactory);threadPoolExecutor.allowCoreThreadTimeOut(true);THREAD_POOL_EXECUTOR = threadPoolExecutor;
}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(); //即調用 FutureTask mFuture.run()方法 } finally {scheduleNext();}}});if (mActive == null) {scheduleNext();}}protected synchronized void scheduleNext() {if ((mActive = mTasks.poll()) != null) {//THREAD_POOL_EXECUTOR為實際執行任務的線程池THREAD_POOL_EXECUTOR.execute(mActive);}}}
可以看出該SerialExecutor在被調用execute后,會將一個任務添加到隊列ArrayDeque中(即前面exec.execute(mFuture)),execute傳入的是實現了Runnable接口的類FutureTask的實例mFuture,SerialExecutor的主要作用實際上時維護任務的隊列,按順序取出任務交給實際執行任務的線程池THREAD_POOL_EXECUTOR
第一個執行時mActive == null所以會調用scheduleNext,(按先進先出)從隊列中拿出一個任務交給線程池THREAD_POOL_EXECUTOR進行執行:
public void run() {try {r.run(); //即調用 FutureTask mFuture.run()方法 } finally {scheduleNext(); //執行完畢后,獲取下一個任務進行處理}}
執行的即為傳入的mFuture.run()方法,執行完成后會再次調用scheduleNext獲取并執行下一個AsyncTask任務。
下面看一下FutureTask.run()方法所做的操作
FutureTask類
//FutureTask類構造函數,這邊傳入的實際上是實現了Callable接口的WorkerRunnable類對象mWorker
//mWorker與mFuture的關聯在AsyncTask創建時構造函數中完成
public FutureTask(Callable<V> callable) {if (callable == null)throw new NullPointerException();this.callable = callable;this.state = NEW; // ensure visibility of callable
}//線程池THREAD_POOL_EXECUTOR中執行的方法
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(); //調用的是mWorker的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 interruptsint s = state;if (s >= INTERRUPTING)handlePossibleCancellationInterrupt(s);}
}
由上述可知,線程池THREAD_POOL_EXECUTOR中執行的mFuture.run()方法中實際上會調用result = c.call()即實現了Callable接口的類的WorkerRunnable對象mWorker.call();
其中WorkerRunnable類對象mWorker、FutureTask類對象mFuture的創建和關聯位于AsyncTask構造函數中;
AsyncTask構造函數
//部分需要聯系使用的成員變量//注意是靜態的,類共享一個線程池private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;//注意是靜態的變量,類共享一個handlerprivate static InternalHandler sHandler; private final WorkerRunnable<Params, Result> mWorker;private final FutureTask<Result> mFuture;private final Handler mHandler;/*** Creates a new asynchronous task. This constructor must be invoked on the UI thread.*/public AsyncTask() {this((Looper) null);}/*** Creates a new asynchronous task. This constructor must be invoked on the UI thread.** @hide*/public AsyncTask(@Nullable Handler handler) {this(handler != null ? handler.getLooper() : null);}/*** Creates a new asynchronous task. This constructor must be invoked on the UI thread.** @hide*/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 uncheckedresult = doInBackground(mParams); //線程池中最終執行doInBackgroundBinder.flushPendingCommands();} catch (Throwable tr) {mCancelled.set(true);throw tr;} finally {postResult(result);}return result;}};mFuture = new FutureTask<Result>(mWorker) {@Overrideprotected 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);}}};}
由上可以看出默認構造函數會執行
public AsyncTask(@Nullable Looper callbackLooper),傳入參數為null,
此時mHandler = getMainHandler()獲取到一個主線程的handler;
創建了mWorker和mFuture對象,并通過mFuture = new FutureTask<Result>(mWorker)
完成mFuture與mWorker的一個關聯;
后續,線程池THREAD_POOL_EXECUTOR中執行的mFuture.run()會執行到mWorker.call()由上可看出:
try {Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);//noinspection uncheckedresult = doInBackground(mParams); //線程池中最終執行doInBackgroundBinder.flushPendingCommands();} catch (Throwable tr) {mCancelled.set(true);throw tr;} finally {postResult(result);}
觸發執行了doInBackground(mParams)后,通過postResult(result)將結果result通過hander發出最終在handler的ui線程中觸發了onPostExecute回調;
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發送處理結束的消息
getMainHandler函數
private static Handler getMainHandler() {synchronized (AsyncTask.class) {if (sHandler == null) {sHandler = new InternalHandler(Looper.getMainLooper());}return sHandler;}
}
該方法簡單的創建一個handler通過Looper.getMainLooper()使得該handler運行于ui線程,注意的是sHandler是靜態的,也就是說進程所有的AsyncTask共享一個handler進行處理消息;
InternalHandler類和finish方法
private static class InternalHandler extends Handler {public InternalHandler(Looper looper) {super(looper);}@SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})@Overridepublic void handleMessage(Message msg) {AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;switch (msg.what) {//接收消息并且在ui線程回調執行finish最終調用onPostExecute方法case MESSAGE_POST_RESULT:// There is only one resultresult.mTask.finish(result.mData[0]);break;//接收消息并且在ui線程回調執行onProgressUpdate方法case MESSAGE_POST_PROGRESS: result.mTask.onProgressUpdate(result.mData);break;}}}
該handler主要用于接收消息MESSAGE_POST_PROGRESS后在ui線程執行onProgressUpdate方法,
或者接收MESSAGE_POST_RESULT后,最終在ui線程執行onPostExecute/onCancelled方法,
AsyncTask中2個重要的方法onProgressUpdate、onPostExecute由該handler執行;
private void finish(Result result) {if (isCancelled()) {//任務執行完之后,如果用戶之前調用了AsyncTask.cancle()則觸發onCancelledonCancelled(result);} else {//否則觸發onPostExecuteonPostExecute(result); }mStatus = Status.FINISHED;}
SerialExecutor implements Executor : sDefaultExecutor
ThreadPoolExecutor : THREAD_POOL_EXECUTOR:
進程中對于使用execute開始的所有的AsyncTask順序排隊執行
mWorker = new WorkerRunnable<Params, Result>(){..}..mFuture = new FutureTask<Result>(mWorker) {..}..
3.使用execute的時候:
過程中如果調用publishProgress會通過handler觸發onProgressUpdate;
如果要并行執行asynctask則不使用execute而是直接使用executeOnExecutor傳遞給合適的線程池,跳過SerialExecutor的排隊步驟;
掃描關注微信公眾號:
?
版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。
工作时间:8:00-18:00
客服电话
电子邮件
admin@qq.com
扫码二维码
获取最新动态