在开发Android应用时必须遵守单线程模型的原则: Android UI操作并不是线程安全的并且这些操作必须在UI线程中执行。在单线程模型中始终要记住两条法则:
1. 不要阻塞UI线程
2. 确保只在UI线程中访问Android UI工具包
当一个程序第一次启动时,Android会同时启动一个对应的主线程(Main Thread),主线程主要负责处理与UI相关的事件,如:用户的按键事件,用户接触屏幕的事件以及屏幕绘图事件,并把相关的事件分发到对应的组件进行处理。所以主线程通常又被叫做UI线程。
比如说从网上获取一个网页,在一个TextView中将其源代码显示出来,这种涉及到网络操作的程序一般都是需要开一个线程完成网络访问,但是在获得页面源码后,是不能直接在网络操作线程中调用TextView.setText()的.因为其他线程中是不能直接访问主UI线程成员 。

android提供了几种在其他线程中访问UI线程的方法。
Activity.runOnUiThread( Runnable )
View.post( Runnable )
View.postDelayed( Runnable, long )
Hanlder
这些类或方法同样会使你的代码很复杂很难理解。然而当你需要实现一些很复杂的操作并需要频繁地更新UI时这会变得更糟糕。

为了解决这个问题,Android 1.5提供了一个工具类:AsyncTask,它使创建需要与用户界面交互的长时间运行的任务变得更简单。相对来说AsyncTask更轻量级一些,适用于简单的异步处理,不需要借助线程和Handler即可实现。
AsyncTask是抽象类.AsyncTask定义了三种泛型类型 Params,Progress和Result。
  Params 启动任务执行的输入参数,比如HTTP请求的URL。
  Progress 后台任务执行的百分比。
  Result 后台执行任务最终返回的结果,比如String。

AsyncTask的执行分为四个步骤,每一步都对应一个回调方法,这些方法不应该由应用程序调用,开发者需要做的就是实现这些方法。
  1) 子类化AsyncTask
  2) 实现AsyncTask中定义的下面一个或几个方法
   onPreExecute(), 该方法将在执行实际的后台操作前被UI thread调用。可以在该方法中做一些准备工作,如在界面上显示一个进度条。
   doInBackground(Params...), 将在onPreExecute 方法执行后马上执行,该方法运行在后台线程中。这里将主要负责执行那些很耗时的后台计算工作。可以调用 publishProgress方法来更新实时的任务进度。该方法是抽象方法,子类必须实现。
   onProgressUpdate(Progress...),在publishProgress方法被调用后,UI thread将调用这个方法从而在界面上展示任务的进展情况,例如通过一个进度条进行展示。
   onPostExecute(Result), 在doInBackground 执行完成后,onPostExecute 方法将被UI thread调用,后台的计算结果将通过该方法传递到UI thread.

为了正确的使用AsyncTask类,以下是几条必须遵守的准则:
  1) Task的实例必须在UI thread中创建
  2) execute方法必须在UI thread中调用
  3) 不要手动的调用onPreExecute(), onPostExecute(Result),doInBackground(Params...), onProgressUpdate(Progress...)这几个方法
  4) 该task只能被执行一次,否则多次调用时将会出现异常
doInBackground方法和onPostExecute的参数必须对应,这两个参数在AsyncTask声明的泛型参数列表中指定,第一个为doInBackground接受的参数,第二个为显示进度的参数,第第三个为doInBackground返回和onPostExecute传入的参数。

Android从1.5版本就引入了一个AsyncTask类,使用它就可以非常灵活方便地从子线程切换到UI线程,我们本篇文章的主角也就正是它了。

AsyncTask很早就出现在Android的API里了,所以我相信大多数朋友对它的用法都已经非常熟悉。不过今天我还是准备从AsyncTask的基本用法开始讲起,然后我们再来一起分析下AsyncTask源码,看看它是如何实现的,最后我会介绍一些关于AsyncTask你所不知道的秘密。

AsyncTask的基本用法

首先来看一下AsyncTask的基本用法,由于AsyncTask是一个抽象类,所以如果我们想使用它,就必须要创建一个子类去继承它。在继承时我们可以为AsyncTask类指定三个泛型参数,这三个参数的用途如下:

1. Params

在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。

2. Progress

后台任何执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。

3. Result

当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。

因此,一个最简单的自定义AsyncTask就可以写成如下方式:

[java]view plaincopy
  1. classDownloadTaskextendsAsyncTask<Void,Integer,Boolean>{
  2. ……
  3. }

