android 工作线程 界面交互,深入探讨安卓UI线程与子线程交互5大设计

什么是UI线程

在一个Android 程序开始运行的时候,会单独启动一个Process。默认的情况下,所有这个程序中的Activity或者Service(Service和 Activity只是Android提供的Components中的两种,除此之外还有Content Provider和Broadcast Receiver)都会跑在这个Process

一个Android 程序默认情况下也只有一个Process,但一个Process下却可以有许多个Thread。在这么多Thread当中,有一个Thread,我们称之为UI Thread。UI Thread在Android程序运行的时候就被创建,是一个Process当中的主线程Main Thread,主要是负责控制UI界面的显示、更新和控件交互。在Android程序创建之初,一个Process呈现的是单线程模型,所有的任务都在一个线程中运行。

UI线程和子线程分开的意义

UI Thread所执行的每一个函数,所花费的时间都应该是越短越好。而其他比较费时的工作(访问网络,下载数据,查询数据库等),都应该交由子线程去执行,以免阻塞主线程。在android的设计思想中,为了确保用户顺滑的操作体验。一些耗时的任务不能够在UI线程中运行,像访问网络就属于这类任务。因此我们必须要重新开启一个后台线程运行这些任务。然而,往往这些任务最终又会直接或者间接的需要访问和控制UI控件。例如访问网络获取数据,然后需要将这些数据处理显示出来。就出现了上面所说的情况。原本这是在正常不过的现象了,但是android规定除了UI线程外,其他线程都不可以对那些UI控件访问和操控。为了解决这个问题,下面将探讨这UI线程于子线程之间的五种交互方式。

UI线程于子线程的交互方式

handler

Activity.runOnUIThread(Runnable)

View.Post(Runnable)

View.PostDelayed(Runnabe,long)

AsyncTask

handler使用

解释:当应用程序启动时,Android首先会开启一个主线程 (也就是UI线程) , 主线程为管理界面中的UI控件, 进行事件分发, 比如说, 你要是点击一个 Button ,Android会分发事件到Button上,来响应你的操作。 如果此时需要一个耗时的操作,例如: 联网读取数据, 或者读取本地较大的一个文件的时候,你不能把这些操作放在主线程中,如果你放在主线程中的话,界面会出现假死现象, 如果5秒钟还没有完成的话,会收到Android系统的一个错误提示 "强制关闭"。 这个时候我们需要把这些耗时的操作,放在一个子线程中,因为子线程涉及到UI更新,,Android主线程是线程不安全的, 也就是说,更新UI只能在主线程中更新,子线程中操作是危险的。 这个时候,Handler就出现了。,来解决这个复杂的问题 ,由于Handler运行在主线程中(UI线程中), 它与子线程可以通过Message对象来传递数据, 这个时候,Handler就承担着接受子线程传过来的(子线程用sedMessage()方法传弟)Message对象,(里面包含数据) , 把这些消息放入主线程队列中,配合主线程进行更新UI

Handler实例

子类需要继承Hendler类,并重写handleMessage(Message msg) 方法, 用于接受线程数据。

以下为一个实例,它实现的功能为:通过线程修改界面Button的内容

public class MyHandlerActivity extends Activity {

Button button;

MyHandler myHandler;

protected void onCreate(Bundle savedInstanceState) {

super。onCreate(savedInstanceState);

setContentView(R.layout.handlertest);

button = (Button) findViewById(R.id.button);

myHandler = new MyHandler();

// 当创建一个新的Handler实例时, 它会绑定到当前线程和消息的队列中,开始分发数据

// Handler有两个作用, (1) : 定时执行Message和Runnalbe 对象

// (2): 让一个动作,在不同的线程中执行。

// 它安排消息,用以下方法

// post(Runnable)

// postAtTime(Runnable, long)

// postDelayed(Runnable, long)

// sendEmptyMessage(int)

// sendMessage(Message);

// sendMessageAtTime(Message, long)

// sendMessageDelayed(Message, long)

// 以上方法以 post开头的允许你处理Runnable对象

//sendMessage()允许你处理Message对象(Message里可以包含数据)

MyThread m = new MyThread();

new Thread(m).start();

}

/**

* 接受消息,处理消息 ,此Handler会与当前主线程一块运行

* */

class MyHandler extends Handler {

public MyHandler() {

}

public MyHandler(Looper L) {

super(L);

}

// 子类必须重写此方法,接受数据

@Override

public void handleMessage(Message msg) {

// TODO Auto-generated method stub

Log.d("MyHandler", "handleMessage。。。。。。");

super.handleMessage(msg);

// 此处可以更新UI

Bundle b = msg.getData();

String color = b.getString("color");

MyHandlerActivity.this.button.append(color);

}

}

class MyThread implements Runnable {

public void run() {

try {

Thread.sleep(10000);

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

}

Log.d("thread......", "mThread......");

Message msg = new Message();

Bundle b = new Bundle();// 存放数据

b.putString("color","我的");

msg.setData(b);

MyHandlerActivity.this.myHandler.sendMessage(msg);

// 向Handler发送消息,更新UI

}

}

}

