一、Android多线程编程
1、异步消息处理机制
1.1 弱引用
WeakReference
(弱引用)是一种在Java中用于管理对象的引用的特殊引用类型。它的作用是在垃圾回收过程中,允许对象在没有强引用指向它时被回收(当一个对象只有弱引用指向它,而没有强引用指向它时,垃圾回收器可能会在下一次垃圾回收时回收该对象,即使系统内存并不紧张。),从而避免潜在的内存泄漏问题。
有些情况下,**我们可能希望对象在没有强引用指向它时能够被垃圾回收,以避免潜在的内存泄漏问题。**例如,考虑以下情况:
- 缓存:我们可能使用缓存来存储某些对象,但当这些对象不再被使用时,我们希望它们能够被垃圾回收,释放内存。
- 图片加载:在Android开发中,我们经常加载大量的图片,为了避免内存溢出,我们可能希望在图片不再显示时能够及时释放相关的资源。
WeakReference
是一种特殊的引用类型,它允许对象在没有强引用指向它时被垃圾回收。
创建弱引用的方法:
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);
当我们不再需要弱引用所引用的对象时,可以将弱引用设置为null,使得对象变得不再可达,从而在下一次垃圾回收时被回收。
obj = null;
此时,如果没有其他强引用指向obj
,它就会成为弱引用所引用的对象,因为没有强引用指向它,垃圾回收器可能会在适当的时候回收这个对象。
常用方法:
- weakRef.get()
弱引用对象是通过 WeakReference
类来创建的,该类提供了一个 get()
方法,可以获取弱引用所引用的对象。
如果该对象还没有被垃圾回收器回收,那么 get()
方法会返回该对象的引用;但是如果对象已经被垃圾回收器回收,或者在调用 get()
方法的过程中被回收,那么 get()
方法会返回 null
。
1.2 强引用
当我们在Java中创建对象,并使用变量来引用这些对象时,通常使用的是强引用。强引用使得对象在有至少一个强引用指向它时保持存活状态,即使系统内存紧张,垃圾回收器也不会回收这些对象。
当我们创建一个对象并将其赋值给一个变量时,这个变量就是一个强引用。
只要强引用存在,垃圾回收器就不会回收被引用的对象,即使系统内存紧张也不会回收。只有当没有任何强引用指向一个对象时,垃圾回收器才会在适当的时候回收该对象,释放内存。
Object obj = new Object(); // 这里obj是一个强引用
即使代码中没有使用 obj
,只要 obj
这个变量在作用域内且没有被显式释放(比如设置为 null
或跳出作用域),该对象依然不会被垃圾回收。只有当没有任何强引用指向这个对象时,垃圾回收器才有可能回收它。
1.3 使用Handler构建实例
在Android 10以前我们通常使用这样的方法构建Handler
Handler handler = new Handler(){
public void handlerMessage(Messafe msg){
}
}
但在已经被标记为过时,这个代码编译时会得到一个警告:“ 警告: [deprecation] Handler中的Handler()已过时” 。因为上面的代码存在内存泄露风险,那么怎么泄露内存,原理是什么呢?
当Activity被销毁时,如果其中使用的Handler被持有,并且在外部引用它的对象的生命周期之外保持活动状态,这就可能导致内存泄漏。在这种情况下,Handler所在的外部类可能无法被回收,因为它与Handler存在着相互引用的关系。
为避免内存泄漏,可以采取以下措施:
- 使用静态内部类或独立类来实现Handler,以避免与外部类的强引用关系。
- 尽量避免在Handler中持有外部类(Activity等)的引用,或者在不需要使用Handler时及时取消Message和Runnable的发送。
为了防止内存泄露,官方要求将handler声明为静态类,可内存泄露问题解决了,却引来了另一个很棘手的问题,静态类是无法访问外部类成员的,
怎么解决呢?这个问题很简单将外部类对象通过构造函数传递进来就可以了。
如果消息没处理完呢activity
被销毁了,那么我们要怎么办?
此时我们就需要使用到弱引用的**.get()**方法,感知activity
是否还存在。
public class MainActivity extends AppCompatActivity {
public static final int UPDATE_TEXT = 1;
static ActivityMainBinding binding;
private Handler handler = new MyHandler(Looper.myLooper(),this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
//触发按钮修改文本文字
binding.button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
new Thread(new Runnable() {
@Override
public void run() {
Message message = new Message();
message.what = UPDATE_TEXT;
handler.sendMessage(message);
}
}).start();
}
});
}
static class MyHandler extends Handler{
WeakReference<Activity> mainActivityWeakReference;
//构造函数传入外部的this
public MyHandler(@NonNull Looper looper, Activity mainActivity) {
super(looper);
//构建一个弱引用对象
mainActivityWeakReference = new WeakReference<Activity>(mainActivity);
}
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
//判断活动是否还存在,如果不存在则结束
Activity activity = mainActivityWeakReference.get();
if (activity == null){
return;
}
switch (msg.what){
case UPDATE_TEXT:
binding.Text.setText("Nice to meet too");
}
}
}
}
2、解析异步消息处理机制
Android中的异步消息处理机制主要由四个部分组成:Message、Handler、MessageQueue、Looper
2.1 Message
在线程之间传递消息,用于不同线程只见交换数据,例如上段代码的what
用法。
2.2 Handler
Handler也就是处理者,主要用于发送消息和处理消息。发送消息一般使用Handler
的setMessage()
方法,发送的消息最终会传递到Handler的HanleMessage()
方法中,我们通常需要重写HanlerMessage
方法。
2.3 MessageQueue
消息队列,通常存放通过Handler发送的所以消息,每个线程中只有一个MessageQueue对象。
2.4 Looper
Looper是每个线程中MessageQueue的管家,调用Looper的loop()方法就可以进入一个无限的循环,每当在MessageQueue中发现一条消息,就会将消息取出发送给Handler的HandleMessage()方法中,同样每个线程中也只有一个Looper对象。
Looper.myLooper()
是一个静态方法,用于获取当前线程的Looper对象。
2.5 异步消息处理流程
首先需要在主线程当中创建一个静态继承Handler的内部类,并重写handleMessage()
方法。然后创建一个该内部类的对象MyHandler。
然后当子线程中需要进行UI操作时,就创建一个Message对象,并通过MyHandler将这条消息发送出去。
之后这条消息会被添加到MessageQueue的队列中等待被处理,而Looper则会一直尝试从MessageQueue中取出待处理消息,最后分发回内部类的handleMessage()
方法中。
由于Handler是在主线程中创建的,所以此时handleMessage()
方法中的代码也会在主线程中运行,于是我们在这里就可以安心地进行UI操作了。
3、使用AsyncTask(谨用)
借助AsyncTask即使不了解异步消息处理机制也可以十分简单的从子线程切换到主线程。
AsyncTask是一个抽象方法我们需要用一个子类继承它,并且使用时需要指定三个泛型参数:
- **Params:**在执行AsyncTask时需要传入的参数
- **Progress:**后台任务执行时,如果需要在界面显示当前进度,则使用这里的泛型类型作为进度单位。
- **Result:**当任务结束后的返回值。
例如:
class DownloadTask extends AsyncTask<Void,Integer,Boolean>{
}
- 第一个泛型参数指定为Void,表示在执行AsyncTask的时候不需要传人参数给后台任务。
- 第二个泛型参数指定为Integer,表示使用整型数据来作为进度显示单
位。 - 第三个泛型参数指定为Boolean,则表示使用布尔型数据来反馈执行结果。
接下来需要重写几个方法:
3.1 onPreExecute
这个方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。
3.2 doInBackground()
**这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。**任务一旦完成就可以通过return语句来将任务的执行结果返回,如果AsyncTask的第三个泛型参数指定的是void,就可以不返回任务执行结果。注意,在这个方法中是不可以进行UI操作的。
比如说反馈当前任务的执行进度,可以调用publishProgress(Progress…)方法来完成。
3.3 OnProgressUpdate(Progress…)
当在后台任务中调用了publishProgress
方法后,onProgressUpdate
方法就会很快被调用。
该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新。
3.4 onPostExecute(Result)
当后台任务结束时并且通过retrun语句返回,这个方法就会被调用。返回的数据会作为参数传递到这个方法,可以根据数据进行UI操作。
3.5 实现自定义AsyncTask
首先使用AlertDialog和ProgressBar实现一个悬浮的加载框。
class MyAskncTask extends AsyncTask<Void,Integer,Boolean> {
private AlertDialog alertDialog;
int downloadPercent = 0;
@Override
protected void onPreExecute() {
super.onPreExecute();
MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setView(R.layout.progressbar);
alertDialog = builder.create();
alertDialog.setMessage("Downloaded: "+0+"%");
alertDialog.show();
}
});
}
@Override
protected Boolean doInBackground(Void... voids) {
try {
while (true){
Thread.currentThread().join(100);
downloadPercent+= 1;
//传递下载进度
publishProgress(downloadPercent);
if(downloadPercent>100){
break;
}
}
}catch (Exception e){
return false;
}
return true;
}
@Override
protected void onPostExecute(Boolean aBoolean) {
alertDialog.dismiss();
}
@Override
protected void onProgressUpdate(Integer... values) {
alertDialog.setMessage("Downloaded"+values[0]+"%");
}
}
实现后台运行加载,当再次点击按钮时,进入后台加载的页面:
binding.button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
runOnUiThread(new Runnable() {
final MyAskncTask myAskncTask = new MyAskncTask(); // 在这里赋值
@Override
public void run() {
//判断是否正在运行
if ( myAskncTask.getStatus() == AsyncTask.Status.RUNNING) {
if (alertDialog != null) {
//如果正在运行重新显示弹窗
alertDialog.show();
}
} else {
myAskncTask.execute();
}
}
});
}
});
PENDING
:任务已创建,但尚未执行。RUNNING
:任务正在运行,它的doInBackground()
方法正在执行。FINISHED
:任务已完成执行。一旦任务完成运行,它就不能再次执行。