Android编程示例之——UI主线程以及多线程

本文探讨了Android中非UI线程操作UI元素的问题,通过一个编程示例解释了为何有时这种操作不会立即抛出CalledFromWrongThreadException异常。分析了TextView::setText的执行路径,揭示了UI修改线程和数据修改线程的并发执行可能导致的时序错过现象。解决问题的方法是确保数据修改线程使用UI主线程的Looper。
摘要由CSDN通过智能技术生成

前几天看到一个帖子,有位朋友列出了一段代码1:

/********************代码1:在非UI线程里面操作UI*****************************/

class myThread extends Thread {
private Handler myHandler;
public void run() {
Looper myLooper, mainLooper;
Looper.prepare();
myLooper = Looper.myLooper();
mainLooper = Looper.getMainLooper();
String obj;
if (myLooper == null) {
Log.d("ALOOP", "myLooper == null " + Thread.currentThread().getId());
} else {
myHandler = new Handler(myLooper) {//DebugPoint 2
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case MSG_TIMER2:
String obj1 = "MSG_TIMER MainThread";
Log.d("ALOOP",
"Print1  myHandler::handleMessage begin thread id= "
+ Thread.currentThread().getId());
vText.setText(obj1);
Log.d("ALOOP",
"Print2 myHandler::handleMessage end thread id= "
+ Thread.currentThread().getId());
break;
}
}
};
myHandler.sendEmptyMessage(MSG_TIMER2);
}
Looper.loop();
};
}

       显然,这段代码是在在非UI线程里面操作UI。Android 的UI元素不是线程安全的。也就是说,不应该在非UI线程里面操作UI元素。

       但是这位朋友很困惑的是,这段代码确实运行正常。而且运行了若干次依然正常。

       其实多线程编程,有时候你运行若干次,结果正确,并不表明你的逻辑就是对的。

       那为什么这里运行多次一直正常呢?根据作者的以往的调试经验,这类代码通常会在运行的时候报

 CalledFromWrongThreadException: “Only the original thread that created a view hierarchy can touch its views.” 。可是这段代码我运行多次,确实没有发现这个异常出来。

      为了刨根究底, 我们不妨跟着vText.setText代码往下看,看看这段代码的执行路径:

      TextView::setText

      View::invalidate

      ViewRoot::invalidateChild

      作者在 View::invalidate里面加入了打印信息,发现在myHandler.handleMessage函数的打印信息“Print1”和“Print1”之间,

判断条件mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)是不成立的,也就是说并没有调用p.invalidateChild(this, r)。

    public void invalidate() {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.INVALIDATE);
        }
        if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {
            mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;
            final ViewParent p = mParent;
            final AttachInfo ai = mAttachInfo;
            if (p != null && ai != null) {
                final Rect r = ai.mTmpInvalRect;
                r.set(0, 0, mRight - mLeft, mBottom - mTop);
                // Don't call invalidate -- we don't want to internally scroll
                // our own bounds
                p.invalidateChild(this, r);
            }
        }
    }

    p.invalidateChild的调用路径是: 

    View::invalidateChild
    ViewGroup::invalidateChild
    ...(ViewGroup::invalidateChild若干)
    ViewGroup::invalidateChild
    ViewRoot::invalidateChild
    ViewRoot::checkThread //这个地方会抛出CalledFromWrongThreadException

     所以既然在 “Print1”和“Print1”之间没有调用p.invalidateChild,那自然就不会抛出CalledFromWrongThreadException的异常。

    从实验结果来看,这里调用的  TextView::setText,仅仅在于修改了TextView::mText的成员变量。但是并未对TextView 发起一次刷新。也就是说,并没有完成对textView的修改,修改的仅仅是他的模型(数据)。可是做过实验的朋友会发现TextView的文本在这句话之后界面确实会显示“MSG_TIMER MainThread” 。这个界面(视图)的显示其实不是TextView::setText的结果,而是Activity::setContentView的结果。

    这部分代码的实际运行路径分为两条线,UI修改线和textView数据修改线。

    UI修改线程:

    Activity::setContentView                             

    ViewRoot::invalidateChild

    textView数据修改线程:

    TextView::setText

    View::invalidate(并未实际修改UI)

    ViewRoot::invalidateChild(因为状态不对,没有进入)

    这两条线是并行进行的。UI修改线程会将mPrivateFlags 置为DRAWN | HAS_BOUNDS。但是“textView数据修改线程”的执行路径很短,所以他很可能在mPrivateFlags被UI修改线程修改之前到达View::invalidate,从而导致判断的失败,代码也并没有进入ViewRoot::invalidateChild。

    所以,如果我们在代码vText.setText(obj1);之前加上一个延时,让textView数据修改线程稍微等待UI修改线程一会会,系统基本上会一直报异常CalledFromWrongThreadException:

try {//DebugPoint 3

Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

  vText.setText(obj1);

   综上,这个代码本身是有问题的。之所以没有报异常,是UI线程和数据修改线程在时序上错过了。

   要根本上解决这个问题,就要把数据修改线程的Looper修改为UI主线程的Looper:

   //myHandler = new Handler(mainLooper) { //DebugPoint 1   

    

  完整代码如下: 

/*TempTestActivity.java*/

package com.test;


import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.widget.TextView;


public class TempTestActivity extends Activity {
private final int MSG_TIMER2 = 14;


private TextView vText = null;
myThread t;


@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
vText = (TextView) findViewById(R.id.tv01);
//t.myHandler.sendEmptyMessage(MSG_TIMER2);
//vText.setText("adnb");
t = new myThread();
t.start();
}
class myThread extends Thread {
private Handler myHandler;


public void run() {
//Looper myLooper, 
Looper mainLooper;
//Looper.prepare();
//myLooper = Looper.myLooper();
mainLooper = Looper.getMainLooper();
String obj;
//if (myLooper == null) {
Log.d("ALOOP", "myLooper == null " + Thread.currentThread().getId());
//} else {
myHandler = new Handler(mainLooper) { //DebugPoint 1
//myHandler = new Handler(myLooper) {//DebugPoint 2
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case MSG_TIMER2:
String obj1 = "MSG_TIMER MainThread";
Log.d("ALOOP",
"myHandler::handleMessage begin thread id= "
+ Thread.currentThread().getId());
/*
try { //DebugPoint 3
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
*/
vText.setText(obj1);
//vText.setVisibility(View.VISIBLE);
//vText.invalidate();
Log.d("ALOOP",
"myHandler::handleMessage end thread id= "
+ Thread.currentThread().getId());
break;
}
}
};
myHandler.sendEmptyMessage(MSG_TIMER2);
//}
//Looper.loop();
};
}
}

主要Layout文件: 

 <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/LinearLayoutTestInval"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<TextView  
android:id="@+id/tv01"
    android:layout_width="fill_parent" 
    android:layout_height="wrap_content" 
    android:text="@string/hello"
    />
</LinearLayout>

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值