这里我们把AsyncTask的第一个泛型参数指定为Void,表示在执行AsyncTask的时候不需要传入参数给后台任务。第二个泛型参数指定为Integer,表示使用整型数据来作为进度显示单位。第三个泛型参数指定为Boolean,则表示使用布尔型数据来反馈执行结果。

当然,目前我们自定义的DownloadTask还是一个空任务,并不能进行任何实际的操作,我们还需要去重写AsyncTask中的几个方法才能完成对任务的定制。经常需要去重写的方法有以下四个:

1. onPreExecute()

这个方法会在后台任务开始执行之间调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。

2. doInBackground(Params...)

这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成就可以通过return语句来将任务的执行结果进行返回,如果AsyncTask的第三个泛型参数指定的是Void,就可以不返回任务执行结果。注意,在这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用publishProgress(Progress...)方法来完成。

3. onProgressUpdate(Progress...)

当在后台任务中调用了publishProgress(Progress...)方法后,这个方法就很快会被调用,方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新。

4. onPostExecute(Result)

当后台任务执行完毕并通过return语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些UI操作,比如说提醒任务执行的结果,以及关闭掉进度条对话框等。

因此,一个比较完整的自定义AsyncTask就可以写成如下方式:

[java]view plaincopy
  1. classDownloadTaskextendsAsyncTask<Void,Integer,Boolean>{
  2. @Override
  3. protectedvoidonPreExecute(){
  4. progressDialog.show();
  5. }
  6. @Override
  7. protectedBooleandoInBackground(Void...params){
  8. try{
  9. while(true){
  10. intdownloadPercent=doDownload();
  11. publishProgress(downloadPercent);
  12. if(downloadPercent>=100){
  13. break;
  14. }
  15. }
  16. }catch(Exceptione){
  17. returnfalse;
  18. }
  19. returntrue;
  20. }
  21. @Override
  22. protectedvoidonProgressUpdate(Integer...values){
  23. progressDialog.setMessage("当前下载进度:"+values[0]+"%");
  24. }
  25. @Override
  26. protectedvoidonPostExecute(Booleanresult){
  27. progressDialog.dismiss();
  28. if(result){
  29. Toast.makeText(context,"下载成功",Toast.LENGTH_SHORT).show();
  30. }else{
  31. Toast.makeText(context,"下载失败",Toast.LENGTH_SHORT).show();
  32. }
  33. }
  34. }

这里我们模拟了一个下载任务,在doInBackground()方法中去执行具体的下载逻辑,在onProgressUpdate()方法中显示当前的下载进度,在onPostExecute()方法中来提示任务的执行结果。如果想要启动这个任务,只需要简单地调用以下代码即可:

[java]view plaincopy
  1. newDownloadTask().execute();

以上就是AsyncTask的基本用法,怎么样,是不是感觉在子线程和UI线程之间进行切换变得灵活了很多?我们并不需求去考虑什么异步消息处理机制,也不需要专门使用一个Handler来发送和接收消息,只需要调用一下publishProgress()方法就可以轻松地从子线程切换到UI线程了。

分析AsyncTask的源码

虽然AsyncTask这么简单好用,但你知道它是怎样实现的吗?那么接下来,我们就来分析一下AsyncTask的源码,对它的实现原理一探究竟。注意这里我选用的是Android 4.0的源码,如果你查看的是其它版本的源码,可能会有一些出入。

从之前DownloadTask的代码就可以看出,在启动某一个任务之前,要先new出它的实例,因此,我们就先来看一看AsyncTask构造函数中的源码,如下所示:

[java]view plaincopy
  1. publicAsyncTask(){
  2. mWorker=newWorkerRunnable<Params,Result>(){
  3. publicResultcall()throwsException{
  4. mTaskInvoked.set(true);
  5. Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
  6. returnpostResult(doInBackground(mParams));
  7. }
  8. };
  9. mFuture=newFutureTask<Result>(mWorker){
  10. @Override
  11. protectedvoiddone(){
  12. try{
  13. finalResultresult=get();
  14. postResultIfNotInvoked(result);
  15. }catch(InterruptedExceptione){
  16. android.util.Log.w(LOG_TAG,e);
  17. }catch(ExecutionExceptione){
  18. thrownewRuntimeException("AnerroroccuredwhileexecutingdoInBackground()",
  19. e.getCause());
  20. }catch(CancellationExceptione){
  21. postResultIfNotInvoked(null);
  22. }catch(Throwablet){
  23. thrownewRuntimeException("Anerroroccuredwhileexecuting"
  24. +"doInBackground()",t);
  25. }
  26. }
  27. };
  28. }

