android/java中主线程和子线程的详解
一、UI线程(主线程)介绍
1、UI线程是什么?
UI线程,也称为主线程(Main Thread),是Android应用启动时由系统自动创建的第一个线程,是一个特殊的、预先创建好的单线程消息循环。你的应用入口点(如 MainActivity
的 onCreate
方法)就是运行在这个线程上的,系统为每个应用分配了且仅有一个UI线程,是唯一有权限直接修改UI的线程。
它的核心身份是:
- 一个普通的线程:它是
java.lang.Thread
的一个实例。 - 一个特殊的消息循环线程:它内部运行着一个 Looper 和 MessageQueue,不断地处理消息(Message)或 Runnable 对象。
2、UI线程的核心职责
UI线程被设计为承担所有与用户交互相关的敏感操作,主要原因是为了保证线程安全和操作的有序性。
-
UI绘制与更新:
- 测量、布局、绘制视图树中的所有View(
onMeasure()
,onLayout()
,onDraw()
)。 - 更新任何UI组件的属性,例如
TextView.setText()
,ImageView.setImageBitmap()
。
- 测量、布局、绘制视图树中的所有View(
-
处理用户输入事件:
- 分发和处理屏幕触摸事件(
onTouchEvent
)、按键事件(onKeyDown
)。
- 分发和处理屏幕触摸事件(
-
执行生命周期回调:
- Activity、Fragment、Service等组件的生命周期方法(
onCreate
,onResume
,onPause
等)都在UI线程中被调用。
- Activity、Fragment、Service等组件的生命周期方法(
-
执行通过
Handler
或View.post()
提交的Runnable
任务。
3、UI线程的工作原理:消息循环机制(Message Loop)
这是理解UI线程如何工作的关键。其核心组件包括:
组件 | 作用 |
---|---|
MessageQueue(消息队列) | 一个单链表结构的优先级队列,用于存放由 Handler 发送过来的 Message 或 Runnable。它是一个阻塞队列,当没有消息时,线程会进入休眠状态以节省CPU。 |
Looper(循环器) | UI线程的“引擎”。它在一个无限循环中工作,不断地从 MessageQueue 中取出(MessageQueue.next() )消息。如果队列为空,它就阻塞;如果有消息,它就将其分发给对应的目标。一个线程只能有一个Looper。 |
Handler(处理器) | 消息的“发送者”和“处理者”。它被绑定到创建它的线程(及其Looper)上。开发者通过 Handler 将 Message 或 Runnable 发送(post/send) 到消息队列中,也通过它在其绑定的线程上处理(handleMessage) 取出的消息。 |
4. 正确的使用方法:线程池 + UI线程协作
标准的做法是让线程池和UI线程各司其职,协同工作:
- 在线程池中执行耗时任务:使用线程池(如
AsyncTask
的旧方式,或现在推荐的ExecutorService
、Coroutine
+Dispatchers.IO
)在后台执行耗时操作。 - 将结果发送回UI线程:当后台任务完成并得到结果后,使用专门的方法将结果从工作线程发送(Post) 到UI线程,然后在UI线程上更新界面。
5.如何从后台线程更新UI?
有多种方法可以将代码切回UI线程执行:
1. Activity.runOnUiThread(Runnable action)
// 在后台线程中(例如线程池的线程)
executorService.execute(() -> {// 执行耗时操作,比如网络请求String result = doNetworkRequest();// 操作完成后,切回UI线程更新界面runOnUiThread(() -> {textView.setText(result); // 这是在UI线程中安全执行的});
});
2. View.post(Runnable action)
// 任何View都可以
executorService.execute(() -> {String result = doNetworkRequest();myTextView.post(() -> {myTextView.setText(result);});
});
3. Handler(Looper.getMainLooper())
Handler mainHandler = new Handler(Looper.getMainLooper());
executorService.execute(() -> {String result = doNetworkRequest();mainHandler.post(() -> {textView.setText(result);});
});
二、什么时候需要开启子线程?
核心原则:所有可能阻塞主线程(UI 线程)的耗时操作都必须在子线程中执行。
具体包括以下情况:
1. 网络请求
// 必须在子线程中执行
new Thread(new Runnable() {@Overridepublic void run() {try {URL url = new URL("https://api.example.com/data");HttpURLConnection connection = (HttpURLConnection) url.openConnection();// 读取数据...} catch (Exception e) {e.printStackTrace();}}
}).start();
2. 文件读写操作
// 文件操作需要在子线程
new Thread(() -> {try {File file = new File(getFilesDir(), "data.txt");FileOutputStream fos = new FileOutputStream(file);fos.write("Hello World".getBytes());fos.close();} catch (IOException e) {e.printStackTrace();}
}).start();
3. 数据库操作(特别是大量数据)
// 大量数据库查询
new Thread(() -> {SQLiteDatabase db = dbHelper.getReadableDatabase();Cursor cursor = db.query("table_name", null, null, null, null, null, null);// 处理数据...cursor.close();
}).start();
4. 复杂计算或数据处理
// 复杂计算
new Thread(() -> {double result = 0;for (int i = 0; i < 1000000; i++) {result += Math.sin(i) * Math.cos(i);}// 计算完成后需要更新UI的话要回到主线程
}).start();
5. 图片处理/压缩
// 图片压缩处理
new Thread(() -> {Bitmap compressedBitmap = Bitmap.createScaledBitmap(originalBitmap, targetWidth, targetHeight, true);// 处理完成后需要回到主线程显示图片
}).start();
三、什么时候需要回到主线程执行?
核心原则:所有UI更新操作都必须在主线程中执行。
具体包括以下情况:
1. 更新UI组件
// 在子线程中获取数据后,回到主线程更新UI
new Thread(() -> {// 模拟网络请求try {Thread.sleep(2000);final String result = "获取的数据";// 回到主线程更新UIrunOnUiThread(new Runnable() {@Overridepublic void run() {textView.setText(result); // UI更新必须在主线程progressBar.setVisibility(View.GONE);}});} catch (InterruptedException e) {e.printStackTrace();}
}).start();
2. 显示Toast
new Thread(() -> {// 耗时操作...// 显示Toast需要主线程runOnUiThread(() -> {Toast.makeText(MainActivity.this, "操作完成", Toast.LENGTH_SHORT).show();});
}).start();
3. 操作Adapter和RecyclerView/ListView
new Thread(() -> {List<Data> newData = fetchDataFromServer();runOnUiThread(() -> {adapter.setData(newData);adapter.notifyDataSetChanged(); // 必须在主线程调用});
}).start();
4. 启动Activity或Fragment事务
// 任何界面跳转都应在主线程
runOnUiThread(() -> {Intent intent = new Intent(MainActivity.this, DetailActivity.class);startActivity(intent);
});
四、需要/不需要确认线程的情况
1. 不需要确认线程的情况
不需要确认每一行代码,但需要确认每一段有特定线程要求的代码。**
1、不需要确认每一行代码的情况
大部分普通的、无副作用的计算代码不需要关心线程。比如:
int a = 10;
int b = 20;
int result = a + b; // 这个加法在任何线程执行结果都一样
String name = "Hello"; // 字符串赋值
final User user = new User("John"); // 创建对象(如果构造函数很简单)
这些代码是"线程安全"的,因为它们:
- 只操作局部变量
- 不访问共享资源
- 不修改外部状态
- 没有特定的线程要求
2. 必须确认线程情况的代码(关键!)
- UI操作 - 必须主线程
textView.setText("Hello"); // ✅ 必须在主线程
button.setEnabled(false); // ✅ 必须在主线程
recyclerView.notifyDataSetChanged(); // ✅ 必须在主线程
- Android系统组件生命周期方法 - 通常在主线程
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState); // 在主线程setContentView(R.layout.activity_main); // 在主线程
}@Override
public void onResume() {super.onResume(); // 在主线程
}
- 耗时操作 - 必须工作线程
// 网络请求
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); // ❌ 不要在主线程
InputStream response = connection.getInputStream(); // ❌ 不要在主线程// 文件读写
FileOutputStream fos = new FileOutputStream(file); // ❌ 不要在主线程
fos.write(data); // ❌ 不要在主线程// 大量数据库操作
SQLiteDatabase db = dbHelper.getWritableDatabase(); // ❌ 简单查询可以,大量操作不要在主线程
db.insert(...); // ❌
- 访问共享资源/状态 - 需要线程安全考虑
// 静态变量
public static List<String> cachedData = new ArrayList<>();// 单例对象中的状态
public class DataManager {private static DataManager instance;private int requestCount = 0; // 需要同步访问public void incrementCount() {requestCount++; // ❌ 非原子操作,需要同步}
}
3. 实用的思维模式
不要想着:“我需要检查每行代码的线程”
而应该想:“我知道这几类代码有线程要求,当我写这些代码时要特别注意”
4. 如何培养这种意识(开发习惯)
-
建立清单:记住哪些操作必须在主线程(所有UI操作),哪些必须在工作线程(所有I/O操作)
-
使用代码结构提示:
// 看到这些方法,就要意识到可能在工作线程 public void fetchData() {// 这里应该切换线程或确保已在工作线程 }private void onDataReceived(String data) {// 收到数据后要更新UI?那需要切回主线程 }
-
善用工具和注解:
@WorkerThread // 标记该方法应在工作线程调用 public void loadFromDatabase() { ... }@MainThread // 标记该方法应在主线程调用 public void updateUI() { ... }@AnyThread // 标记该方法线程安全,可在任何线程调用 public synchronized int getCount() { ... }
-
运行时检查(调试时非常有用):
// 在怀疑的地方添加检查 if (Looper.myLooper() != Looper.getMainLooper()) {Log.w("ThreadCheck", "这段UI代码在后台线程执行!");// 或者直接抛出异常throw new IllegalStateException("必须在主线程调用"); }
情况 | 是否需要确认线程 | 原因 |
---|---|---|
简单的局部变量操作 | ❌ 不需要 | 线程安全 |
UI更新 | ✅ 必须确认 | 必须在主线程 |
网络/文件/数据库操作 | ✅ 必须确认 | 必须在工作线程 |
访问共享状态 | ✅ 必须确认 | 需要线程同步 |
系统生命周期方法 | ✅ 应该知道 | 通常在主线程 |
最终建议:不需要过度焦虑地检查每一行代码,但要培养对关键代码段的线程敏感性。
五、推荐的线程管理方式
使用Handler
Handler handler = new Handler(Looper.getMainLooper());new Thread(() -> {// 子线程执行耗时操作String result = doWork();// 通过Handler回到主线程handler.post(() -> {updateUI(result);});
}).start();
使用现代并发工具(推荐)
// 使用ExecutorService管理线程池
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.execute(() -> {// 子线程工作String result = fetchData();runOnUiThread(() -> {// 回到主线程更新UItextView.setText(result);});
});
六、总结
场景 | 执行线程 | 原因 |
---|---|---|
网络请求 | 子线程 | 避免阻塞UI线程导致ANR |
文件操作 | 子线程 | 磁盘IO可能很慢 |
数据库操作 | 子线程 | 特别是大量数据查询 |
复杂计算 | 子线程 | 占用CPU资源,会卡顿UI |
图片处理 | 子线程 | 解码和处理可能很耗时 |
UI更新 | 主线程 | Android的UI系统不是线程安全的 |
Toast显示 | 主线程 | 属于UI操作 |
Adapter更新 | 主线程 | 避免并发修改异常 |
记住这个简单规则:
-
- 做工作:在子线程(后台线程)
-
- 展示结果:在主线程(UI线程)
-
- 永远不要阻塞UI线程,也不要从非UI线程操作UI。正确的做法是使用线程池处理耗时任务,任何可能超过几毫秒的操作都必须移到后台线程。然后通过
runOnUiThread()
,view.post()
或协程等方式将结果传回UI线程进行更新。
- 永远不要阻塞UI线程,也不要从非UI线程操作UI。正确的做法是使用线程池处理耗时任务,任何可能超过几毫秒的操作都必须移到后台线程。然后通过