为什么子线程中不能直接更新UI

点击上方“
dotNET全栈开发
”,“

设为星标

加“

星标



”,
每天11.50, 好文必达


全文约
4000

字,预计阅读时间



8


分钟

当初有同事就碰到类似的问题,于是就总结了一些,那时写这篇文章是我还在第一家公司。今天有人提到,之前在csdn发布过,我就重新修改了一下,发到微信。两年过去了,过去的同事不再交流问题,但问题仍然出现,交流的人换了几波!有些同事换了方向,而我仍在坚持xamarin。是难得还是无奈,我也不知道。反正奥力给,干就完事了!
文中的图已经掉,该错过就错过吧,见谅!

01

主线程也叫UI线程

当一个程序启动的时候,系统自动创建一个主线程,在这个主线程中,你的应用(app、winform等客户端程序)和UI组件发生交互,负责处理UI组件的各种事件,所以主线程也叫UI线程。

02

UI组件的更新一定要在UI线程里

android为了线程安全,不允许在UI线程外的子线程操作UI,这个结论不仅仅是说android,这个概念同样适用于其他的客户端系统,它 的好处时提高客户端UI的用户体验和执行效率(稍后解释),防止线程阻塞。在Java 原生的android中有两种方式更新UI线程

  • handler消息传递机制更新UI线程
  • AsyncTask异步任务更新UI线程

AsyncTask是Android提供的一个轻量级的用于处理异步任务的类,类似于C#中的Task

03

永远不要阻塞UI线程

刚刚说了UI组件的更新一定要在UI线程中,当我们在主线程中发起请求>请求的执行(http请求耗时)>请求完成填充数据,更新UI组件。这一过程一旦超过了10秒钟就会抛出ANR异常(Application Not Responding)应用程序员不响应,所以网络请求耗时的操作大多使用异步操作,早起异步Task相对麻烦,在.net 4.5中增加了新的特性await/async,使用await/async 就简化了很多。原则上的要求就是永远不要阻塞UI线程。
我们通过下面几个简单的示例逐步地学西和掌握如何在子线程中更新UI线程

  • ANR异常
  • 使用RunOnUIThread更新UI线程
  • 异步加载图片,在子线程中更新UI线程

04

4.1 阻塞UI线程并输入事件-模拟ANR异常

下面我们创建一个简单的登录程序,登录的时候使用Thread.Sleep(10000模拟耗时10秒钟,在这10秒钟内程序没有任何响应(如果你做了输入事件比如:触摸屏幕,按返回键),通俗说法就是卡界面了。代码如下,主要是了解ANR异常。

[Activity(Label = "LoginActivity", MainLauncher = true)]
    public class LoginActivity : Activity
    {
        private EditText et_name;
        private EditText et_pwd;
        private Button btn;
        protected override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
            SetContentView(Resource.Layout.Login);
            et_name = FindViewById(Resource.Id.et_name);
            et_pwd = FindViewById(Resource.Id.et_pwd);
            btn = FindViewById

4.2 ANR异常是如何产生的

ANR:Application Not Responding的缩写,当程序爆出“应用程序无响应”,系统会向用户显示一个对话框,“等待”可以让程序继续运行,“强制关闭”直接kill掉了。
这是值得注意的一点,记得上学的时候用的是酷派,虽然是充话费送的,但其实那手机配置还是可以,但是用就久了,很卡,打开一些app,长时间没有反应,我便在屏幕上点、按返回键等,于是就经常报这个“**程序没有响应”“等待”“退出”的一个对话框,好吧,其实用酷派手机卡了这些无关紧要。
在android程序中,程序的响应时由Activity manager和WindowManager系统服务监听的,主要是由以下两种情况造成的:

  • 在5秒外没有响应UI事件(点击屏幕,点击按钮,按返回键等),反之在5秒内比如Thread.sleep(5000)去点击屏幕,按返回键也不会报出ANR异常的。
  • BroadcaseReceiver在10秒内没有执行完毕

产生上面两种情况原因比较多,要注意的是即时是在UI线程中做了耗时的事情(5秒以上),如果用户没有触发屏幕的任何的事件,这时虽然UI线程阻塞了,也不会产生ANR。其实避免ANR异常原则要求还是那句话” 不要再UI线程上做耗时的事情

05

使用RunOnUIThread更新UI线程

大家一定使用过Timer,Timer对象会开启多个线程,但最少不止一个。下面这个例子,将演示两个timer,1秒钟更新一次,对比一下两个TextView的显示的时间。

[Activity(Label = "Xamarin_android", MainLauncher = true, Icon = "@drawable/icon")]
    public class TimerActivity : Activity
    {
        private TextView tv_test;
        private TextView tv_test1;
        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.Main);
            tv_test = FindViewById(Resource.Id.tv_test);
            tv_test1 = FindViewById(Resource.Id.tv_test1);
            tv_test.Text = "现在的时间是" + DateTime.Now.ToString("yyyy年MM月dd日 HH:mm:ss");
            tv_test1.Text = "现在的时间是" + DateTime.Now.ToString("yyyy年MM月dd日 HH:mm:ss");

            System.Diagnostics.Debug.Write("主线程"+Thread.CurrentThread.ManagedThreadId);
            System.Timers.Timer timer = new System.Timers.Timer(10000);
            timer.Elapsed += delegate
            {
                System.Diagnostics.Debug.Write("timer线程"+Thread.CurrentThread.ManagedThreadId);
                RunOnUiThread(()=> {
                    tv_test.Text = "现在的时间是" + DateTime.Now.ToString("yyyy年MM月dd日 HH:mm:ss");
                });
            };
            timer.Enabled = true;

            System.Diagnostics.Debug.Write("主线程" + Thread.CurrentThread.ManagedThreadId);
            System.Timers.Timer timer1 = new System.Timers.Timer(10000);
            timer1.Elapsed += delegate
            {
                System.Diagnostics.Debug.Write("timer1线程" + Thread.CurrentThread.ManagedThreadId);
                tv_test1.Text = "现在的时间是" + DateTime.Now.ToString("yyyy年MM月dd日 HH:mm:ss");
            };
            timer1.Enabled = true;
        }
    }

通过这段代码说明两个问题:

  • 1.timer会开启至少一个线程
  • 2.tv_test的时间是1秒更新一次,tv_test1的时间不会更新,在子线程中无法直接更新UI。

xamarin android中子线程更新UI线程的方法就是RunOnUIThread,该方法参数是一个无参无返回值的委托。

06

异步加载图片,在子线程中更新UI线程

我们已经知道子线程中更新UI的使用方法是RunOnUIThread ,下面这个例子使用异步加载图片,异步的重点是开启子线程。
关于http请求的库,microsoft封装的库在命名空间System.NET.Http,这里演示的是第三方的http请求库RestSharp,你可以在nuget上添加引用。

[Activity(Label = "Xamarin_android", MainLauncher = true, Icon = "@drawable/icon")]    public class AsyncLoadImageActivity : Activity
    {        private ImageView  img;        private Button btn;        private TextView tv_result;        private Bitmap bitmap;        private Button btn_test;        private int noBlock_number;        protected override void OnCreate(Bundle bundle)
        {            base.OnCreate(bundle);
            SetContentView(Resource.Layout.AsyncLoadImage);
            btn = FindViewById

从运行的结果我们可以看到,UI线程ID和异步方法中的回调方法子线程ID不一样,使用异步方法不会阻塞UI线程,执行耗时请求图片方法时,任然可以点击按钮,输入其他的事件。