探讨android更新UI的几种方法

      做为IT新手,总觉得只要有时间,有精力,什么东西都能作出来。这种念头我也有过,但很快就熄灭了,由于现实是残酷的,就算一开始的时间和精力很是充足,也会随着项目的推动而逐步消磨殆尽。咱们会发现,本身愈来愈消极怠工,只是在无心义的敲代码,敲的仍是网上抄来的代码,若是不行,继续找。java

     这就是项目进度没有规划好而致使的。android

     最近在作有关蓝牙的项目,一开始的进度都安排得很顺利,可是由于测试须要两部手机,并且还要是android手机,暑假已经开始了,同窗们都回家了,加上我手机的蓝牙坏了,致使个人进度严重被打乱!并且更加可怕的是,就算我手机这边调试完毕,我最终的目标是实现手机与蓝牙模块的通讯,那个测试板至今未送过来,因此,我开始消极怠工了。编程

     经验教训很是简单:根据整个项目的时间长度规划好天天的进度,视实际状况的变化而改变规划,就算真的是没法开展工做,像是如今这样抽空出来写写博客都要好过无心义的敲代码。安全

     今天讲的内容很是简单,只是讲讲有关于android界面更新的方面。多线程

1.利用Looper更新UI界面并发

     若是咱们的代码须要随时将处理后的数据交给UI更新,那么咱们想到的方法就是另开一个线程更新数据(也必须这么作,若是咱们的数据更新运算量较大,就会阻塞UI线程),也就是界面更新和数据更新是在不一样线程中(android采用的是UI单线程模型,因此咱们也只能在主线程中对UI进行操做),但这会致使另外一个问题:如何在两个线程间通讯呢?android提供了Handler机制来保证这种通讯。异步

     先是一个简单的例子:socket

public class MainActivity extends Activity {
    private Button mButton;
    private TextView mText;
    
    @SuppressLint("HandlerLeak")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        mButton = (Button)this.findViewById(R.id.button);
        mText = (TextView)this.findViewById(R.id.text);
        
        final Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg){
                super.handleMessage(msg);
                if(msg.what == 1){
                    mText.setText("更新后");
                }
            }
        };
        
        mText.setText("更新前");
        final Thread thread = new Thread(new Runnable(){

            @Override
            public void run() {
                 Message message = new Message();
                 message.what = 1;
                 handler.sendMessage(message);
            }
            
        });
        mButton.setOnClickListener(new OnClickListener() {
            
            @Override
            public void onClick(View v) {
                 thread.start();
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

}

      在Main主线程中新开一个线程,该线程负责数据的更新,而后将更新后的数据放在Message里面,而后经过Handler传递给相应的UI进行更新。ide

     

  

      使用TextView或者其余组件的时候,若是出现这样的错误:oop

      android.content.res.Resources$NotFoundException:String resource ID #0x86

      这样的错误误导性真大!我觉得是个人资源ID用错了,但就是这个ID,一会儿就无法子了,查了好久,结果发现是TextView.setText()要求的是字符串,但我传入了一个int!就这个问题,本来是传参错误,但android居然没有报错,并且这个错误提示也太那个了吧!!

      Message的任务很简单,就是用来传递数据更新信息,但有几点也是值得注意的:咱们可使用构造方法来建立Message,但出于节省内存资源的考量,咱们应该使用Message.obtain()从消息池中得到空消息对象,并且若是Message只是携带简单的int信息,优先使用Message.arg1和Message.arg2来传递信息,这样比起使用Bundle更省内存,而Message.what用于标识信息的类型。

      咱们如今来了解Handler的工做机制。

      Handler的做用就是两个:在新启动的线程中发送消息和在主线程中获取和处理消息。像是上面例子中的Handler就包含了这两个方面:咱们在新启动的线程thread中调用Handler的sendMessage()方法来发送消息。发送给谁呢?从代码中能够看到,就发送给主线程建立的Handler中的handleMessage()方法处理。这就是回调的方式:咱们只要在建立Handler的时候覆写handleMessage()方法,而后在新启动的线程发送消息时自动调用该方法。

      要想真正明白Handler的工做机制,咱们就要知道Looper,Message和MessageQueue。

      Looper正如字面上的意思,就是一个"循环者",它的主要做用就是使咱们的一个普通线程变成一个循环线程。若是咱们想要获得一个循环线程,咱们必需要这样:

class LooperThread extends Thread{
     public Handler mHandler;
     
     public void run(){
         Looper.prepare();
         mHandler = new Handler(){
              public void handleMessage(Message msg){
                   //process incoming message here
             }
        };
        Looper.loop();
     }
}

      Looper.prepare()就是用来使当前的线程变成一个LooperThread,而后咱们在这个线程中用Handler来处理消息队列中的消息,接着利用Looper.loop()来遍历消息队列中的全部消息。

      话是这么说,可是最后处理的是消息队列中的最后一个消息:

 mHandler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                mTextView.setText(msg.what + "");
            }
        };
        
        mButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                 LooperThread thread = new LooperThread();
                 thread.setHandler(mHandler);
                 thread.start();
            }
        });
    }

    class LooperThread extends Thread {
        Handler handler;
        
        public void setHandler(Handler handler){
            this.handler = handler;
        }

        @Override
        public void run() {
            Looper.prepare();
            for (int i = 0; i < 10; i++) {
                Message message = Message.obtain();
                message.arg1 = i;
                handler.sendMessage(message);
            }
            Looper.loop();
        }
    }

      结果显示的是9!!难道说MessageQueue是"先进后出"的队列?

      这只是由于处理得太快,若是咱们这样子:

try{
  Thread.sleep(1000);
  handler.sendMessage(message);
}catch(InterruptedException e){}

       咱们就能够看到TextView从0一直数到9。

       由此可知道,sendMessage()方法的实现是回调了handleMessage(),因此说是处理消息队列中的全部消息也是正确的,由于消息一发送到消息队列中就当即被处理。

       Looper线程应该怎么使用,获得一个Looper引用咱们能干吗?

      让咱们继续思考这个问题。

      每一个线程最多只有一个Looper对象,它的本质是一个ThreadLocal,而ThreadLocal是在JDK1.2中引入的,它为解决多线程程序的并发问题提供了一种新思路。

      ThreadLocal并非一个Thread,它是Thread的局部变量,正确的命名应该是ThreadLocalVariable才对。若是是常常看android源码的同窗,有时候也会发现它的一些变量的命名也很随便。

      ThreadLocal为每一个使用该变量的线程提供独立的变量副本,因此每个线程均可以独立的改变本身的副本而不会影响到其余线程的副本。这种解决方案就是为每个线程提供独立的副本,而不是同步该变量。

      可是该变量并非在线程中声明的,它是该线程使用的变量,由于对于线程来讲,它所使用的变量就是它的本地变量,因此Local就是取该意。

      学过java的同窗都知道,编写线程局部变量比起同步该变量来讲,实在是太笨拙了,因此咱们更多使用同步的方式,并且java对该方式也提供了很是便利的支持。

      如今最大的问题就是:ThreadLocal是如何维护该变量的副本呢?

      实现的方式很是简单:在ThreadLocal中有一个Map,用于存储每个线程的变量副本,Map中元素的键为线程对象,而值对应的是该线程的变量副本。

      一样是为了解决多线程中相同变量的访问冲突问题,ThreadLocal和同步机制相比,有什么优点呢?

      使用同步机制,咱们必须经过对象的锁机制保证同一时间只有一个线程访问变量。因此,咱们必须分析何时对该变量进行读写,何时须要锁定某个对象,又是何时该释放对象锁等问题,更糟糕的是,咱们根本就没法保证这样作事万无一失的。

      ThreadLocal是经过为每个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突,因此咱们也就没有必要使用对象锁这种难用的东西,这种方式更加安全。

      ThreadLocal最大的问题就是它须要为每一个线程维护一个副本,也就是"以空间换时间"的方式。咱们知道,内存空间是很是宝贵的资源,这也是咱们大部分时候都不会考虑该方式的缘由。

     为何Looper是一个ThreadLocal呢?Looper自己最大的意义就是它内部有一个消息队列,而其余线程是能够向该消息队列中添加消息的,因此Looper自己就是一个ThreadLocal,每一个线程都维护一个副本,添加到消息队列中的消息都会被处理掉。

  mHandler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                if(msg.what == 1){
                mTextView.setText(msg.what + "");
                }else{
                    Toast.makeText(MainActivity.this, msg.what + "", Toast.LENGTH_LONG).show();
                }
            }
        };
        
        mButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                 Thread1 thread1 = new Thread1();
                 thread1.setHandler(mHandler);
                 thread1.start();
                 Thread2 thread2 = new Thread2();
                 thread2.setHandler(mHandler);
                 thread2.start();
            }
        });
    }
    
    class  Thread2 extends Thread {
        Handler handler;
        
        public void setHandler(Handler handler){
            this.handler = handler;
        }

        @Override
        public void run() {
                Message message = Message.obtain();
                message.what = 2;
                handler.sendMessage(message);
                   
        }
    }

    class  Thread1 extends Thread {
        Handler handler;
        
        public void setHandler(Handler handler){
            this.handler = handler;
        }

        @Override
        public void run() {
                Message message = Message.obtain(); 
message.what = 1; handler.sendMessage(message); } }

      上面这段代码是新建两个线程,每一个线程都维护一个Handler,而后都向这个Handler发送消息,结果就是这两个消息同时被处理。
      Hanlder自己就持有一个MessageQueue和Looper的引用,默认状况下是建立该Handler的线程的Looper和该Looper的MessageQueue。

      Hanler只能处理由本身发出的消息,它会通知MessageQueue,代表它要执行一个任务,而后在轮到本身的时候执行该任务,这个过程是异步的,由于它不是采用同步Looper的方式而是采用维护副本的方式解决多线程共享的问题。

      一个线程能够有多个Handler,可是只能有一个Looper,理由同上:维护同一个Looper的副本。

      到了这里,咱们能够发现:新开一个线程用于处理数据的更新,在主线程中更新UI,这种方式是很是天然的,并且这也是所谓的观察者模式的使用(使用回调的方式来更新UI,几乎能够认为是使用了观察者模式)。

      咱们继续就着Looper探讨下去。

      由于Handler须要当前线程的MessageQueue,因此咱们必须经过Looper.prepare()来为Handler启动MessageQueue,而主线程默认是有MessageQueue,因此咱们不须要在主线程中调用prepare()方法。在Looper.loop()后面的代码是不会被执行的,除非咱们显式的调用Handler.getLooper().quit()方法来离开MessageQueue。

      到了这里,咱们以前的问题:LooperThread应该如何使用?已经有了很好的答案了: LooperThread用于UI的更新,而其余线程向其Handler发送消息以更新数据。由于主线程本来就是一个LooperThread,因此咱们平时的习惯都是在主线程里建立Handler,而后再在其余线程里更新数据,这种作法也是很是保险的,由于UI组件只能在主线程里面更新。

      固然,Handler并不只仅是用于处理UI的更新,它自己的真正意义就是实现线程间的通讯:

  new LooperThread().start();
  mButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                final int MESSAGE_HELLO = 0;
                String message = "hello";
                mHandler.obtainMessage(MESSAGE_HELLO, message).sendToTarget();
            }
        });
    }

    class LooperThread extends Thread {

        @Override
        public void run() {
            Looper.prepare();
            mHandler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                    case MESSAGE_HELLO:
                        Toast.makeText(MainActivity.this, (String) msg.obj,
                                Toast.LENGTH_SHORT).show();
                        break;
                    default:
                        break;
                    }

                }
            };
            Looper.loop();
        }
    }

      上面是Handler很是经典的用法:咱们经过Handler的obtainMessage()方法来建立一个新的Message(int what, Object obj),而后经过sendToTarget()发送到建立该Handler的线程中。若是你们作过相似蓝牙编程这样须要经过socket通讯的项目,就会清楚的知道,判断socket的状态是多么重要,而Message的what就是用来存储这些状态值(一般这些状态值是final int),值得注意的是,obj是Object,因此咱们须要强制转型。但这样的编码会让咱们的代码拥有一大堆常量值,并且switch的使用是不可避免的,若是状态值不少,那这个switch就真的是太臃肿了,就连android的蓝牙官方实例也没法避免这点。

      总结一下:Android使用消息机制实现线程间的通讯,线程经过Looper创建本身的消息循环,MessageQueue是FIFO的消息队列,Looper负责从MessageQueue中取出消息,而且分发到引用该Looper的Handler对象,该Handler对象持有线程的局部变量Looper,而且封装了发送消息和处理消息的接口。

      若是Handler仅仅是用来处理UI的更新,还能够有另外一种使用方式:

        mHandler = new Handler();
        mRunnable = new Runnable() {

            @Override
            public void run() {
                mTextView.setText("haha");
            }
        };
        mButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                new Thread() {
                    public void run() {
                        mHandler.post(mRunnable);
                    }
                }.start();
            }
        });
    }

      使用Handler的post()方法就显得UI的更新处理很是简单:在一个Runnable对象中更新UI,而后在另外一个线程中经过Handler的post()执行该更新动做。值得注意的是,咱们就算不用新开一个新线程照样能够更新UI,由于UI的更新线程就是Handler的建立线程---主线程。

      表面上Handler彷佛能够发送两种消息:Runnable对象和Message对象,实际上Runnable对象会被封装成Message对象。

