您的位置:首页 > 产品设计 > UI/UE

【小学生学Android(1)】我们来谈谈Android的线程安全以及只有主线程才能修改UI

2016-11-29 15:34 507 查看
参考文章:

http://blog.csdn.net/zhaokaiqiang1992/article/details/43410351

http://blog.csdn.net/aigestudio/article/details/43449123

http://blog.csdn.net/happy_horse/article/details/52529140

https://www.zhihu.com/question/24764972

http://blog.csdn.net/xyh269/article/details/52728861

我们知道,Android的UI操作不是线程安全的,“只有主线程才能操作UI”,同时主线程对UI操作有一定的时间限制(最长5秒),所以一些费时的操作应该交给独立的线程(子线程)来执行。

这里涉及到几个概念先来解释一下,熟悉的老司机可以直接跳过啦。(PS:话说老司机也不会来这看我这小学生的博客呀,嘿嘿~ 木事儿,我就YY一下,无伤大雅)

1.线程安全VS线程不安全

首先明确一个概念性的问题,线程安全与否说的是方法,而不是线程。可以说某个方法是线程安全的,而不能说某个线程是安全的。线程没有安全与不安全之说。

线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用.这样做的好处是,不会出现数据不一致或者数据污染。

线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

2.UI操作不是线程安全的

Android中的View提供了invalidate()方法实现界面刷新,但是这个方法是线程不安全的。参见源码:

/**
* This must be called from a UI thread. To call from a non-UI thread, call {@link #postInvalidate()}.
*/
public void invalidate() {
invalidate(true);
}


注解说的很清楚“This must be called from a UI thread.”这个方法只能UI线程调用,所以说更新UI操作是线程不安全的。对吗?

对也不对。

对-结论是对的。

不对-注解说了非UI线程可以调用postInvalidate()方法啊“To call from a non-UI thread, call postInvalidate()”看到这名字有没想起什么来?post打头的方法其常用的不多哟。我先想到的是http请求的post(),其次就是handler的post()和postDelayed().http请求显然不相关,那么跟handler有没有关系呢?看下源码:

public void postInvalidateDelayed(long delayMilliseconds) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
}
}


这里判断不为空,调用了 ViewRootImpl的dispatchInvalidateDelayed()方法

并没有handler啊,别急 我们接着看这个dispatchInvalidateDelayed()

public void dispatchInvalidateDelayed(View view, long delayMilliseconds){
Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
mHandler.sendMessageDelayed(msg, delayMilliseconds);
}


看到这个mHandler有没觉得倍感亲切呢,哈哈,sendMessageDelayed() 这个方法想必大家闭着眼睛都会写吧?!

结论:

1).postInvalidate()方法虽然可以从子线程调用,但是其本质还是通过handler发送到主线程执行的.

2).invalidate()只能UI线程调用.

3).UI操作不是线程安全的

3.只有主线程才能操作UI

在非UI线程中调用invalidate()可能会导致显示异常,可能在非UI线程中刷新界面的时候,其他非UI线程也在刷新界面,这样就导致多个界面刷新的操作不能同步,导致显示异常.

这里的“只有主线程才能操作UI”为什么要加引号呢,首先加引号并不到表这句话不正确,那还了得!这么多年坚信的真理,哪能说破就破?之所以这么说,之所以会有这篇文章,是收到了文章开头提到的博客的启发。这里我贴下我的Demo核心代码,我们来做个试验。马克思教导我们,“实践是检验真理的唯一标准!”如果是真理就能经得住实践的检验。闲话不说,上代码。

activity_thread_ui.xml布局 TextView的文本是“Hello World!”

<Button
android:id="@+id/btnUpdate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="子线程 更新UI" />

<TextView
android:id="@+id/tvText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World" />


ThreadUIActivity代码

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_thread_ui);
new UpdateThread().start();//
}
class UpdateThread extends Thread {
@Override
public void run() {
setText();
}
}
private void setText() {
tvText.setText("Update Yeah!");
}


猜想下运行结果,会报错吗?CalledFromWrongThreadException?Only the original thread that created a view hierarchy can touch its views.?并没有!运行截图:



我这里简单解释下原因。

1).CalledFromWrongThreadException是在invalidate()时,调用ViewRootImpl的checkThread()方法时候抛出的。

void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}


2)在onCreate()方法中View还没画出来,开启子线程,调用setText(),相当于只是设置TextView的一个属性,并不会调用invalidate(),也就不会调用ViewRootImpl的checkThread(),也就不会报错。

