android基础知识02——线程安全2:handler、message、runnable

       

        android的UI操作不是线程安全的,同时也只有主线程才能够操作UI,同时主线程对于UI操作有一定的时间限制(最长5秒)。为了能够做一些比较耗时的操作(比如下载、打开大文件等),android提供了一些列机制。《android基础知识02——线程安全》系列文章就是参考了网上许多网友的文章后,整理出来的一个系列,介绍了主要的方法。分别如下:

         android基础知识02——线程安全1:定义及例子

        android基础知识02——线程安全2:handler、message、runnable

        android基础知识02——线程安全3:Message,MessageQueue,Handler,Looper

        android基础知识02——线程安全4:HandlerThread

        android基础知识02——线程安全5: AsyncTask


四、使用消息队列的实现        

         在上文中,我们提到在android中只有主线程才可以进行UI操作,我们例子中让子线程操作UI结果出现了错误。

         为了使该程序得以运行,我们使用如下解决方案:

private EditText editText;
    private Handler messageHandler;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        editText = (EditText) findViewById(R.id.weather_city_edit);
        Button button = (Button) findViewById(R.id.goQuery);
        button.setOnClickListener(this);
        //得到当前线程 的Looper实例,由于 当前线程是UI线程也可以 通过Looper.getMainLooper()得到
        Looper looper = Looper.myLooper();
        //此处甚至可以 不需要设置Looper,因为 Handler默认就使用当 前线程的Looper
        messageHandler = new MessageHandler(looper);
    }

@Override
    public void onClick(View v) {
        //创建一个子线 程去做耗时的网络连接工作
        new Thread() {
            @Override
            public void run() {
                //活动用户输入 的城市名称
                String city = editText.getText().toString();
                //调用Google 天气API查询指定城 市的当日天气情况
                String weather = getWetherByCity(city);
                //创建一个Message对象,并把得 到的天气信息赋值给Message对象
                Message message = Message.obtain();
                message.obj = weather;
                //通过Handler发布携带有天 气情况的消息
                messageHandler.sendMessage(message);
            }
        }.start();
    }

 //子类化一个Handler
    class MessageHandler extends Handler {
        public MessageHandler(Looper looper) {
            super(looper);
        }
        @Override
        public void handleMessage(Message msg) {
            //处理收到的消 息,把天气信息显示在title上
            setTitle((String) msg.obj);
        }
    }

        以上程序使用消息队列来实现,通过消息队列改写过后的天气预报程序已经可以成功运行,因为Handler的handleMessage方法实 际是由关联有该消息队列的UI thread调用,而在UI thread中更新title并没有违背Android的单线程模型的原则。

五、消息队列

        在上文中使用到了android的消息队列。说到消息队列,我们可以想到一个流程:产生消息——接收消息(存入消息队列)——消息队列——处理消息。这里android的实现,涉及handler、messagequeue、looper等定义,下面来一一介绍:

1、handler

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

       handler可以分发Message对象和Runnable对象到主线程中, 每个Handler实例,都会绑定到创建他的线程中(一般是位于主线程),
        它有两个作用: (1):  安排消息或Runnable 在某个主线程中某个地方执行, (2)安排一个动作在不同的线程中执行
      
        Handler中分发消息的一些方法
        post(Runnable)
        postAtTime(Runnable,long)
        postDelayed(Runnable long)
        sendEmptyMessage(int)
        sendMessage(Message)
        sendMessageAtTime(Message,long)
        sendMessageDelayed(Message,long)
        以上post类方法允许你排列一个Runnable对象到主线程队列中,
        sendMessage类方法, 允许你安排一个带数据的Message对象到队列中,等待更新.

