一、技术概述
1.1 技术介绍
Android系统中,默认所有操作都运行在主线程(UI线程)中,主线程负责处理UI更新、用户交互等操作。如果在主线程中执行耗时操作(如网络请求、文件读写、复杂计算),会导致界面卡顿,甚至ANR(应用无响应)。多线程技术就是将耗时操作放到子线程中执行,避免阻塞主线程,提高应用的流畅性和响应速度。
1.2 适用场景
网络请求、文件上传下载
数据库操作、大量数据计算
音视频解码、图片处理
定时任务、倒计时功能
所有耗时超过50ms的操作都应该放到子线程执行
1.3 优缺点
| 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Thread+Handler | 灵活可控,功能强大 | 需要手动处理线程切换,代码相对繁琐 | 复杂的多线程场景 |
| AsyncTask | 简单易用,自动处理线程切换 | 版本兼容性问题,不适合长时间耗时操作 | 短时间的后台任务 |
| HandlerThread | 自带消息循环的线程,可以处理串行任务 | 不适合并行任务 | 串行执行的后台任务 |
| IntentService | 适合执行后台串行任务,执行完自动销毁 | 无法并行处理任务 | 后台串行任务,如下载、上传 |
| 线程池 | 复用线程,减少线程创建销毁开销,控制并发数 | 配置相对复杂 | 大量频繁的耗时操作 |
二、基本效果
使用多线程后,可以实现:
执行网络请求时,界面仍然可以响应用户操作,不会卡顿
游戏倒计时可以在后台运行,不会因为用户操作界面而暂停
上传头像、下载文件等操作可以在后台执行,不影响用户使用其他功能
大量数据计算时,界面保持流畅
三、技术本质
Android多线程的本质是CPU时间片的轮转调度,多个线程交替执行,宏观上看起来是同时运行的。Android多线程核心要解决两个问题:
耗时操作放到子线程执行:避免阻塞主线程
子线程执行完成后切换回主线程更新UI:因为Android规定只有主线程才能更新UI 运行流程:
主线程收到耗时操作请求
创建子线程,将耗时操作放到子线程执行
子线程执行耗时操作,执行过程中可以通知主线程更新进度
子线程执行完成后,通过Handler、runOnUiThread等方式切换回主线程
主线程根据执行结果更新UI
四、核心原理
4.1 线程状态
| 状态 | 说明 |
|---|---|
| 新建状态(New) | 创建了Thread对象,但还没有调用start()方法 |
| 就绪状态(Runnable) | 调用了start()方法,等待CPU调度执行 |
| 运行状态(Running) | 获得CPU时间片,正在执行 |
| 阻塞状态(Blocked) | 因为某些原因暂停执行,如sleep()、wait()、IO阻塞 |
| 死亡状态(Terminated) | 线程执行完成或异常退出 |
4.2 线程切换原理
子线程不能直接更新UI,必须切换回主线程,常用的切换方式:
Handler.sendMessage()/post():将消息发送到主线程的消息队列,由主线程处理
Activity.runOnUiThread():直接在子线程中执行主线程的代码
View.post()/postDelayed():将Runnable投递到主线程执行
AsyncTask的onProgressUpdate()/onPostExecute():自动切换到主线程
五、使用示例
5.1 Thread+Handler方式
// 主线程中创建Handler private Handler mHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(@NonNull Message msg) { super.handleMessage(msg); switch (msg.what) { case 1001: // 更新UI int progress = (int) msg.obj; progressBar.setProgress(progress); break; case 1002: // 任务完成 Toast.makeText(MainActivity.this, "下载完成", Toast.LENGTH_SHORT).show(); break; } } }; // 启动子线程执行耗时操作 new Thread(new Runnable() { @Override public void run() { for (int i = 0; i <= 100; i++) { try { // 模拟下载耗时 Thread.sleep(50); // 发送进度消息到主线程 Message msg = Message.obtain(); msg.what = 1001; msg.obj = i; mHandler.sendMessage(msg); } catch (InterruptedException e) { e.printStackTrace(); } } // 发送完成消息 mHandler.sendEmptyMessage(1002); } }).start();5.2 runOnUiThread简化方式
new Thread(new Runnable() { @Override public void run() { // 子线程执行耗时操作 final String result = doLongTimeOperation(); // 切换到主线程更新UI runOnUiThread(new Runnable() { @Override public void run() { tvResult.setText(result); } }); } }).start();5.3 AsyncTask方式
// 三个泛型参数:1.传入参数类型 2.进度更新类型 3.返回结果类型 private class DownloadTask extends AsyncTask<String, Integer, Boolean> { // 任务开始前执行,运行在主线程 @Override protected void onPreExecute() { super.onPreExecute(); progressBar.setVisibility(View.VISIBLE); } // 后台执行耗时操作,运行在子线程 @Override protected Boolean doInBackground(String... urls) { String url = urls[0]; for (int i = 0; i <= 100; i++) { try { Thread.sleep(50); // 发布进度,触发onProgressUpdate publishProgress(i); } catch (InterruptedException e) { e.printStackTrace(); return false; } } return true; } // 进度更新,运行在主线程 @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); progressBar.setProgress(values[0]); } // 任务完成,运行在主线程 @Override protected void onPostExecute(Boolean result) { super.onPostExecute(result); progressBar.setVisibility(View.GONE); if (result) { Toast.makeText(MainActivity.this, "下载完成", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(MainActivity.this, "下载失败", Toast.LENGTH_SHORT).show(); } } } // 使用方式 new DownloadTask().execute("https://example.com/file.apk");5.4 线程池方式
// 创建固定大小线程池,最多同时3个线程执行 ExecutorService threadPool = Executors.newFixedThreadPool(3); // 执行任务 threadPool.execute(new Runnable() { @Override public void run() { // 执行耗时操作1 } }); threadPool.execute(new Runnable() { @Override public void run() { // 执行耗时操作2 } }); // 关闭线程池 threadPool.shutdown();5.5 在专注力测试APP中的应用示例
游戏倒计时功能使用Thread+Handler实现:
// 游戏倒计时 private Handler mGameHandler = new Handler(Looper.getMainLooper()); private Runnable mCountDownRunnable; private int mCountDownTime = 30; // 30秒 private void startCountDown() { mCountDownRunnable = new Runnable() { @Override public void run() { mCountDownTime--; tvTime.setText("剩余时间:" + mCountDownTime + "s"); if (mCountDownTime > 0) { // 每秒执行一次 mGameHandler.postDelayed(this, 1000); } else { // 时间到,结束游戏 endGame(); } } }; mGameHandler.post(mCountDownRunnable); } @Override protected void onDestroy() { super.onDestroy(); // 移除回调,避免内存泄漏 mGameHandler.removeCallbacks(mCountDownRunnable); }六、常见问题与解决方案
6.1 ANR问题
问题:应用出现无响应弹窗原因:主线程被耗时操作阻塞超过5秒,或者BroadcastReceiver 10秒内没有处理完成解决方案:
所有耗时操作(网络、文件、数据库、复杂计算)都放到子线程执行
避免在主线程中执行复杂的布局计算、大量循环操作
6.2 内存泄漏问题
问题:非静态内部类线程持有Activity引用,Activity销毁后线程还在运行,导致Activity无法回收解决方案:
使用静态内部类+弱引用的方式
private static class MyRunnable implements Runnable { private WeakReference<MainActivity> mActivity; public MyRunnable(MainActivity activity) { mActivity = new WeakReference<>(activity); } @Override public void run() { MainActivity activity = mActivity.get(); if (activity != null) { // 执行操作 } } }Activity销毁时,停止正在执行的线程,移除Handler的回调和消息
6.3 线程安全问题
问题:多个线程同时访问同一个数据,导致数据混乱解决方案:
使用synchronized关键字同步访问共享数据
使用线程安全的集合类,如ConcurrentHashMap
尽量避免多个线程同时修改同一个变量
6.4 子线程更新UI崩溃
问题:在子线程中直接更新UI,抛出CalledFromWrongThreadException异常解决方案:
使用Handler、runOnUiThread、View.post等方式切换到主线程再更新UI
严格遵守只有主线程才能更新UI的规则
七、学习总结
7.1 学习收获
掌握了Android中几种常用的多线程实现方式,了解了每种方式的优缺点和适用场景,理解了线程切换的原理,能够在开发中选择合适的多线程实现方式,避免主线程阻塞和ANR问题。
7.2 项目应用场景
在本次专注力测试APP中,多线程的应用场景:
游戏倒计时、计时功能的实现
网络请求(登录、上传成绩、获取排行榜等)放到子线程执行
头像上传、图片加载处理
本地数据库操作、文件读写
游戏成绩计算、复杂逻辑处理