目录
- 1 传统更新UI的七种方式
- 1.1 new Handler()
- 1.2 new Handler.Callback()
- 1.3 new Handler().post(Runnable r)
- 1.4 new Handler().postDelayed(Runnable r, long delayMillis)
- 1.5 Activity.runOnUiThread(Runnable action)
- 1.6 View.post(Runnable action)
- 1.7 View.postDelayed(Runnable action, long delayMillis)
- 2 在子线程中更新UI
- 2.1 先执行一次requestLayout()再更新
- 2.2 在子线程中调用windowManager.addView()创建一个ViewRootImpl
- 2.3 让布局的宽高属性为固定值,并开启硬件加速
- 2.4 使用绘制流程不走checkThread()的SurfaceView来更新
- 3 对比分析
- 3.1 在onCreate()中直接更新
- 3.2 子线程休眠后再更新
- 3.3 将TextView改成wrap_content
- 4 测试动画
1 传统更新UI的七种方式
1.1 new Handler()
Button button = new Button(this);
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == 1) {
button.setText("子线程更新UI");
}
}
};
new Thread(new Runnable() {
@Override
public void run() {
// Message和Handler均可获得msg
// Message msg = handler.obtainMessage();
Message msg = Message.obtain();
msg.what = 1;
msg.arg1 = 10;
handler.sendMessage(msg);
}
}).start();
1.2 new Handler.Callback()
Button button = new Button(this);
private Handler.Callback callback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == 1) {
button.setText("子线程更新UI");
}
return true;
}
};
Handler handler = new Handler(callback);
new Thread(new Runnable() {
@Override
public void run() {
// Message和Handler均可获得msg
// Message msg = handler.obtainMessage();
Message msg = Message.obtain();
msg.what = 1;
msg.arg1 = 11;
handler.sendMessage(msg);
}
}).start();
1.3 new Handler().post(Runnable r)
Button button = new Button(this);
new Thread(new Runnable() {
@Override
public void run() {
new Handler().post(new Runnable() {
@Override
public void run() {
button.setText("子线程更新UI");
}
});
}
}).start();
1.4 new Handler().postDelayed(Runnable r, long delayMillis)
Button button = new Button(this);
new Thread(new Runnable() {
@Override
public void run() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
button.setText("子线程更新UI");
}
}, 3000);
}
}).start();
1.5 Activity.runOnUiThread(Runnable action)
Button button = new Button(this);
new Thread(new Runnable() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
button.setText("子线程更新UI");
}
});
}
}).start();
1.6 View.post(Runnable action)
Button button = new Button(this);
new Thread(new Runnable() {
@Override
public void run() {
button.post(new Runnable() {
@Override
public void run() {
button.setText("子线程更新UI");
}
});
}
}).start();
1.7 View.postDelayed(Runnable action, long delayMillis)
Button button = new Button(this);
new Thread(new Runnable() {
@Override
public void run() {
button.postDelayed(new Runnable() {
@Override
public void run() {
button.setText("子线程更新UI");
}
}, 3000);
}
}).start();
其它异步更新UI的方法如AsyncTask、EventBus等框架还有很多。
2 在子线程中更新UI
在子线程中更新UI的方法:
1、先执行一次requestLayout()再更新
2、在子线程中调用windowManager.addView()创建一个ViewRootImpl
3、让布局的宽高属性为固定值,并开启硬件加速
4、使用绘制流程不走checkThread()的SurfaceView来更新
代码中的注释很详细,未全部贴到正文中,一定要看注释啊。
先上概览图
2.1 先执行一次requestLayout()再更新
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:text="Hello"
android:textSize="22sp" />
</FrameLayout>
package com.example.customview.layoutdrawprocess
import android.annotation.SuppressLint
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.example.customview.R
import kotlin.concurrent.thread
/**
* 布局文件中TextView的宽高可随意设置,都不报错。
*
* 在子线程中更新UI报错的主要原因是
* ViewRootImpl.java -> requestLayout() -> checkThread(),解决方案一:
* 调用两次requestLayout(),即先在主线程中显示调用一次,
* 修改UI时会自动再调用一次(隐式调用)但不会再执行。
*
* 方案解释:
* 如果当前ViewRootImpl.java正在处理一个requestLayout()的任务,
* 再次触发requestLayout()时将不会被执行,相当于第二次触发时绕开了checkThread()。
*/
class RequestBeforeActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_wrap_text)
val textView = findViewById<TextView>(R.id.textView)
textView.setOnClickListener {
/*
等价于it.requestLayout()。
这行代码会在主线程执行,会执行requestLayout的整个流程,这样就完成了“申请”修改布局。
此时,在⼦线程⽴即调⽤ textView.text = "xx.." 这个代码就会因为它已经"申请"过requestLayout了,
就不会层层往上调⽤parent的requestLayout()⽅法,也就不会在⼦线程触发checkThread()⽅法了。
*/
// textView.text = "Main"
// 在主线程中执行,触发scheduleTraversals()
it.requestLayout()
thread {
// 子线程修改UI会调用requestLayout()但不会执行,因此绕开了checkThread()。
textView.text = "先执行requestLayout()再更新"
}
}
}
}
2.2 在子线程中调用windowManager.addView()创建一个ViewRootImpl
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
</FrameLayout>
package com.example.customview.layoutdrawprocess
import android.annotation.SuppressLint
import android.graphics.Color
import android.os.Bundle
import android.os.Looper
import android.os.SystemClock
import android.view.WindowManager
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import com.example.customview.R
import kotlin.concurrent.thread
/**
* 本例中布局文件不含子View,只有一个FrameLayout。
*
* 在子线程中更新UI报错的主要原因是
* ViewRootImpl.java -> requestLayout() -> checkThread(),解决方案二:
* 在子线程中调用windowManager.addView()创建一个ViewRootImpl,
* 运行到checkThread()时不会进入它的 if (mThread != current)就不报错了。
* 但ViewRootImpl中有Handler,因此调用addView()之前需要创建looper,详见代码。
*/
class AddViewWithoutMainThreadActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add_view_without_main_thread)
thread {
Looper.prepare()
val button = Button(this)
button.setBackgroundColor(Color.MAGENTA)
button.text = "在子线程中添加View:I will be added in child thread."
button.isAllCaps = false
button.setOnClickListener {
(it as Button).text =
"${Thread.currentThread().name}, ${SystemClock.uptimeMillis()}"
}
windowManager.addView(button, WindowManager.LayoutParams().apply {
this.width = WindowManager.LayoutParams.WRAP_CONTENT
this.height = WindowManager.LayoutParams.WRAP_CONTENT
})
Looper.loop()
}
}
}
2.3 让布局的宽高属性为固定值,并开启硬件加速
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:gravity="center"
android:text="Hello"
android:textSize="22sp" />
</FrameLayout>
package com.example.customview.layoutdrawprocess
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.example.customview.R
import kotlin.concurrent.thread
/**
* 布局文件中TextView的宽高是固定值时不报错,是wrap_content、match_parent则会报错。
*
* 在子线程中更新UI报错的主要原因是
* ViewRootImpl.java -> requestLayout() -> checkThread(),解决方案三:
* 当布局文件中子View(TextView)的宽、高属性都是固定值时,在View中开启硬件加速(默认开启),
* 不会触发requestLayout(),只会触发invalidate(),也绕开了checkThread()。
*
* 如果在AndroidManifest.xml中的<application>或<activity>标签中关闭硬件加速,
* 则在子线程中修改UI将失败,会报checkThread()中定义的CalledFromWrongThreadException。
*
* 为什么会报错:
* 在ViewGroup.java -> invalidateChild()中可以看到,是否开启硬件加速会有不同的处理逻辑。
* 而开启硬件加速后,TextView.java -> checkForRelayout() -> invalidate()会链接到ViewRootImpl.java -> invalidate()。
* ViewRootImpl.java -> invalidate()与ViewRootImpl.java -> requestLayout()的区别是它不会调用checkThread()。
*
* 结论:
* 如果布局文件中子View(如TextView)的布局(宽高)没有发生改变(固定值),当只有内容发生改变时,
* 在启用硬件加速的情况下是不会报错的,因为它不会触发requestLayout(),只会触发invalidate()来刷新。
*
* View的布局没有发生改变是指什么呢?
* ViewRootImpl.java -> performLayout() -> measureHierarchy()会测量View树。
* measureHierarchy()会传入一个WindowManager.LayoutParams参数,
* 而WindowManager.java -> LayoutParams()的构造方法会先调用super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
* measureHierarchy()内部也会判断lp.width == ViewGroup.LayoutParams.WRAP_CONTENT进而走不同的逻辑,
* 当布局文件中子View宽高都是wrap_content或match_parent时该例子会报错。
*
* DecorView是最顶层View,而DecorView的测量与绘制又是由ViewRootImpl完成,
* ViewRootImpl实现了测量performMeasure()、布局performLayout()、绘制performDraw()各流程。
* ViewRootImpl.java -> draw()会判断是否开启硬件加速并给出相应处理。
*/
class OnClickActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_on_child_thread)
val textView = findViewById<TextView>(R.id.textView)
textView.setOnClickListener {
thread {
textView.text = "在点击事件中更新"
}
}
}
}
2.4 使用绘制流程不走checkThread()的SurfaceView来更新
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/surface"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_gravity="center" />
</FrameLayout>
package com.example.customview.layoutdrawprocess
import android.annotation.SuppressLint
import android.app.Activity
import android.graphics.Color
import android.os.Bundle
import android.os.SystemClock
import android.view.SurfaceHolder
import android.view.SurfaceView
import androidx.appcompat.app.AppCompatActivity
import com.example.customview.R
import java.util.Random
import kotlin.concurrent.thread
/**
* 布局文件中SurfaceView的宽高可随意设置,都不会报错。
*
* 在子线程中更新UI报错的主要原因是
* ViewRootImpl.java -> requestLayout() -> checkThread(),解决方案四:
* Android中有⼀个控件SurfaceView,它可以通过holder获得Canvas对象,
* 可以直接在⼦线程中更新 UI。
* SurfaceView的绘制流程不走checkThread(),因此可以直接在子线程中更新UI。
*/
@SuppressLint("SetTextI18n")
class SampleSurfaceViewActivity : AppCompatActivity() {
var destroyed = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_surface_view)
val surface = findViewById<SurfaceView>(R.id.surface)
surface.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
thread {
while (!destroyed) {
val canvas = holder.lockCanvas()
val random = Random()
val r = random.nextInt(255)
val g = random.nextInt(255)
val b = random.nextInt(255)
canvas.drawColor(Color.rgb(r, g, b))
holder.unlockCanvasAndPost(canvas)
SystemClock.sleep(500)
}
}
}
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
})
}
}
3 对比分析
3.1 在onCreate()中直接更新
xml文件同《2.3 让布局的宽高属性为固定值,并开启硬件加速》
package com.example.customview.layoutdrawprocess
import android.annotation.SuppressLint
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.example.customview.R
import kotlin.concurrent.thread
/**
* 布局文件中TextView的宽高可随意设置,都不会报错。
*/
class OnChildThreadActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_on_child_thread)
val textView = findViewById<TextView>(R.id.textView)
/*
子线程中修改UI成功
ViewRootImpl在Activity处于onResume()之后才被创建的。
在onCreate()中,此时ViewRootImpl还没有被创建,
所以不会执行checkThread(),自然不会报错。
*/
thread {
textView.text = "在onCreate中直接更新"
}
}
}
3.2 子线程休眠后再更新
xml文件同《2.3 让布局的宽高属性为固定值,并开启硬件加速》
package com.example.customview.layoutdrawprocess
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.SystemClock
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.example.customview.R
import kotlin.concurrent.thread
/**
* 布局文件中TextView的宽高是固定值时不报错,是wrap_content、match_parent则会报错。
*
* 见解决方案三
*/
class ChangeWithSleepActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_on_child_thread)
val textView = findViewById<TextView>(R.id.textView)
/*
当布局是非固定值时,此时进行耗时操作,则ViewRootImpl已经创建成功,
会执行checkThread(),所以程序会崩溃。
布局是固定值时,参考解决方案三。
*/
thread {
SystemClock.sleep(2000)
textView.text = "子线程休眠后再更新"
}
}
}
3.3 将TextView改成wrap_content
xml文件同《2.1 先执行一次requestLayout()再更新》
package com.example.customview.layoutdrawprocess
import android.annotation.SuppressLint
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.example.customview.R
import kotlin.concurrent.thread
/**
* 布局文件中TextView的宽高都是固定值不报错,是wrap_content、match_parent则会报错。
*
* 见解决方案三
*/
class WrapTextActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_wrap_text)
val textView = findViewById<TextView>(R.id.textView)
textView.setOnClickListener {
/*
子线程中修改UI 失败: android.view.ViewRootImpl$CalledFromWrongThreadException:
*/
thread {
textView.text = "将TextView的属性改成wrap_content"
}
}
}
}
项目的布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<Button
android:id="@+id/btn1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="在onCreate()中直接更新"
android:textAllCaps="false"
android:textSize="22sp" />
<Button
android:id="@+id/btn2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="子线程休眠后再更新"
android:textAllCaps="false"
android:textSize="22sp" />
<Button
android:id="@+id/btn3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="3.在点击事件中更新"
android:textAllCaps="false"
android:textSize="22sp" />
<Button
android:id="@+id/btn4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="将TextView改成wrap_content"
android:textAllCaps="false"
android:textSize="22sp" />
<Button
android:id="@+id/btn5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="1.先执行requestLayout()再更新"
android:textAllCaps="false"
android:textSize="22sp" />
<Button
android:id="@+id/btn6"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="2.在子线程中addView()"
android:textAllCaps="false"
android:textSize="22sp" />
<Button
android:id="@+id/btn7"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="4.使用SurfaceView"
android:textAllCaps="false"
android:textSize="22sp" />
</LinearLayout>
MainActivity文件
package com.example.customview
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.customview.layoutdrawprocess.AddViewWithoutMainThreadActivity
import com.example.customview.layoutdrawprocess.ChangeWithSleepActivity
import com.example.customview.layoutdrawprocess.OnChildThreadActivity
import com.example.customview.layoutdrawprocess.OnClickActivity
import com.example.customview.layoutdrawprocess.RequestBeforeActivity
import com.example.customview.layoutdrawprocess.SampleSurfaceViewActivity
import com.example.customview.layoutdrawprocess.WrapTextActivity
import kotlinx.android.synthetic.main.activity_draw_process.btn1
import kotlinx.android.synthetic.main.activity_draw_process.btn2
import kotlinx.android.synthetic.main.activity_draw_process.btn3
import kotlinx.android.synthetic.main.activity_draw_process.btn4
import kotlinx.android.synthetic.main.activity_draw_process.btn5
import kotlinx.android.synthetic.main.activity_draw_process.btn6
import kotlinx.android.synthetic.main.activity_draw_process.btn7
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_draw_process)
btn1.setOnClickListener {
startActivity(Intent(this, OnChildThreadActivity::class.java))
}
btn2.setOnClickListener {
startActivity(Intent(this@MainActivity, ChangeWithSleepActivity::class.java))
}
btn3.setOnClickListener {
startActivity(Intent(this@MainActivity, OnClickActivity::class.java))
}
btn4.setOnClickListener {
startActivity(Intent(this@MainActivity, WrapTextActivity::class.java))
}
btn5.setOnClickListener {
startActivity(Intent(this@MainActivity, RequestBeforeActivity::class.java))
}
btn6.setOnClickListener {
startActivity(Intent(this@MainActivity, AddViewWithoutMainThreadActivity::class.java))
}
btn7.setOnClickListener {
startActivity(Intent(this@MainActivity, SampleSurfaceViewActivity::class.java))
}
}
}
4 测试动画
参考文献:
[1] 扔物线官网
微信公众号:TechU