隐藏

xamarin android异步更新UI线程

发布:2019/4/14 14:00:57作者:管理员 来源:本站 浏览次数:1203

xamarin android异步更新UI线程


    UI线程简单了解

一些从事web开发的同学,可能对UI线程没有这个概念,没办法,毕竟“UI线程”这个概念只存在一些客户端(window客户端软件、app等)。下面就来简单地了解一下一下这几个概念,理解起来比用起来更简单。其实android在子线程中更新UI线程,一个方法就欧了,RunOnUiThread(System.Action action),这篇写给小白的,如果知道怎么使用这个方法,就没必要看下面的了。
主线程也叫UI线程
当一个程序启动的时候,系统自动创建一个主线程,在这个主线程中,你的应用(app、winform等客户端程序)和UI组件发生交互,负责处理UI组件的各种事件,所以主线程也叫UI线程。
UI组件的更新一定要在UI线程里
android为了线程安全,不允许在UI线程外的子线程操作UI,这个结论不仅仅是说android,这个概念同样适用于其他的客户端系统,它 的好处时提高客户端UI的用户体验和执行效率(稍后解释),防止线程阻塞。在java 原生的android中有两种方式更新UI线程
- handler消息传递机制更新UI线程,http://www.runoob.com/w3cnote/android-tutorial-handler-message.html
- AsyncTask异步任务更新UI线程http://www.runoob.com/w3cnote/android-tutorial-ansynctask.html
AsyncTask是Android提供的一个轻量级的用于处理异步任务的类,只能说有点类似于C#中的Task
android程序中使用异步避免ANR异常
刚刚说了UI组件的更新一定要在UI线程中,当我们在主线程中发起请求>请求的执行(http请求耗时)>请求完成填充数据,更新UI组件。这一过程一旦超过了10秒钟就会抛出ANR异常(Application Not Responding)应用程序员不响应,所以网络请求耗时的操作大多使用异步操作,早起异步Task相对麻烦,在.net 4.5中增加了新的特性await/async,使用await/async 就简化了很多。原则上的要求就是永远不要阻塞UI线程。

我们通过下面几个简单的示例逐步地学西和掌握如何在子线程中更新UI线程
1. 模拟ANR异常
2. 使用Timer对象开启子线程,在子线程使用RunOnUIThread更新UI线程
3. 异步加载图片,在子线程中更新UI线程

    阻塞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<EditText>(Resource.Id.et_name);
            et_pwd = FindViewById<EditText>(Resource.Id.et_pwd);
            btn = FindViewById<Button>(Resource.Id.btn);
            btn.Click += (s, e) =>
            {
                Client.Client_Login(et_name.Text, et_pwd.Text, () =>
                {
                    Toast.MakeText(this, "登录成功", ToastLength.Long).Show();
                }, error =>
                {
                    Toast.MakeText(this, error, ToastLength.Long).Show();
                });
            };
         }
    }
    public class Client
    {
        /// <param name="successAction">登录成功的回调</param>
        /// <param name="errorAction">登录失败的回调</param>
        public static void Client_Login(string name, string pwd,Action successAction,Action<string> errorAction)
        {
            Thread.Sleep(6000);
            if (name == "123" && pwd == "123")
            {
                successAction();
                return;
            }
            errorAction("密码不正确");
        }
    }


ANR异常
这是值得注意的一点,记得上学的时候用的是酷派,虽然是充话费送的,但其实那手机配置还是可以,但是用就久了,很卡,打开一些app,长时间没有反应,我便在屏幕上点、按返回键等,于是就经常报这个“**程序没有响应”“等待”“退出”的一个对话框,好吧,其实用酷派手机卡了这些无关紧要。重点还是要说说ANR异常是如何产生的吧

ANR:Application Not Responding的缩写,当程序爆出“应用程序无响应”,系统会向用户显示一个对话框,“等待”可以让程序继续运行,“强制关闭”直接kill掉了。
在android程序中,程序的响应时由Activity manager和WindowManager系统服务监听的,主要是由以下两种情况造成的,

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

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

    使用Timer对象开启子线程,在子线程使用RunOnUIThread更新UI线程

大家一定使用过Timer,可能有些人还不知道Timer对象会开启多个线程,但最少不止一个。下面这个例子,将演示两个timer,1秒钟更新一次,对比一下两个TextView的显示的时间。
在.net 中timer主要有三种1.System.Threading.Timer 、2.System.Timers.Timer、3.System.Windows.Forms.Timer 前两者会创建一个新的线程(这个会在下面的代码证明),第三种的Timer实在主线程中创建的,也就是在UI线程中的。

    [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<TextView>(Resource.Id.tv_test);
            tv_test1 = FindViewById<TextView>(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,该方法参数是一个无参无返回值的委托。

    异步加载图片,在子线程中更新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<Button>(Resource.Id.btn);
            img = FindViewById<ImageView>(Resource.Id.img);
            tv_result = FindViewById<TextView>(Resource.Id.tv_result);
            btn_test = FindViewById<Button>(Resource.Id.btn_noBlockTest);
            btn_test.Click += (s, e) =>
            {
                noBlock_number++;
                btn_test.Text += "点击了" + noBlock_number+"次";
            };
            System.Diagnostics.Debug.Write("UI线程ID:"+Thread.CurrentThread.ManagedThreadId);
            const string url = "https://gss0.bdstatic.com/-4o3dSag_xI4khGkpoWK1HF6hhy/baike/w%3D268%3Bg%3D0/sign=0003b03088b1cb133e693b15e56f3173/0bd162d9f2d3572c257447038f13632763d0c35f.jpg";
            btn.Click += (s, e) =>
            {
                System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
                sw.Start();
                GetStreamAsync(url,()=>RunOnUiThread(()=> {
                    System.Diagnostics.Debug.Write("异步线程ID:" + Thread.CurrentThread.ManagedThreadId);
                    sw.Stop();
                    double seconds = sw.Elapsed.TotalSeconds;
                    tv_result.Text = "加载图片使用了" + seconds + "秒";
                    img.SetImageBitmap(bitmap);
                }),
                error =>RunOnUiThread(()=> {
                    tv_result.Text = error;
                }));
            };
        }
        /// <param name="successAction">获取图片成功回调方法</param>
        /// <param name="errorAction">获取失败回调方法</param>
        public  void  GetStreamAsync(string  url,Action successAction,Action<string> errorAction)
        {
            try
            {
                RestClient client = new RestClient(url);
                RestRequest request = new RestRequest();
                var result = client.GetAsync(request,(response,handler)=> {
                if (response.StatusCode == 0)
                {
                    errorAction("网络状况差,请稍后再试");
                    return;
                }
                if(response.StatusCode == System.Net.HttpStatusCode.OK)
                {
                        var bytes = response.RawBytes;
                        MemoryStream stream = new MemoryStream(bytes);
                        bitmap = BitmapFactory.DecodeStream(stream);
                        successAction();
                 }
              });
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.Write(ex.StackTrace);
                errorAction(ex.ToString());
            }
        }
    }


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