1.1 message和runnable

        通过创建一个Handler子类的对象,每个acvivity只需一个Handler对象。后台进程可通过两种方式Handler进行通信:message和Runnable对象,其结果实质都是将在Handler的队列中放入内容,message是放置信息,可以传递一些参数,Handler获取这些信息并将判度如何处理,而Runnable则是直接给出处理的方法。队列就是依次执行,Handler会处理完一个消息或者执行完某个处理在进行下一步,这样不会出现多个线程同时要求进行UI处理而引发的混乱现象。

        这些队列中的内容(无论Message还是Runnable)可以要求马上执行,延迟一定时间执行或者指定某个时刻执行,如果将他们放置在队列头,则表示具有最高有限级别,立即执行。这些函数包括有:sendMessage(), sendMessageAtFrontOfQueue(), sendMessageAtTime(), sendMessageDelayed()以及用于在队列中加入Runnable的post(), postAtFrontOfQueue(), postAtTime(),postDelay()。

        一般而言,推荐是Messge方式,这样程序设计得可以更为灵活,而Runnable在某些简单明确的方式中使用。我们将通过三种方法编写一个小例子来学习。这个例子是一个进度条,每隔1秒,进度条步进5,如果acvity停止时,进度条归零。

Android XML :

<?xml version="1.0" encoding="utf-8"?> 
<LinearLayout ...... /> 
  <ProgressBar android:id="@+id/c15_progress" 
    style="?android:attr/progressBarStyleHorizontal"   <!-- 这表明采用传统水平进度条的方式--> 
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" />
</LinearLayout>
例子一:线程开启,采用Message传递后台线程和UI主线程之间的信息

public class Chapter15Test1 extends Activity{
    private ProgressBar bar = null;
    private boolean isRunning = false; 
    
    /* 我们为这个Acivity创建一个用于和后台程序通信的handler,简单地,只要一收到message,就将progressbar进度增加5。*/ 
    /* 步骤1:创建Handler,并通过handleMessage()给出当收到消息是UI需要进行如何处理,例子简单不对msg的内容进行分析*/ 
    Handler handler= new Handler(){
        public void handleMessage (Message msg) {
            bar.incrementProgressBy(5);
        }
    };
    
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.chapter_15_test1);
        bar=(ProgressBar)findViewById(R.id.c15_progress);
    }

    /*on Start是UI初始化并显示时调用*/ 
    protected void onStart() {
        super.onStart();
        bar.setProgress(0); 
        /*步骤2:建立后台线程处理,采用Thread,其中run()的内容,就是线程并行处理的内容,Thread是Runnable的implements*/ 
        Thread background = new Thread(new Runnable(){
            public void run () {
                try{
                    for(int i = 0; i < 20 && isRunning; i ++){
                        Thread.sleep(1000);
                        /* 步骤2.1:发送Message到队列中,参数中的obtainMessage()是用于给出一个新Message,本例无参数,对应的在handler在队列中收到这条消息时,则通过handleMessage()进行处理*/ 
                        handler.sendMessage (handler.obtainMessage ());
                    }
                }catch(Throwable t){
                    //jest end the thread 
                }
            }
            
        });
        isRunning = true; 
       /*步骤3:启动线程*/ 
        background.start();
    }

    /*onStop是UI停止显示时调用,例如我们按了返回键*/ 
    protected void onStop() {
        super.onStop();
        isRunning = false; 
    }  
}

例子2:采用Runnable


我们在上面的例子的基础上进行修改,如下

 /*步骤1:由于不需要处理Message,也即不需要处理handleMessage()*/ 
    Handler handler= new Handler();
    /*步骤1.1:定义处理动作,采用Runnable的实例,通过implements run()来定制处理,这里是简单将进度条步进5。由于我们将在Thread中使用这个实例,所以考虑采用final的方式*/ 
    final Runnable r = new Runnable() {
         public void run(){
            bar.incrementProgressBy(5); 
         }  
    };
    /* ... ...在onStart()中的步骤2:线程的处理,和提供message不同,对于runnable方式,采用post */ 
        Thread background = new Thread(new Runnable(){
            public void run() {
                try{
                    for(int i = 0; i < 20 && isRunning; i ++){
                        Thread.sleep(1000); 
                        handler.post(r); 
                    }
                }catch(Throwable t){
                    //jest end the thread
                }
            }          
        });
        background.start();

例子3:本程序还可以用延迟处理实现定时触发,让程序更为简单

在这里例子,事实我们是进行定时的处理,利用Handler队列可以设置延期处理的方式,我们并不需要创建一个后台运行的线程,也可以实现

