AsyncTask是android中一个非常好用的异步执行工具类。

AsyncTask的应用

AsyncTask enables proper and easy use of the UI thread. 这个类允许执行后台操作并在UI线程中发布结果,而却不需要管理threads和/或handlers。

AsyncTask被设计为一个围绕着ThreadHandler的辅助类,而不是一个通用的线程框架。AsyncTask理想的应用场景应该是耗时短的操作(最多几秒钟)。如果你需要使线程长时间的运行,则强烈建议你使用java.util.concurrent包提供的各种APIs,比如ExecutorThreadPoolExecutorFutureTask

一个异步任务由一个在后台线程中执行的计算来定义,而其结果将在UI线程中发布。一个异步任务由3种泛型,称为ParamsProgressResult,和4个步骤,称为onPreExecutedoInBackgroundonProgressUpdateonPostExecute,来定义。

开发者指南

更多关于使用tasks和threads的信息,可以阅读Processes和Threads开发者指南。

使用

必须通过继承来使用AsyncTask。子类将覆写至少一个方法(doInBackground(Params...)),而大多数情况下将覆写第二个(onPostExecute(Result))

这里是一个继承的例子:

privateclassDownloadFilesTaskextendsAsyncTask<URL,Integer,Long>{protectedLongdoInBackground(URL...urls){intcount=urls.length;longtotalSize=0;for(inti=0;i<count;i++){totalSize+=Downloader.downloadFile(urls[i]);publishProgress((int)((i/(float)count)*100));//Escapeearlyifcancel()iscalledif(isCancelled())break;}returntotalSize;}protectedvoidonProgressUpdate(Integer...progress){setProgressPercent(progress[0]);}protectedvoidonPostExecute(Longresult){showDialog("Downloaded"+result+"bytes");}}

建之后,一个task的执行则非常简单:

newDownloadFilesTask().execute(url1,url2,url3);

AsyncTask的泛型

一个异步任务使用的三种类型如下:

  1. Params,任务执行时发送给它的参数的类型

  2. Progress,后台计算过程中发布的进度单元的类型。

  3. Result,后台计算的结果的烈性。

一个异步任务不总是会用到所有的类型。要标记一个类型未被使用,则简单的使用Void

privateclassMyTaskextendsAsyncTask<Void,Void,Void>{...}

4个步骤

当一个异步任务被执行时,则它会历经4个步骤:

  1. onPreExecute(),任务被执行之前在UI线程中调用。这个步骤通常被用来设置任务,比如在UI中显示一个进度条。

  2. doInBackground(Params...)onPreExecute()结束执行之后,这个方法会立即在后台线程中被调用。这个步骤用于执行可能需要比较长时间的后台的计算。异步任务的参数会被传递给这个步骤。计算的结果必须由这个步骤返回,并将会被传回给最后的步骤。这一步中也可以使用publishProgress(Progress...)来发布一个或多个的进度单元。这些值在UI线程中发布,在onProgressUpdate(Progress...)一步中。

  3. onProgressUpdate(Progress...), 调用了一次publishProgress(Progress...)之后,这个方法将在UI线程中被调用。执行的时间则未定义。这个方法被用于在后台计算仍在执行的过程中,在用户接口中显示任何形式的进度。比如,它可被用于在一个进度条中显示动画,或在一个text域中显示logs。

  4. onPostExecute(Result),后台计算结束以后在UI线程中调用。后台计算的结果会作为一个参数传给这个步骤。

取消一个任务

可以在任何时间通过调用cancel(boolean)来取消一个任务。调用这个方法将导致后续对于isCancelled()的调用返回true。调用这个方法之后,在doInBackground(Object[])返回之后,则onCancelled(Object)将会被调用而不是onPostExecute(Object)。为了确保一个任务尽快被取消,只要可能(比如在一个循环中),你就应该总是在doInBackground(Object[])中周期性的检查isCancelled()的返回值。

线程规则

要使这个类能适当的运行,有一些线程规则必须遵循:

  • AsyncTask类必须在UI线程加载。自JELLY_BEAN起将自动完成。

  • task实例必须在UI线程创建。

  • execute(Params...)必须在UI线程中调用。.

  • 不要手动地调用onPreExecute()onPostExecute(Result)doInBackground(Params...)onProgressUpdate(Progress...)

  • task只能被执行一次(如果尝试再次执行的话将会有一个exception跑出来)。

内存可观察性

AsyncTask保证所有的回调调用以这样的方式同步,下面的操作在没有显式同步的情况下也是安全的:

  • .在构造函数中或onPreExecute()中设置成员变量,并在doInBackground(Params...)中引用它们。

  • doInBackground(Params...)中设置成员变量,并在onProgressUpdate(Progress...)onPostExecute(Result)中引用它们。

执行顺序

当第一次被引入时,AsyncTasks是在一个单独的后台线程中串行执行的。自DONUT,开始,这被改为了一个线程池,从而允许多个任务并行的执行。自HONEYCOMB开始,任务被执行在一个单独的线程上来避免并发执行所导致的常见的应用错误。

如果你真的想要并发执行,你可以以THREAD_POOL_EXECUTOR调用executeOnExecutor(java.util.concurrent.Executor, Object[])

AsyncTask的实现

上面AsyncTask的应用的部分,译自Google提供的关于AsyncTask的官方文档。关于AsnycTask在使用时候的注意事项,Google提供的文档可以说覆盖的已经是比较全面了。可是,我们在使用AsyncTask时,为什么一定要按照Google的文档中描述的那样来做呢?这就需要通过研究AsyncTask的实现来解答了。

任务的创建和启动

我们在利用AsnycTask的子类来执行异步任务时,所做的操作通常是new一个对象,执行它的execute()方法,然后就等着任务执行完成就可以了。但是,创建对象的过程,即任务的构造过程,又都做了些什么事情呢?我们的task又是如何被提交并执行起来的呢?这就需要来看下AsyncTask中,任务启动执行部分代码的实现了(android code的分析基于4.4.2_r1)。

先来看AsyncTask的构造,也就是任务的创建:

215privatefinalWorkerRunnable<Params,Result>mWorker;216privatefinalFutureTask<Result>mFuture;221privatefinalAtomicBooleanmTaskInvoked=newAtomicBoolean();278/**279*Createsanewasynchronoustask.ThisconstructormustbeinvokedontheUIthread.280*/281publicAsyncTask(){282mWorker=newWorkerRunnable<Params,Result>(){283publicResultcall()throwsException{284mTaskInvoked.set(true);285286Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);287//noinspectionunchecked288returnpostResult(doInBackground(mParams));289}290};291292mFuture=newFutureTask<Result>(mWorker){293@Override294protectedvoiddone(){295try{296postResultIfNotInvoked(get());297}catch(InterruptedExceptione){298android.util.Log.w(LOG_TAG,e);299}catch(ExecutionExceptione){300thrownewRuntimeException("AnerroroccuredwhileexecutingdoInBackground()",301e.getCause());302}catch(CancellationExceptione){303postResultIfNotInvoked(null);304}305}306};307}308309privatevoidpostResultIfNotInvoked(Resultresult){310finalbooleanwasTaskInvoked=mTaskInvoked.get();311if(!wasTaskInvoked){312postResult(result);313}314}315316privateResultpostResult(Resultresult){317@SuppressWarnings("unchecked")318Messagemessage=sHandler.obtainMessage(MESSAGE_POST_RESULT,319newAsyncTaskResult<Result>(this,result));320message.sendToTarget();321returnresult;322}654privatestaticabstractclassWorkerRunnable<Params,Result>implementsCallable<Result>{655Params[]mParams;656}

可以看到这段code所做的事情。首先是创建了一个Callable,也就是上面的那个WorkerRunnable,其call()所做的事情正是调用我们实现的AsyncTask子类中所覆写的doInBackground()方法,并在其执行结束后将其执行的结果post出来。然后利用前面创建的Callable对象mWorker创建一个FutureTask。由此我们不难理解,AsyncTask的task,其本质是doInBackground(),但是是借助于FutureTask来表述的,以期能够方便的与执行框架衔接。

我们的task创建好了,那这个task又是如何提交并被启动执行的呢?我们接着来看execute()的实现:

218privatevolatileStatusmStatus=Status.PENDING;506/**507*Executesthetaskwiththespecifiedparameters.Thetaskreturns508*itself(this)sothatthecallercankeepareferencetoit.509*510*<p>Note:thisfunctionschedulesthetaskonaqueueforasinglebackground511*threadorpoolofthreadsdependingontheplatformversion.Whenfirst512*introduced,AsyncTaskswereexecutedseriallyonasinglebackgroundthread.513*Startingwith{@linkandroid.os.Build.VERSION_CODES#DONUT},thiswaschanged514*toapoolofthreadsallowingmultipletaskstooperateinparallel.Starting515*{@linkandroid.os.Build.VERSION_CODES#HONEYCOMB},tasksarebacktobeing516*executedonasinglethreadtoavoidcommonapplicationerrorscaused517*byparallelexecution.Ifyoutrulywantparallelexecution,youcanuse518*the{@link#executeOnExecutor}versionofthismethod519*with{@link#THREAD_POOL_EXECUTOR};however,seecommentarythereforwarnings520*onitsuse.521*522*<p>ThismethodmustbeinvokedontheUIthread.523*524*@paramparamsTheparametersofthetask.525*526*@returnThisinstanceofAsyncTask.527*528*@throwsIllegalStateExceptionIf{@link#getStatus()}returnseither529*{@linkAsyncTask.Status#RUNNING}or{@linkAsyncTask.Status#FINISHED}.530*531*@see#executeOnExecutor(java.util.concurrent.Executor,Object[])532*@see#execute(Runnable)533*/534publicfinalAsyncTask<Params,Progress,Result>execute(Params...params){535returnexecuteOnExecutor(sDefaultExecutor,params);536}538/**539*Executesthetaskwiththespecifiedparameters.Thetaskreturns540*itself(this)sothatthecallercankeepareferencetoit.541*542*<p>Thismethodistypicallyusedwith{@link#THREAD_POOL_EXECUTOR}to543*allowmultipletaskstoruninparallelonapoolofthreadsmanagedby544*AsyncTask,howeveryoucanalsouseyourown{@linkExecutor}forcustom545*behavior.546*547*<p><em>Warning:</em>Allowingmultipletaskstoruninparallelfrom548*athreadpoolisgenerally<em>not</em>whatonewants,becausetheorder549*oftheiroperationisnotdefined.Forexample,ifthesetasksareused550*tomodifyanystateincommon(suchaswritingafileduetoabuttonclick),551*therearenoguaranteesontheorderofthemodifications.552*Withoutcarefulworkitispossibleinrarecasesforthenewerversion553*ofthedatatobeover-writtenbyanolderone,leadingtoobscuredata554*lossandstabilityissues.Suchchangesarebest555*executedinserial;toguaranteesuchworkisserializedregardlessof556*platformversionyoucanusethisfunctionwith{@link#SERIAL_EXECUTOR}.557*558*<p>ThismethodmustbeinvokedontheUIthread.559*560*@paramexecTheexecutortouse.{@link#THREAD_POOL_EXECUTOR}isavailableasa561*convenientprocess-widethreadpoolfortasksthatarelooselycoupled.562*@paramparamsTheparametersofthetask.563*564*@returnThisinstanceofAsyncTask.565*566*@throwsIllegalStateExceptionIf{@link#getStatus()}returnseither567*{@linkAsyncTask.Status#RUNNING}or{@linkAsyncTask.Status#FINISHED}.568*569*@see#execute(Object[])570*/571publicfinalAsyncTask<Params,Progress,Result>executeOnExecutor(Executorexec,572Params...params){573if(mStatus!=Status.PENDING){574switch(mStatus){575caseRUNNING:576thrownewIllegalStateException("Cannotexecutetask:"577+"thetaskisalreadyrunning.");578caseFINISHED:579thrownewIllegalStateException("Cannotexecutetask:"580+"thetaskhasalreadybeenexecuted"581+"(ataskcanbeexecutedonlyonce)");582}583}584585mStatus=Status.RUNNING;586587onPreExecute();588589mWorker.mParams=params;590exec.execute(mFuture);591592returnthis;593}

execute()是直接调用了executeOnExecutor(),参数为一个Executor和传进来的参数。而在executeOnExecutor()中,是首先检查了当前的这个AsyncTask的状态,如果不是Status.PENDING的话,则会抛出Exception出来,即是说,一个AsyncTask只能被执行一次。然后将状态设为Status.RUNNING。接着调用AsyncTask子类覆写的onPreExecute()方法。前面Google官方文档中4个步骤部分提到onPreExecute()这个步骤,这个方法在任务执行之前在UI线程中调用,这是由于我们在UI线程中执行execute()提交或启动任务时,会直接调用到这个方法(execute()->executeOnExecutor()->onPreExecute())。接着是将传进来的参数params赋给mWorker的mParams,至此才算是为任务的执行完全准备好了条件。万事俱备,只欠东风,准备好的任务只等提交执行了。接着便是将任务提交给传进来的Executor来执行。

那Executor又是如何来执行我们的Task的呢?我们来看AsyncTask中的Excutor。在execute()中是使用了sDefaultExecutor来执行我们的任务:

185privatestaticfinalThreadFactorysThreadFactory=newThreadFactory(){186privatefinalAtomicIntegermCount=newAtomicInteger(1);187188publicThreadnewThread(Runnabler){189returnnewThread(r,"AsyncTask#"+mCount.getAndIncrement());190}191};192193privatestaticfinalBlockingQueue<Runnable>sPoolWorkQueue=194newLinkedBlockingQueue<Runnable>(128);195196/**197*An{@linkExecutor}thatcanbeusedtoexecutetasksinparallel.198*/199publicstaticfinalExecutorTHREAD_POOL_EXECUTOR200=newThreadPoolExecutor(CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,KEEP_ALIVE,201TimeUnit.SECONDS,sPoolWorkQueue,sThreadFactory);207publicstaticfinalExecutorSERIAL_EXECUTOR=newSerialExecutor();214privatestaticvolatileExecutorsDefaultExecutor=SERIAL_EXECUTOR;223privatestaticclassSerialExecutorimplementsExecutor{224finalArrayDeque<Runnable>mTasks=newArrayDeque<Runnable>();225RunnablemActive;226227publicsynchronizedvoidexecute(finalRunnabler){228mTasks.offer(newRunnable(){229publicvoidrun(){230try{231r.run();232}finally{233scheduleNext();234}235}236});237if(mActive==null){238scheduleNext();239}240}241242protectedsynchronizedvoidscheduleNext(){243if((mActive=mTasks.poll())!=null){244THREAD_POOL_EXECUTOR.execute(mActive);245}246}247}

sDefaultExecutor在默认情况下是SERIAL_EXECUTOR,而后者则是一个SerialExecutor对象。在SerialExecutor的execute()中其实只是创建了一个匿名Runnable,也就是我们的task,并将它提交进一个任务队列mTasks中,如果mActive为null,则意味着这是进程所执行的第一个AsnycTask,则将会调用scheduleNext()来第一次将一个task提交进一个线程池executor,来在后台执行我们的任务。

Google的官方文档中提到,AsyncTask任务是串行执行的。AsyncTask任务的串行执行,不是指这些任务是在一个SingleThreadExecutor上执行的(其实任务是在一个可以执行并发操作的thread pool executor中执行的),只是这些任务提交给THREAD_POOL_EXECUTOR是串行的。只有当一个任务执行结束之后,才会有另外的一个任务被提交上去。这种任务的串行提交,是在SerialExecutor的execute()方法里那个匿名Runnable实现的,我们可以看到它是在执行完了一个task之后,才会去schedule另一个task的。

生产者线程可以看作是UI主线程,它会向任务队列中提交任务,而消费者则是执行了某个任务的THREAD_POOL_EXECUTOR中的某个线程。某个时刻,最多只有一个生产者,也最多只有一个消费者。

捋一捋我们的那个用户任务doInBackground()到此为止被包了多少层了。先是被包进一个Callable (mWorker :WorkerRunnable)mWorker又被包进了一个FutureTask (mFuture)中,而mFuture在这个地方则又被包进了一个匿名的Runnable中,然后我们的task才会真正的被提交给THREAD_POOL_EXECUTOR而等待执行。我们的doInBackground()执行时,其调用栈将有点类似于 doInBackground()<-mWorker.call()<-mFuture.run()<-匿名Runnable的run()<-...。

AsyncTask与UI线程的交互

AsyncTask可以很方便的与UI线程进行交互,它可以方便地向UI线程中publish任务执行的进度和任务执行的结果。但这种交互又是如何实现的呢?AsyncTask中有提到,在doInBackground()中,可以通过调用publishProgress(Progress...)来发布一个或多个的进度单元,而这些值将在UI线程中发布,在onProgressUpdate(Progress...)一步中。那我们就具体来看一下publishProgress()的实现:

212privatestaticfinalInternalHandlersHandler=newInternalHandler();607/**608*Thismethodcanbeinvokedfrom{@link#doInBackground}to609*publishupdatesontheUIthreadwhilethebackgroundcomputationis610*stillrunning.Eachcalltothismethodwilltriggertheexecutionof611*{@link#onProgressUpdate}ontheUIthread.612*613*{@link#onProgressUpdate}willnotebecalledifthetaskhasbeen614*canceled.615*616*@paramvaluesTheprogressvaluestoupdatetheUIwith.617*618*@see#onProgressUpdate619*@see#doInBackground620*/621protectedfinalvoidpublishProgress(Progress...values){622if(!isCancelled()){623sHandler.obtainMessage(MESSAGE_POST_PROGRESS,624newAsyncTaskResult<Progress>(this,values)).sendToTarget();625}626}637privatestaticclassInternalHandlerextendsHandler{638@SuppressWarnings({"unchecked","RawUseOfParameterizedType"})639@Override640publicvoidhandleMessage(Messagemsg){641AsyncTaskResultresult=(AsyncTaskResult)msg.obj;642switch(msg.what){643caseMESSAGE_POST_RESULT:644//Thereisonlyoneresult645result.mTask.finish(result.mData[0]);646break;647caseMESSAGE_POST_PROGRESS:648result.mTask.onProgressUpdate(result.mData);649break;650}651}652}

所谓的与UI线程交互,其实也就是向一个Handler,即sHandler中发送消息。对于publishProgress(Progress...)的case,即是发送类型为MESSAGE_POST_PROGRESS的消息。随后消息会在UI线程中被处理,从而实现了将信息由后台线程传递至前台线程的目的。此处的sHandler是一个static的InternalHandler,也即是说,当AsyncTask类被加载起来的时候,这个对象就会被创建。我们知道,Handler其实是向一个Looper中发送消息。这个Looper隶属于某一个Looper thread,比如UI线程。我们可以在创建一个Handler时为它指定一个Looper,当然也可以不指定。如果不指定的话,那Handler关联的Looper就会是创建Handler的线程的Looper。由此则我们不难理解,为什麽Google的文档会强调,AsyncTask类必须在UI线程加载,因为sHandler所关联的Looper将会是VM中第一次访问AsyncTask的线程的Looper。也即是说,VM中第一个访问AsyncTask的线程,也将决定后续创建的所有的AsyncTask,它发布进度及结果的目标线程。实际上,由于后台线程与之交互的线程和后台线程通信是通过handler来完成的,则大概只要保证加载AsyncTask的线程是一个HandlerThread就可以了,只不过在这种情况下,创建的所有的AsyncTask都将会把进度和结果publish到那个HandlerThread中了。

不过话说回来了,是否真的有可能AsyncTask与之交互的线程只是一个普通的HandlerThread而不是main UI线程呢?答案当然是否定的。来看下面的这么一段code:

if(sMainThreadHandler==null){sMainThreadHandler=thread.getHandler();}AsyncTask.init();

这段code来自于frameworks/base/core/java/android/app/ActivityThread.java的main()方法中。可见Google也是早就料到了其中的风险,并已经有在此做防范,以确保AsyncTask中的static的Handler一定是在主线程创建的。

InternalHandler.handleMessage()中对于MESSAGE_POST_PROGRESS,相信我们也就不难理解所谓的步骤onProgressUpdate(Progress...)是什么了。

看完了后台线程向UI线程发布进度的实现之后,我们再来看后台线程向UI线程中发布结果的实现:

316privateResultpostResult(Resultresult){317@SuppressWarnings("unchecked")318Messagemessage=sHandler.obtainMessage(MESSAGE_POST_RESULT,319newAsyncTaskResult<Result>(this,result));320message.sendToTarget();321returnresult;322}

同样是向sHandler中send message,只不过message的类型为了MESSAGE_POST_RESULT。这段code也不需要做过多解释。在MESSAGE_POST_RESULT的处理部分,我们看到了4个步骤中的另一个步骤onPostExecute():

628privatevoidfinish(Resultresult){629if(isCancelled()){630onCancelled(result);631}else{632onPostExecute(result);633}634mStatus=Status.FINISHED;635}

也就是在任务结束之后,通知UI线程的部分。

并行执行AsyncTask及THREAD_POOL_EXECUTOR

AsyncTask不只能像上面说的那样串行执行,它们同样可以并行执行。即以THREAD_POOL_EXECUTOR为参数调用executeOnExecutor(java.util.concurrent.Executor, Object[])。比如:

newDownloadFilesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,url1,url2,url3);

那THREAD_POOL_EXECUTOR本身又都有些什么样的限制呢?接下来我们来看THREAD_POOL_EXECUTOR的创建:

180privatestaticfinalintCPU_COUNT=Runtime.getRuntime().availableProcessors();181privatestaticfinalintCORE_POOL_SIZE=CPU_COUNT+1;182privatestaticfinalintMAXIMUM_POOL_SIZE=CPU_COUNT*2+1;183privatestaticfinalintKEEP_ALIVE=1;184185privatestaticfinalThreadFactorysThreadFactory=newThreadFactory(){186privatefinalAtomicIntegermCount=newAtomicInteger(1);187188publicThreadnewThread(Runnabler){189returnnewThread(r,"AsyncTask#"+mCount.getAndIncrement());190}191};192193privatestaticfinalBlockingQueue<Runnable>sPoolWorkQueue=194newLinkedBlockingQueue<Runnable>(128);195196/**197*An{@linkExecutor}thatcanbeusedtoexecutetasksinparallel.198*/199publicstaticfinalExecutorTHREAD_POOL_EXECUTOR200=newThreadPoolExecutor(CORE_POOL_SIZE,MAXIMUM_POOL_SIZE,KEEP_ALIVE,201TimeUnit.SECONDS,sPoolWorkQueue,sThreadFactory);

创建ThreadPoolExecutor传进去了一个容量为128的链接阻塞队列(sPoolWorkQueue)作为任务队列,这意味着在同一时间可以提交的最大的任务数量为128,既包括通过AsyncTask.execute()提交的任务,也包括通过AsyncTask.executeOnExecutor()提交的任务。线程池中线程的最小数量及最大数量则是与CPU数量相关的值CORE_POOL_SIZE和MAXIMUM_POOL_SIZE。KEEP_ALIVE则表示,当线程池中的线程数量超过了CORE_POOL_SIZE,那些空闲的线程能够存活的最长时间。sThreadFactory则用于维护线程池创建过的线程的数量,并给每个AsyncTask的线程创建一个适当的线程名。

Task的生命周期管理

声明周期管理,主要是指取消操作。

220privatefinalAtomicBooleanmCancelled=newAtomicBoolean();423/**424*Returns<tt>true</tt>ifthistaskwascancelledbeforeitcompleted425*normally.Ifyouarecalling{@link#cancel(boolean)}onthetask,426*thevaluereturnedbythismethodshouldbecheckedperiodicallyfrom427*{@link#doInBackground(Object[])}toendthetaskassoonaspossible.428*429*@return<tt>true</tt>iftaskwascancelledbeforeitcompleted430*431*@see#cancel(boolean)432*/433publicfinalbooleanisCancelled(){434returnmCancelled.get();435}466publicfinalbooleancancel(booleanmayInterruptIfRunning){467mCancelled.set(true);468returnmFuture.cancel(mayInterruptIfRunning);469}

AsyncTask用一个原子变量来描述,任务是否是被cancel的。而提交给线程池的任务的cancel则委托给FutureTask。我们知道,在Java中,对于线程的cancel只能是提示性的,而不是强制性的。要真正的使任务有好的生命周期管理,则还需要在doInBackground()中,在适当的时机检查任务是否被cancel掉,并进一步清理环境,退出任务执行。

Done。

更多相关文章

  1. 基于ActionbarActivity中Actionbar自定义布局
  2. Android(安卓)教程 翻译 1 Activities 活动
  3. android应用框架搭建------BaseActivity
  4. listview的使用----BaseAdapter
  5. IntentService源码解读
  6. Service启动之启动方式和绑定方式
  7. Android(安卓)调用H5界面(交互)
  8. APK应用LOG保存
  9. Android_Http交互

随机推荐

  1. Android—自制Android闹钟3步即可!(2020-6
  2. android 获取实时麦克风声音大小
  3. 《Android第一行代码》first reading 十
  4. Android(安卓)7.0 系统解决拍照的问题 ex
  5. Android Broadcast 用法简单讨论
  6. android studio 使用adb 命令传递文件到a
  7. android 一些常用控件属性设置
  8. Android自定义对话框的使用
  9. Android 进阶—— Android 系统安全机制
  10. Android Excel导入