为什么子线程中不能直接更新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<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("密碼不正確");
}
}
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<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,該方法參數是一個無參無返回值的委托。
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<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線程,執行耗時請求圖片方法時,任然可以點擊按鈕,輸入其他的事件。
推薦閱讀
工具程序員必裝的10款谷歌插件
技巧14個實用的 數據庫設計技巧
原創程序員:我終于知道post和get的區別
面試面試官:你連RESTful都不知道我怎么敢要你?
熱議年底300人被裁,感受互聯網民工討薪的心路歷程
轉載程序員:改完這9段屎一樣的代碼,還挺香!
技巧99%的人不知道搜索引擎的6個技巧
面試面試官:你們前后端分離的接口規范是什么?
總結
以上是生活随笔為你收集整理的为什么子线程中不能直接更新UI的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在Asp.Net Core MVC 开发
- 下一篇: 解决问题的能力 10倍程序员