这段代码虽然看起来有点长,但实际上并没有任何具体的逻辑会得到执行,只是初始化了两个变量,mWorker和mFuture,并在初始化mFuture的时候将mWorker作为参数传入。mWorker是一个Callable对象,mFuture是一个FutureTask对象,这两个变量会暂时保存在内存中,稍后才会用到它们。

接着如果想要启动某一个任务,就需要调用该任务的execute()方法,因此现在我们来看一看execute()方法的源码,如下所示:

[java]view plaincopy
  1. publicfinalAsyncTask<Params,Progress,Result>execute(Params...params){
  2. returnexecuteOnExecutor(sDefaultExecutor,params);
  3. }

简单的有点过分了,只有一行代码,仅是调用了executeOnExecutor()方法,那么具体的逻辑就应该写在这个方法里了,快跟进去瞧一瞧:

[java]view plaincopy
  1. publicfinalAsyncTask<Params,Progress,Result>executeOnExecutor(Executorexec,
  2. Params...params){
  3. if(mStatus!=Status.PENDING){
  4. switch(mStatus){
  5. caseRUNNING:
  6. thrownewIllegalStateException("Cannotexecutetask:"
  7. +"thetaskisalreadyrunning.");
  8. caseFINISHED:
  9. thrownewIllegalStateException("Cannotexecutetask:"
  10. +"thetaskhasalreadybeenexecuted"
  11. +"(ataskcanbeexecutedonlyonce)");
  12. }
  13. }
  14. mStatus=Status.RUNNING;
  15. onPreExecute();
  16. mWorker.mParams=params;
  17. exec.execute(mFuture);
  18. returnthis;
  19. }

果然,这里的代码看上去才正常点。可以看到,在第15行调用了onPreExecute()方法,因此证明了onPreExecute()方法会第一个得到执行。可是接下来的代码就看不明白了,怎么没见到哪里有调用doInBackground()方法呢?别着急,慢慢找总会找到的,我们看到,在第17行调用了Executor的execute()方法,并将前面初始化的mFuture对象传了进去,那么这个Executor对象又是什么呢?查看上面的execute()方法,原来是传入了一个sDefaultExecutor变量,接着找一下这个sDefaultExecutor变量是在哪里定义的,源码如下所示:

[java]view plaincopy
  1. publicstaticfinalExecutorSERIAL_EXECUTOR=newSerialExecutor();
  2. ……
  3. privatestaticvolatileExecutorsDefaultExecutor=SERIAL_EXECUTOR;

可以看到,这里先new出了一个SERIAL_EXECUTOR常量,然后将sDefaultExecutor的值赋值为这个常量,也就是说明,刚才在executeOnExecutor()方法中调用的execute()方法,其实也就是调用的SerialExecutor类中的execute()方法。那么我们自然要去看看SerialExecutor的源码了,如下所示:

[java]view plaincopy
  1. privatestaticclassSerialExecutorimplementsExecutor{
  2. finalArrayDeque<Runnable>mTasks=newArrayDeque<Runnable>();
  3. RunnablemActive;
  4. publicsynchronizedvoidexecute(finalRunnabler){
  5. mTasks.offer(newRunnable(){
  6. publicvoidrun(){
  7. try{
  8. r.run();
  9. }finally{
  10. scheduleNext();
  11. }
  12. }
  13. });
  14. if(mActive==null){
  15. scheduleNext();
  16. }
  17. }
  18. protectedsynchronizedvoidscheduleNext(){
  19. if((mActive=mTasks.poll())!=null){
  20. THREAD_POOL_EXECUTOR.execute(mActive);
  21. }
  22. }
  23. }

SerialExecutor类中也有一个execute()方法,这个方法里的所有逻辑就是在子线程中执行的了,注意这个方法有一个Runnable参数,那么目前这个参数的值是什么呢?当然就是mFuture对象了,也就是说在第9行我们要调用的是FutureTask类的run()方法,而在这个方法里又会去调用Sync内部类的innerRun()方法,因此我们直接来看innerRun()方法的源码:

[java]view plaincopy
  1. voidinnerRun(){
  2. if(!compareAndSetState(READY,RUNNING))
  3. return;
  4. runner=Thread.currentThread();
  5. if(getState()==RUNNING){//recheckaftersettingthread
  6. Vresult;
  7. try{
  8. result=callable.call();
  9. }catch(Throwableex){
  10. setException(ex);
  11. return;
  12. }
  13. set(result);
  14. }else{
  15. releaseShared(0);//cancel
  16. }
  17. }

可以看到,在第8行调用了callable的call()方法,那么这个callable对象是什么呢?其实就是在初始化mFuture对象时传入的mWorker对象了,此时调用的call()方法,也就是一开始在AsyncTask的构造函数中指定的,我们把它单独拿出来看一下,代码如下所示:

[java]view plaincopy
  1. publicResultcall()throwsException{
  2. mTaskInvoked.set(true);
  3. Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
  4. returnpostResult(doInBackground(mParams));
  5. }

在postResult()方法的参数里面,我们终于找到了doInBackground()方法的调用处,虽然经过了很多周转,但目前的代码仍然是运行在子线程当中的,所以这也就是为什么我们可以在doInBackground()方法中去处理耗时的逻辑。接着将doInBackground()方法返回的结果传递给了postResult()方法,这个方法的源码如下所示:

[java]view plaincopy
  1. privateResultpostResult(Resultresult){
  2. Messagemessage=sHandler.obtainMessage(MESSAGE_POST_RESULT,
  3. newAsyncTaskResult<Result>(this,result));
  4. message.sendToTarget();
  5. returnresult;
  6. }

如果你已经熟悉了异步消息处理机制,这段代码对你来说一定非常简单吧。这里使用sHandler对象发出了一条消息,消息中携带了MESSAGE_POST_RESULT常量和一个表示任务执行结果的AsyncTaskResult对象。这个sHandler对象是InternalHandler类的一个实例,那么稍后这条消息肯定会在InternalHandler的handleMessage()方法中被处理。InternalHandler的源码如下所示:

[java]view plaincopy
  1. privatestaticclassInternalHandlerextendsHandler{
  2. @SuppressWarnings({"unchecked","RawUseOfParameterizedType"})
  3. @Override
  4. publicvoidhandleMessage(Messagemsg){
  5. AsyncTaskResultresult=(AsyncTaskResult)msg.obj;
  6. switch(msg.what){
  7. caseMESSAGE_POST_RESULT:
  8. //Thereisonlyoneresult
  9. result.mTask.finish(result.mData[0]);
  10. break;
  11. caseMESSAGE_POST_PROGRESS:
  12. result.mTask.onProgressUpdate(result.mData);
  13. break;
  14. }
  15. }
  16. }

这里对消息的类型进行了判断,如果这是一条MESSAGE_POST_RESULT消息,就会去执行finish()方法,如果这是一条MESSAGE_POST_PROGRESS消息,就会去执行onProgressUpdate()方法。那么finish()方法的源码如下所示:

[java]view plaincopy
  1. privatevoidfinish(Resultresult){
  2. if(isCancelled()){
  3. onCancelled(result);
  4. }else{
  5. onPostExecute(result);
  6. }
  7. mStatus=Status.FINISHED;
  8. }

可以看到,如果当前任务被取消掉了,就会调用onCancelled()方法,如果没有被取消,则调用onPostExecute()方法,这样当前任务的执行就全部结束了。

我们注意到,在刚才InternalHandler的handleMessage()方法里,还有一种MESSAGE_POST_PROGRESS的消息类型,这种消息是用于当前进度的,调用的正是onProgressUpdate()方法,那么什么时候才会发出这样一条消息呢?相信你已经猜到了,查看publishProgress()方法的源码,如下所示:

[java]view plaincopy
  1. protectedfinalvoidpublishProgress(Progress...values){
  2. if(!isCancelled()){
  3. sHandler.obtainMessage(MESSAGE_POST_PROGRESS,
  4. newAsyncTaskResult<Progress>(this,values)).sendToTarget();
  5. }
  6. }

非常清晰了吧!正因如此,在doInBackground()方法中调用publishProgress()方法才可以从子线程切换到UI线程,从而完成对UI元素的更新操作。其实也没有什么神秘的,因为说到底,AsyncTask也是使用的异步消息处理机制,只是做了非常好的封装而已。

读到这里,相信你对AsyncTask中的每个回调方法的作用、原理、以及何时会被调用都已经搞明白了吧。

关于AsyncTask你所不知道的秘密