2.AsyncTask利用线程任务异步更新UI界面

      AsyncTask的原理和Handler很接近,都是经过往主线程发送消息来更新主线程的UI,这种方式是异步的,因此就叫AsyncTask。使用AsyncTask的场合像是下载文件这种会严重阻塞主线程的任务就必须放在异步线程里面:

public class MainActivity extends Activity {
    private Button mButton;
    private ImageView mImageView;
    private ProgressBar mProgressBar;

    @SuppressLint("HandlerLeak")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = (Button) this.findViewById(R.id.button);
        mImageView = (ImageView) this.findViewById(R.id.image);
        mProgressBar = (ProgressBar) this.findViewById(R.id.progressBar);
        mButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                AsyncTaskThread thread = new AsyncTaskThread();
                thread.execute("http://g.search2.alicdn.com/img/bao/uploaded/i4/"
                        + "i4/12701024275153897/T1dahpFapbXXXXXXXX_!!0-item_pic.jpg_210x210.jpg");
            }
        });
    }

    class AsyncTaskThread extends AsyncTask<String, Integer, Bitmap> {

        @Override
        protected Bitmap doInBackground(String... params) {
            publishProgress(0);
            HttpClient client = new DefaultHttpClient();
            publishProgress(30);
            HttpGet get = new HttpGet(params[0]);
            final Bitmap bitmap;
            try {
                HttpResponse response = client.execute(get);
                bitmap = BitmapFactory.decodeStream(response.getEntity()
                        .getContent());
            } catch (Exception e) {
                return null;
            }
            publishProgress(100);
            return bitmap;
        }

        protected void onProgressUpdate(Integer... progress) {
            mProgressBar.setProgress(progress[0]);
        }

        protected void onPostExecute(Bitmap result) {
            if (result != null) {
                Toast.makeText(MainActivity.this, "成功获取图片", Toast.LENGTH_LONG)
                        .show();
                mImageView.setImageBitmap(result);
            } else {
                Toast.makeText(MainActivity.this, "获取图片失败", Toast.LENGTH_LONG)
                        .show();
            }
        }

        protected void onPreExecute() {
            mImageView.setImageBitmap(null);
            mProgressBar.setProgress(0);
        }

        protected void onCancelled() {
            mProgressBar.setProgress(0);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
}

     实际的效果如图:

    

      当咱们点击下载按钮的时候,就会启动下载图片的线程,主线程这里显示下载进度条,而后在下载成功的时候就会显示图片,这时咱们再点击按钮的时候就会清空图片,进度条也从新清零。

      仔细看上面的代码,咱们会发现不少有趣的东西。

      AsyncTask是为了方便编写后台线程与UI线程交互的辅助类,它的内部实现是一个线程池,每一个后台任务会提交到线程池中的线程执行,而后经过向UI线程的Handler传递消息的方式调用相应的回调方法实现UI界面的更新。

     AsyncTask的构造方法有三个模板参数:Params(传递给后台任务的参数类型),Progress(后台计算执行过程当中,进度单位(progress units)的类型,也就是后台程序已经执行了百分之几)和Result(后台执行返回的结果的类型)。

        protected Bitmap doInBackground(String... params) {
            publishProgress(0);
            HttpClient client = new DefaultHttpClient();
            publishProgress(30);
            HttpGet get = new HttpGet(params[0]);
            final Bitmap bitmap;
            try {
                HttpResponse response = client.execute(get);
                bitmap = BitmapFactory.decodeStream(response.getEntity()
                        .getContent());
            } catch (Exception e) {
                return null;
            }
            publishProgress(100);
            return bitmap;
        }

       params是一个可变参数列表,publishProgress()中的参数就是Progress,一样是一个可变参数列表,它用于向UI线程提交后台的进度,这里咱们一开始设置为0,而后在30%的时候开始获取图片,一旦获取成功,就设置为100%。中间的代码用于下载和获取网上的图片资源。

protected void onProgressUpdate(Integer... progress) {
    mProgressBar.setProgress(progress[0]);
}

      onProgressUpdate()方法用于更新进度条的进度。

protected void onPostExecute(Bitmap result) {
   if (result != null) {
       Toast.makeText(MainActivity.this, "成功获取图片", Toast.LENGTH_LONG).show();
       mImageView.setImageBitmap(result);
   } else {
       Toast.makeText(MainActivity.this, "获取图片失败", Toast.LENGTH_LONG).show();
   }
}

     onPostExecute()方法用于处理Result的显示,也就是UI的更新。

protected void onPreExecute() {
    mImageView.setImageBitmap(null);
    mProgressBar.setProgress(0);
}

protected void onCancelled() {
    mProgressBar.setProgress(0);
}

      这两个方法主要用于在执行前和执行后清空图片和进度。
      最后咱们只须要调用AsyncTask的execute()方法并将Params参数传递进来进行。完整的流程是这样的:

      UI线程执行onPreExecute()方法把ImageView的图片和ProgressBar的进度清空,而后后台线程执行doInBackground()方法,千万不要在这个方法里面更新UI,由于此时是在另外一条线程上,在使用publishProgress()方法的时候会调用onProgressUpdate()方法更新进度条,最后返回result---Bitmap,当后台任务执行完成后,会调用onPostExecute()方法来更新ImageView。

      AsyncTask本质上是一个静态的线程池,由它派生出来的子类能够实现不一样的异步任务,但这些任务都是提交到该静态线程池中执行,执行的时候经过调用doInBackground()方法执行异步任务,期间会经过Handler将相关的信息发送到UI线程中,但神奇的是,并非调用UI线程中的回调方法,而是AsyncTask自己就有一个Handler的子类InternalHandler会响应这些消息并调用AsyncTask中相应的回调方法。从上面的代码中咱们也能够看到,UI的ProgressBar的更新是在AsyncTask的onProgressUpdate(),而ImageView是在onPostExecute()方法里。这是由于InternalHandler实际上是在UI线程里面建立的,因此它可以调用相应的回调方法来更新UI。

      AsyncTask就是专门用来处理后台任务的,并且它针对后台任务的五种状态提供了五个相应的回调接口,使得咱们处理后台任务变得很是方便。

      若是只是普通的UI更新操做,像是不断更新TextView这种动态的操做,可使用Handler,但若是是涉及到后台操做,像是下载任务,而后根据后台任务的进展来更新UI,就得使用AsyncTask,但若是前者咱们就使用AsyncTask,那真的是太大材小用了!!

      要想真正理解好AsyncTask,首先就要理解不少并发知识,像是静态线程池这些难以理解的概念是必不可少的,做为新手,其实没有必要在实现细节上过度追究,不然很容易陷入细节的泥潭中,咱们先要明白它是怎么用的,等用得多了,就会开始思考为何它能这么用,接着就是怎么才能用得更好,这都是一个天然的学习过程,谁也没法越过,什么阶段就作什么事。所以,关于AsyncTask的讨论我就先放到一边,接下来的东西我也根本理解不了,又怎能讲好呢?

3.利用Runnable更新UI界面

      剩下的方法都是围绕着Runnable对象来更新UI。

      一些组件自己就有提供方法来更新本身,像是ProgressBar自己就有一个post()方法,只要咱们传进一个Runnable对象,就能更新它的进度。只要是继承自View的组件,均可以利用post()方法,并且咱们还可使用postDelay()方法来延迟执行该Runnable对象。android的这种作法就真的是让人称道了,至少我不用为了一个ProgressBar的进度更新就写出一大堆难懂的代码出来。

      还有另外一种利用Runnable的方式:Activity.runOnUiThread()方法。这名字实在是太直白了!!使用该方法须要新启一个线程:

class ProgressThread extends Thread {
        @Override
        public void run() {
            super.run();
            while (mProgress <= 100) {
                runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        mProgressBar.setProgress(mProgress);
                        mProgress++;
                    }
                });
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {


                }
            }
        }
    }

4.总结

     上面提供了三种思路来解决UI更新的问题,有些地方的讨论已经严重脱离标题,那也是没有办法,由于要说明一些概念,就必须涉及到并发的其余相关知识。方法不少,但它们都有本身适合的场合:

1.若是只是单纯的想要更新UI而不涉及到多线程的话,使用View.post()就能够了;

2.须要另开线程处理数据以避免阻塞UI线程,像是IO操做或者是循环,可使用Activity.runOnUiThread();

3.若是须要传递状态值等信息,像是蓝牙编程中的socket链接,就须要利用状态值来提示链接状态以及作相应的处理,就须要使用Handler + Thread的方式;

4.若是是后台任务,像是下载任务等,就须要使用AsyncTask。     原本只是由于蓝牙项目而开始这篇博客,但没想到在写的过程发现愈来愈多的东西,因而也一块儿写上来了,写得很差是必定的,由于是大三菜鸟,正在拼命加强本身薄弱的编程基础中,若是错误的地方,还但愿可以指点迷津。

相关文章
相关标签/搜索