3)进一步解释是这样的,“一个APP然可以有多个Activity,但是每个Activity只会有一个Window一个DecorView一个ViewRootImpl。并且 ViewRootImpl是在onResume()方法被执行后才会被生成。”所以在onCreate()的时候 ViewRootImpl 都还没有创建呢,包括它的checkThread(), 此时子线程更新TextView的setText()能顺利执行。

哈哈,想必我这小学生的表述会有人没看懂,没关系,咱们有外援啊,怕啥!以上3点大家有疑问的话,推荐大家去看赵凯强和爱哥的大作,分析的很到位哟!必要的话留个言,咱们一起交流学习哟。

http://blog.csdn.net/zhaokaiqiang1992/article/details/43410351

http://blog.csdn.net/aigestudio/article/details/43449123

这里我想跟着爱哥的思路,进一步解放思想。

1).“一个APP然可以有多个Activity,但是每个Activity只会有一个Window一个DecorView一个ViewRootImpl。并且 ViewRootImpl是在onResume()方法被执行后才会被生成。”

2)“扩展一下问题一个APP是否可以拥有多个根视图呢?答案是肯定的。但是!在framework的默认实现中有且仅有一个根视图,那就是DecorView.”

我们换个角度来讲理解上面的话:

a).一个APP只有一个ViewRootImpl,它是在主线程创建的。

ViewRootImpl的checkThread()方法,检查currentThread 是否是主线程,不是抛出CalledFromWrongThreadException异常。

这里的主线程 就是创建该ViewRootImpl的线程*。

请教了一下教我们语文的数学老师,这句话还可以这么说:

ViewRootImpl的checkThread()方法,检查currentThread 是否是创建该ViewRootImpl的线程。

b).那这个ViewRootImpl 能不能由子线程创建呢?如果能,那么checkThread()在“检查currentThread 是否是创建该ViewRootImpl的线程”就不会再抛异常了,这是显而易见的。查了下资料,喜忧参半。忧的是 这个ViewRootImpl是个隐藏类,不能直接在代码中创建;喜的是我们还可以“曲线救国”通过WindowManager的addView()来间接创建ViewRootImpl。(这里为什么“addView() 可以创建ViewRootImpl”,挖个坑,回头再填)那么,下面上代码。

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_thread_ui);
new UpdateThread().start();//
ButterKnife.bind(this);
initSources();
}
private TextView tx;
private WindowManager wm;
private WindowManager.LayoutParams params;
private int num;
private boolean hasAdded;

private void initSources() {
tx = new TextView(ThreadUIActivity.this);
wm = ThreadUIActivity.this.getWindowManager();//获取WindowManager
//创建LayoutParams,暂不关心这个参数,主要设置显示风格 透明度 显示位置等
params = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
0, 100, WindowManager.LayoutParams.FIRST_SUB_WINDOW,
WindowManager.LayoutParams.TYPE_TOAST, PixelFormat.TRANSLUCENT);
}
class UpdateThreadWithPrepareManager extends Thread {
@Override
public void run() {
Looper.prepare();//这里需要Looper 否则异常
addWindow();
Looper.loop();
}
}
private void addWindow() {
if (hasAdded) {
wm.removeView(tx);
}
num++;
tx.setText("Add Yeah! " + num);
wm.addView(tx, params);//addView 间接创建ViewRootImpl
hasAdded = true;
}


颤抖吧,骚年!来看下效果!



可以多次添加.每次计数器+1



分析:

1.通过在子线程里调用WindowManager的addView()方法,间接创建ViewRootImpl.这种情况下,ViewRootImpl的checkThread() 检查当前线程就是创建ViewRootImpl的线程,所以并不会异常。

2.同一个View 只能添加一次,如果需要反复add同一个View ,需先调用remove()方法。这就是hasAdded存在的意义。

总结一下:

1.Android的UI操作遵循单线程模型,其刷新View的方法invalidate()

是线程不安全的。

2.在进行UI操作的时候,ViewRootImpl的chekThread()方法负责检查当前线程是否是创建ViewRootImpl的线程,如果不是抛出CalledFromWrongThreadException异常。

3.默认情况下“每个Activity只会有一个Window一个DecorView一个ViewRootImpl。它是在主线程创建的。

4.子线程也可以创建ViewRootImpl,这样其chekThread()方法怎么检查也不会异常!这样我们就可通过该ViewRootImpl实现对UI的更新。

保持一颗好奇心,去探索Android OS,我是旋涡小学生,欢迎各位大神批评指正。谢谢
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android ui 线程