不得不说,刚才我们在分析SerialExecutor的时候,其实并没有分析的很仔细,仅仅只是关注了它会调用mFuture中的run()方法,但是至于什么时候会调用我们并没有进一步地研究。其实SerialExecutor也是AsyncTask在3.0版本以后做了最主要的修改的地方,它在AsyncTask中是以常量的形式被使用的,因此在整个应用程序中的所有AsyncTask实例都会共用同一个SerialExecutor。下面我们就来对这个类进行更加详细的分析,为了方便阅读,我把它的代码再贴出来一遍:

[java]view plaincopy
  1. privatestaticclassSerialExecutorimplementsExecutor{
  2. finalArrayDeque<Runnable>mTasks=newArrayDeque<Runnable>();
  3. RunnablemActive;
  4. publicsynchronizedvoidexecute(finalRunnabler){
  5. mTasks.offer(newRunnable(){
  6. publicvoidrun(){
  7. try{
  8. r.run();
  9. }finally{
  10. scheduleNext();
  11. }
  12. }
  13. });
  14. if(mActive==null){
  15. scheduleNext();
  16. }
  17. }
  18. protectedsynchronizedvoidscheduleNext(){
  19. if((mActive=mTasks.poll())!=null){
  20. THREAD_POOL_EXECUTOR.execute(mActive);
  21. }
  22. }
  23. }

可以看到,SerialExecutor是使用ArrayDeque这个队列来管理Runnable对象的,如果我们一次性启动了很多个任务,首先在第一次运行execute()方法的时候,会调用ArrayDeque的offer()方法将传入的Runnable对象添加到队列的尾部,然后判断mActive对象是不是等于null,第一次运行当然是等于null了,于是会调用scheduleNext()方法。在这个方法中会从队列的头部取值,并赋值给mActive对象,然后调用THREAD_POOL_EXECUTOR去执行取出的取出的Runnable对象。之后如何又有新的任务被执行,同样还会调用offer()方法将传入的Runnable添加到队列的尾部,但是再去给mActive对象做非空检查的时候就会发现mActive对象已经不再是null了,于是就不会再调用scheduleNext()方法。

那么后面添加的任务岂不是永远得不到处理了?当然不是,看一看offer()方法里传入的Runnable匿名类,这里使用了一个try finally代码块,并在finally中调用了scheduleNext()方法,保证无论发生什么情况,这个方法都会被调用。也就是说,每次当一个任务执行完毕后,下一个任务才会得到执行,SerialExecutor模仿的是单一线程池的效果,如果我们快速地启动了很多任务,同一时刻只会有一个线程正在执行,其余的均处于等待状态。Android照片墙应用实现,再多的图片也不怕崩溃这篇文章中例子的运行结果也证实了这个结论。

不过你可能还不知道,在Android 3.0之前是并没有SerialExecutor这个类的,那个时候是直接在AsyncTask中构建了一个sExecutor常量,并对线程池总大小,同一时刻能够运行的线程数做了规定,代码如下所示:

[java]view plaincopy
  1. privatestaticfinalintCORE_POOL_SIZE=5;
  2. privatestaticfinalintMAXIMUM_POOL_SIZE=128;
  3. privatestaticfinalintKEEP_ALIVE=10;
  4. ……
  5. privatestaticfinalThreadPoolExecutorsExecutor=newThreadPoolExecutor(CORE_POOL_SIZE,
  6. MAXIMUM_POOL_SIZE,KEEP_ALIVE,TimeUnit.SECONDS,sWorkQueue,sThreadFactory);

可以看到,这里规定同一时刻能够运行的线程数为5个,线程池总大小为128。也就是说当我们启动了10个任务时,只有5个任务能够立刻执行,另外的5个任务则需要等待,当有一个任务执行完毕后,第6个任务才会启动,以此类推。而线程池中最大能存放的线程数是128个,当我们尝试去添加第129个任务时,程序就会崩溃。

因此在3.0版本中AsyncTask的改动还是挺大的,在3.0之前的AsyncTask可以同时有5个任务在执行,而3.0之后的AsyncTask同时只能有1个任务在执行。为什么升级之后可以同时执行的任务数反而变少了呢?这是因为更新后的AsyncTask已变得更加灵活,如果不想使用默认的线程池,还可以自由地进行配置。比如使用如下的代码来启动任务:

[java]view plaincopy
  1. Executorexec=newThreadPoolExecutor(15,200,10,
  2. TimeUnit.SECONDS,newLinkedBlockingQueue<Runnable>());
  3. newDownloadTask().executeOnExecutor(exec);