Handler handler= new Handler();
    
   ... ... 在onStart() ... ... 
    //利用handler.postDelayed(r,1000),在队列中要求延迟1秒后进行r的处理,而在r的处理中, 最后在handler的队列中加 入一个要求延迟1秒的处理,如是,就可以实现每隔1秒的定期处理。 
        handler.postDelayed(new Runnable() {
            public void run() {
                if(isRunning && Chapter15Test2.step < 20){
                    step ++;
                    bar.incrementProgressBy(5); 
                    handler.postDelayed(this, 1000); 
                } 
            }         
        },1000 );

       在这个例子中,我们基础某种判度,自动停止向队列加入处理。如果有某种情况,我们需要清除队列中的消息或者理,可以使用removMessages()或者removeCallbacks()的处理,这种对于延迟处理方式是非常有用的,可以中断定期的处理。当然,一般来讲我们希望能够得到某种判度,以使得定期处理能够优雅地结束,而不是简单地从队列中将消息或者处理删除。


例子4:不知道在UI主线程还是在后台线程

       有时候,我们并不清楚代码将在UI线程还是后台线程运行,例如这些代码封装为一个JAR,提供给其他人调用,我们并不清楚其他人如何使用这些代码。为了解决这个问题Android在activity中提供了runOnUiThread(),如果在UI线程,则马上执行,如果在后台线程,则将Runnable的执行内容加入到后台线程的队列中,这样无论代码在UI线程还是后台线程都能安全地执行。

我们在例子1的基础上进行试验:

1、建立一个Runnable,以便我们将在UI和后台Thread中进行试验

Runnable runAction = new Runnable(){
        public void run(){
            //注意,我们不能使用Toast.makeText(this,....),因为我们无法确定Runnable具体运行的context 
            Toast.makeText(getApplicationContext (),"Hello!",Toast.LENGTH_SHORT).show(); 
            //Log.d("WEI","runAction .... is called"); 
        }
    };

       由于Toast的显示和隐藏需要一定的时间,而间隔1秒显然不够,我们将例子1的间隔时间1000ms,改为5000ms这样会比较清晰,当然可以采用Log.d的方式来替代。

2、在UI线程中执行该操作,在后台线程中增加该操作,这个操作无论是在UI还是在后台线程都是可以正确执行的。

protected void onStart() {
    ... ... 
    Thread background = new Thread(new Runnable(){
        public void run() {
            try{
                for(int i = 0; i < 20 && isRunning; i ++){
                    Thread.sleep(5000);
                    handler.sendMessage(handler.obtainMessage()); 
                    runOnUiThread (runAction);
                }
            }catch(Throwable t){
                //jest end the thread
            } 
        }           
    });
        
    isRunning = true;
    background.start(); 
    runOnUiThread (runAction);
}

例子5:HandlerThread

        在上面的例子中,无论是否使用了后台线程(例子1-2),Handler的处理实际就是UI主线程的处理,一般的使用方式为我们通过后台线程执行某些操作,如果需要进行UI的互动,将消息或者处理方式到Handler的的队列中,然手在UI主线程中进行处理。这是我们通用的情况。

        之前我们讨论过为何UI的归UI,处理的处理,然而,可能有这样的需求,举个例子,在某些情况下,Handler收到消息触发的处理中可能会有Sleep(),这会导致main线程进入sleep状态,不是我们期待的。因此我们希望通过一个线程专门处理Hanlder的消息,这个线程也是依次从Handler的队列中获取信息,逐个进行处理,保证安全,不会出现混乱引发的异常。

        针对此Android提供的HandlerThread。方式使用方法如下:

//步骤1:创新HandlerThread的一个对象,并开启这个线程,HandlerThread将通过Looper来处理Handler对来中的消息,也就是如果发现Handler中有消息,将在HandlerThread这个线程中进行处理。 
HandlerThread ht = new HandlerThread("hander_thread");
//步骤2:启动handerhandler这个线程; 
ht.start();

//步骤3:创建handler中,带上Looper的参数,即handlerThread.getLooper()。注意,此处理必须在HandlerThread启动后才能调用,否则会报错 ,getLooper()会返回null,则程序异常出错 
Handler handler = new Handler(ht.getLooper()){
    ....
    public void handleMessage(Message msg){
    ... ...    /*这里的处理,将不在主线程中执行,而在HandlerThread线程中执行,可以通过Thread.currentThread().getId()或者Thread.currentThread().getName()来确定*/ 
    }
};


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值