Activity.runOnUIThread(Runnable)

编写后台线程,这回你可以直接调用UI控件

创建后台线程的实例

调用UI线程对应的Activity的runOnUIThread方法,将后台线程实例作为参数传入其中。

注意:无需调用后台线程的start方法

View.Post(Runnable)

该方法和方法二基本相同,只是在后台线程中能操控的UI控件被限制了,只能是指定的UI控件View。方法如下

编写后台线程,这回你可以直接调用UI控件,但是该UI控件只能是View

创建后台线程的实例

调用UI控件View的post方法,将后台线程实例作为参数传入其中。

以下是官方文档对该方法的注释及源码。(postDelayed类似,不再赘述)

Causes the Runnable to be added to the message queue.The runnable >will be run on the user interface thread.

public boolean post(Runnable action) {

final AttachInfo attachInfo = mAttachInfo;

if (attachInfo != null) {

return attachInfo.mHandler.post(action);

}

// Assume that post will succeed later

ViewRootImpl.getRunQueue().post(action);

return true;

}

意思是将任务添加到消息队列中,保证在UI线程执行。从本质上说,它还是依赖于以Handler、Looper、MessageQueue、Message为基础的异步消息处理机制。相对于新建Handler进行处理更加便捷。下面举一个常用的例子,比如在onCreate方法中获取某个view的宽高,而直接View#getWidth获取到的值是0。要知道View显示到界面上需要经历onMeasure、onLayout和onDraw三个过程,而View的宽高是在onLayout阶段才能最终确定的,而在Activity#onCreate中并不能保证View已经执行到了onLayout方法,也就是说Activity的声明周期与View的绘制流程并不是一一绑定。那为什么调用post方法就能起作用呢?首先MessageQueue是按顺序处理消息的,而在setContentView()后队列中会包含一条询问是否完成布局的消息,而我们的任务通过View#post方法被添加到队列尾部,保证了在layout结束以后才执行。

View.PostDelayed(Runnabe,long)

该方法是方法三的补充,long参数用于制定多少时间后运行后台进程

这是一种可以创建多线程消息的函数

使用方法:

试例1:闹钟提醒延时

1,首先创建一个Handler对象

Handler handler=new Handler();

2,然后创建一个Runnable对象

Runnable runnable=new Runnable(){

@Override

public void run() {

// TODO Auto-generated method stub

//要做的事情,这里再次调用此Runnable对象,以实现每两秒实现一次的定时器操作

handler.postDelayed(this, 2000);

}

};

3,使用PostDelayed方法,两秒后调用此Runnable对象

handler.postDelayed(runnable, 2000);

实际上也就实现了一个2s的一个定时器

4,如果想要关闭此定时器,可以这样操作

handler.removeCallbacks(runnable);

当然,你也可以做一个闹钟提醒延时的函数试试,比如,先用MediaPlayer播放闹钟声音, 如果不想起,被停止播放之后,下次就5分钟后再播放,再被停止的话,下次就4分钟后播放, ……………… 只要更改延时的时间就可以实现了,用一个static对象的话会比较容易操作

试例2:欢迎页制作

public class SplanshActivity extends AppCompatActivity {

private Handler handler;

private Boolean isFirst = true;

private SharedPreferences sp ;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_splansh);

