一、技术概述
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中,多线程的应用场景:
-
游戏倒计时、计时功能的实现
-
网络请求(登录、上传成绩、获取排行榜等)放到子线程执行
-
头像上传、图片加载处理
-
本地数据库操作、文件读写
-
游戏成绩计算、复杂逻辑处理