哎,各位Android开发打工人,不知道你们有没有经历过这种让人抓狂的场景——
你兴冲冲打开自己刚写好的APP,点击一个按钮,想要加载一些网络数据。结果呢?屏幕突然卡住,整个界面凝固得像冻住的湖面,过了几秒甚至弹出个“应用无响应”的ANR对话框!
别问,问就是主线程在摸鱼!
没错,今天咱们要聊的就是Android开发中那个让人又爱又恨的话题——多线程。放心,我不会搬出一堆晦涩难懂的术语来折磨你,咱们就用最接地气的方式,把这个看似高深的概念掰开揉碎,让你看完就能用,用了就见效!
首先,你得明白一个残酷的事实:AndroidAPP的UI(用户界面)操作,比如按钮点击、文字显示、图片加载,默认都是在主线程(也叫UI线程)这一个独苗线程上进行的。
你可以把主线程想象成一个996的打工仔,他一个人扛下了所有:
处理你的触摸滑动更新界面上的每个像素点还要忙着做各种计算和网络请求当他遇到一个耗时的任务,比如从网上下载一张高清大图,或者从数据库里查询一万条记录时,会发生什么?
他就会埋头苦干这个耗时任务,完全没空搭理你其他的操作!这时候,UI更新就停了,动画卡住了,按钮按了没反应。用户眼里,就是APP“卡死了”。如果这个阻塞超过5秒,系统就会忍无可忍,弹出ANR(Application Not Responding)错误,直接劝退用户。
所以,结论很简单:所有可能耗时的操作,比如网络请求、大量文件读写、复杂计算,统统不能扔在主线程!
那我们该怎么办?答案就是:开小号! 哦不,是开启新的工作线程,让这些“慢活儿”在后台默默执行。
在Java的世界里(Android也是用Java或Kotlin嘛),开启一个新线程最直接的方式,就是请出我们的老伙计:Thread类 和 Runnable接口。
它俩的关系,好比泡面和火腿肠,天生一对。
Runnable是那包面饼和调料(定义了要执行的任务),而
Thread是那个烧开水的锅(提供了线程运行的环境)。
1. 使用Runnable接口(推荐方式)
为什么推荐它?因为更灵活。一个任务(Runnable)可以交给多个不同的线程(Thread)去执行,符合“组合优于继承”的设计原则。
来看代码,最直观:
// 1. 首先,我们创建一个“任务”(Runnable)
Runnable networkTask = new Runnable() {
@Override
public void run() {
// 这里就是新线程的工作内容了!
// 模拟一个耗时的网络操作,比如下载商品列表
try {
Thread.sleep(3000); // 假装我们网络请求了3秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
// 假设我们获取到了数据
String fakeData = "['华为手机', '小米电视', '苹果耳机']";
// !!!!!! 警告!前方是坑!!!!!!!
// 你现在还在子线程里,绝对不能直接更新UI!
// textView.setText(fakeData); // 这么写会崩溃!
}
};
// 2. 然后,创建一个“工人”(Thread),并把任务分配给他
Thread workerThread = new Thread(networkTask);
// 3. 最后,让工人开始干活!(启动线程)
workerThread.start();
看明白了吗?我们把所有可能慢吞吞的代码都写在
run()方法里。当你调用
thread.start()时,系统就会创建一个新的线程,并在这个新线程里乖乖执行
run()中的代码,再也不会阻塞主线程的流畅操作了。
2. 直接继承Thread类(了解一下就行)
这种方式更直接,但灵活性差一些。
// 自定义一个线程类
class MyWorkerThread extends Thread {
@Override
public void run() {
// 这里是子线程的工作内容
Log.d("MyWorkerThread", "我在子线程里干活呢!");
}
}
// 使用它
MyWorkerThread myThread = new MyWorkerThread();
myThread.start();
虽然写法简单,但Java是单继承的,你继承了
Thread就不能再继承其他类了,所以在实际开发中,我们更倾向于使用上面第一种
Runnable的方式。
光说不练假把式。现在我们来模拟一个电商APP中最常见的场景:点击按钮,从网络加载商品列表,并显示在屏幕上。
完整代码如下:
public class MainActivity extends AppCompatActivity {
private TextView mResultTextView;
private ProgressBar mLoadingProgressBar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mResultTextView = findViewById(R.id.tv_result);
mLoadingProgressBar = findViewById(R.id.pb_loading);
Button loadButton = findViewById(R.id.btn_load);
loadButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 点击按钮后,显示加载进度条
mLoadingProgressBar.setVisibility(View.VISIBLE);
mResultTextView.setText("正在拼命加载中...");
// 创建一个后台任务
Runnable loadProductsTask = new Runnable() {
@Override
public void run() {
// 【子线程】:执行耗时操作
String data = simulateNetworkRequest();
// 【子线程】:数据拿到了,现在要更新UI,必须回主线程
runOnUiThread(new Runnable() {
@Override
public void run() {
// 【主线程】:这里是安全的,可以随意更新UI
mLoadingProgressBar.setVisibility(View.GONE); // 隐藏进度条
mResultTextView.setText("加载完成!商品列表:" + data);
}
});
}
};
// 创建线程并启动,开始异步加载
new Thread(loadProductsTask).start();
}
});
}
/**
* 模拟一个耗时的网络请求
* @return 伪造的商品数据
*/
private String simulateNetworkRequest() {
try {
// 睡眠3秒,模拟网络延迟
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 返回伪造的JSON数据
return "['华为Mate60', '小米SU7', '苹果Vision Pro']";
}
}
代码逐行解析:
界面初始化:在
onCreate里,我们找到了布局中的文本框(
TextView)、进度条(
ProgressBar)和按钮(
Button)。按钮点击:当用户点击“加载”按钮,我们立即显示进度条,并提示“正在加载”,给用户一个即时反馈。创建后台任务:我们定义了一个
Runnable任务
loadProductsTask。它的核心工作放在
run()方法里。开启新线程:
new Thread(loadProductsTask).start() 这行代码是精髓!它瞬间开辟了一条新的工作线程,让耗时的
simulateNetworkRequest()方法在后台运行。模拟网络请求:
simulateNetworkRequest方法中用
Thread.sleep(3000)假装我们请求了3秒钟网络。真实开发中,这里会是OkHttp或Retrofit的网络调用。关键一步:切回主线程更新UI!数据在子线程获取后,你不能直接在子线程里调用
mResultTextView.setText(...)。所以我们需要一个“切换器”——
runOnUiThread()。这是一个Android提供的方便方法,它里面的代码会被安全地送回到主线程执行。这样,更新UI的操作就万无一失了。
这个流程,就是Android多线程最经典、最核心的范式:主线程响应用户 → 子线程处理耗时任务 → 子线程完成任务后切回主线程更新UI。
runOnUiThread更新UI时,要确保Activity还没被销毁。否则可能会找不到View而崩溃。更健壮的做法是检查
isFinishing()。内存泄漏坑:如果你在Runnable里持有了Activity的引用,并且这个线程执行时间很长(比如while循环),即使Activity关闭了,线程依然持有它的引用,导致垃圾回收器无法回收这个Activity,内存就泄漏了。解决方案是在Activity的
onDestroy中中断线程,或使用弱引用。性能坑:不要无脑地
new Thread().start()!如果频繁创建短生命周期的线程,创建和销毁线程的开销会很大。在复杂场景下,我们通常会使用
ExecutorService线程池来管理和复用线程,效率高得多。
当然有!
Thread和
Runnable是基础,但在现代Android开发中,我们有了更多强大的“武器”:
这些高级工具的背后,思想都是共通的:主线程保流畅,耗时任务放后台。
好了,今天的Android多线程“防卡顿”摸鱼指南就到这里。记住核心心法:
主线程是爷,得供着,只让它干轻快的UI活。所有可能慢的、累的、要等待的活儿,统统打包成Runnable,扔给Thread小号去干!干完了记得通过runOnUiThread回来汇报工作。
从现在开始,彻底告别APP卡顿,让你的应用丝滑如德芙,用户体验飙升!如果你觉得有用,记得分享给你身边那个还在被ANR折磨的哥们儿!