这样就可以使用我们自定义的一个Executor来执行任务,而不是使用SerialExecutor。上述代码的效果允许在同一时刻有15个任务正在执行,并且最多能够存储200个任务。

好了,到这里我们就已经把关于AsyncTask的所有重要内容深入浅出地理解了一遍,相信在将来使用它的时候能够更加得心应手。

Android AsyncTask源码解析相关推荐

  1. android sdk 源码解析

    AndroidSdkSourceAnalysis:https://github.com/LittleFriendsGroup/AndroidSdkSourceAnalysis 第一期 Class 分析 ...

  2. Android Lifecycle源码解析(一)

    Android Lifecycle源码解析(一) 首先我们看HomeActivity中我们添加到一行代码 public class HomeActivity extends AppCompatActi ...

  3. 【Android】Android Broadcast源码解析

    Android Broadcast源码解析 一.静态广播的注册 静态广播是通过PackageManagerService在启动的时候扫描已安装的应用去注册的. 在PackageManagerServi ...

  4. Android xUtils3源码解析之图片模块

    本文已授权微信公众号<非著名程序员>原创首发,转载请务必注明出处. xUtils3源码解析系列 一. Android xUtils3源码解析之网络模块 二. Android xUtils3 ...

  5. 【Android】Android Parcelable 源码解析

    Android Parcelable 源码解析 大家都知道,要想在Intent里面传递一些非基本类型的数据,有两种方式,一种实现Parcelable,另一种是实现Serializable接口.今天先不 ...

  6. Android xUtils3源码解析之注解模块

    本文已授权微信公众号<非著名程序员>原创首发,转载请务必注明出处. xUtils3源码解析系列 一. Android xUtils3源码解析之网络模块 二. Android xUtils3 ...

  7. Android xUtils3源码解析之数据库模块

    本文已授权微信公众号<非著名程序员>原创首发,转载请务必注明出处. xUtils3源码解析系列 一. Android xUtils3源码解析之网络模块 二. Android xUtils3 ...

  8. Android之AsyncTask源码解析

    转载请标明出处:[顾林海的博客] 个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持! ##前言 AsyncTask是一种轻量级的异步任务类,内部封装了Thread和Ha ...

  9. AsyncTask源码解析,你需要摸清的细节

    AsyncTask简介 1. AsyncTask提供了一种恰当的简单的跟UI Thread交互的方式. 2. 它不需要通过操控Threads或Handler就可以将后台操作以及其结果展示在UI Thr ...

最新文章

  1. 在Mac系统下使用自己安装的PHP
  2. Servlet 工作原理解析--转载
  3. 我们坚持专业与创新,U can U up
  4. EF Code First 学习笔记:关系(转)
  5. git 命令详解_再次学习Git版本控制工具
  6. C | 看看 C 能做什么
  7. 软件测试——第三次作业
  8. atitit 新特性与趋势管理的艺术 v2 s52.docx 1. lang语言系列 java node.js php 2 1.1. Atitit js es5 es6新特性 attilax总结
  9. 电力系统——基于10机39节点的电力系统仿真(Matlab)
  10. linux rpm与deb 区别,rpm与deb的区别
  11. 【华为OD机试真题 JAVA】数字涂色
  12. nandflash地址的物理地址,逻辑地址,spare地址等理解
  13. tcl/tk参考——列表操作llength
  14. c语言拆礼盒,拆礼盒、个人中心功能优化
  15. linux常用命令(六)命令执行顺序控制与管道
  16. OCJP(1Z0-851) 模拟题分析(三)
  17. 基于Python多元线性回归、机器学习、深度学习在近红外光谱分析中的实践应用培训班
  18. 拼多多面试——机器学习岗位面经
  19. base64转图片+图片转base64
  20. 《通信技术导论(原书第5版)》——导读

热门文章

  1. 从官网下载jdk1.6 1.7
  2. php分享十七:http状态码
  3. rhel6下,mysql 5.6.14 主从复制(也称mysql AB复制)环境配置[基于binlog]
  4. 常用的Linux命令行文本处理工具总结
  5. TiKV 是如何存取数据的(下)
  6. 「镁客·请讲」南京布塔:用动作捕捉世界的精彩
  7. tcpdump命令--详解
  8. 《从Excel到R 数据分析进阶指南》一第1章 生成数据表1.1 导入数据表
  9. select和其元素options
  10. lua笔记之userdata