sp = getPreferences(MODE_PRIVATE);

handler = new Handler();

handler.postDelayed(new Runnable() {

@Override

public void run() {

isFirst = sp.getBoolean("isFirst",true);

Intent intent = new Intent();//意图

if(isFirst){

sp.edit().putBoolean("isFirst",false).commit();

//引导界面

intent.setClass(SplanshActivity.this,GuideActivity.class);

}else{

//主界面

intent.setClass(SplanshActivity.this,MainActivity.class);

}

startActivity(intent);

finish();

}

},3000);

}

}

AsyncTask是一个专门用来处理后台进程与UI线程的工具。通过AsyncTask,我们可以非常方便的进行后台线程和UI线程之间的交流。

那么AsyncTask是如何工作的哪。

AsyncTask拥有3个重要参数

Params

rogress

Result

Params是后台线程所需的参数。在后台线程进行作业的时候,他需要外界为其提供必要的参数,就好像是一个用于下载图片的后台进程,他需要的参数就是图片的下载地址。

Progress是后台线程处理作业的进度。依旧上面的例子说,就是下载图片这个任务完成了多少,是20%还是60%。这个数字是由Progress提供。

Result是后台线程运行的结果,也就是需要提交给UI线程的信息。按照上面的例子来说,就是下载完成的图片。

AsyncTask还拥有4个重要的回调方法。

1、onPreExecute

2、doInBackground

3、onProgressUpdate

4、onPostExecute

onPreExecute运行在UI线程,主要目的是为后台线程的运行做准备。当他运行完成后,他会调用doInBackground方法。

doInBackground运行在后台线程,他用来负责运行任务。他拥有参数Params,并且返回Result。在后台线程的运行当中,为了能够更新作业完成的进度,需要在doInbackground方法中调用PublishProgress方法。该方法拥有参数Progress。通过该方法可以更新Progress的数据。然后当调用完PublishProgress方法,他会调用onProgressUpdate方法用于更新进度。

onProgressUpdate运行在UI线程,主要目的是用来更新UI线程中显示进度的UI控件。他拥有Progress参数。在doInBackground中调用PublishProgress之后,就会自动调onProgressUpdate方法

onPostExecute运行在UI线程,当doInBackground方法运行完后,他会调用onPostExecute方法,并传入Result。在onPostExecute方法中,就可以将Result更新到UI控件上。

明白了上面的3个参数和4个方法,你要做的就是

编写一个继承AsyncTask的类,并声明3个参数的类型,编写4个回调方法的内容。

然后在UI线程中创建该类(必须在UI线程中创建)。

最后调用AsyncTask的execute方法,传入Parmas参数(同样必须在UI线程中调用)。

这样就大功告成了。

另外值得注意的2点就是,千万不要直接调用那四个回调方法。还有就是一个AsyncTask实例只能执行一次,否则就出错哦。

private class DownloadFilesTask extends AsyncTask {

//在这里声明了Params、Progress、Result参数的类型

//因为这里不需要使用onPreExecute回调方法,所以就没有加入该方法

//后台线程的目的是更具URL下载数据

protected Long doInBackground(URL... urls) {

int count = urls.length;//urls是数组,不止一个下载链接

long totalSize = 0;//下载的数据

for (int i = 0; i < count; i++) {

//Download是用于下载的一个类,和AsyncTask无关,大家可以忽略他的实现

totalSize += Downloader.downloadFile(urls[i]);

publishProgress((int) ((i / (float) count) * 100));//更新下载的进度

// Escape early if cancel() is called

if (isCancelled()) break;

}

return totalSize;

}

//更新下载进度

protected void onProgressUpdate(Integer... progress) {

setProgressPercent(progress[0]);

}

//将下载的数据更新到UI线程

protected void onPostExecute(Long result) {

showDialog("Downloaded " + result + " bytes");

}

}

有了上面的这个类,接下你要做的就是在UI线程中创建实例,并调用execute方法,传入URl参数就可以了。

这上面的5种方法各有优点。但是究其根本,其实后面四种方法都是基于handler方法的包装。在一般的情形下后面四种似乎更值得推荐。但是当情形比较复杂,还是推荐使用handler

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值