文章目录
- 一、课程背景
- 二、应用场景
- 三、概念介绍
- 四、代码实现最简单Handler
- 五、Handler的发送消息方法
- 1、Handler.sendMessage()
- Handler.sendMessageAtTime(Message ,uptimeMillis)
- Handler.sendMessageDelayed(Message,delayMillis)
- 2、Handler.post()
- Handler.postAtTime(Runnable,long)
- Handler.postDelayed(Runnable,long)
- 六、Handler实践的三种效果
- 1、异步下载文件更新进度条
- 具体代码
- Handler部分黄色的高亮显示,有内存泄漏的风险
- 2、倒计时的实现
- 内存泄漏解决
- 具体代码
- 3、用Handler来实现简单打地鼠游戏
- 具体代码
一、课程背景
1、UI线程/主线程/ActivityThead
2、线程不安全(针对于多线程)
Android的UI线程——线程不安全,所以在子线程更新UI会出现问题,用Handler解决
线程安全: 多线程访问时,采用了加锁机制,当一个线程访问UI时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。
3、消息循环机制
二、应用场景
三、概念介绍
1、Handler:发送消息和处理消息
2、Looper:负责循环读取MessageQueen中的消息,读到消息之后就把消息交给Handler去处理
3、Message:消息对象
4、MessageQueue:存储消息对象的队列
四、代码实现最简单Handler
MainActicity
package com.example.handlerstudy;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
/*
主线程范围
*/
private static final String TAG="MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView textView=(TextView)findViewById(R.id.textView);
@SuppressLint("Handlerleak")
//创建Handler
final Handler handler=new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
//处理消息
Log.d(TAG, "handleMessage: "+msg.what);
}
};
handler.sendEmptyMessage(1001);
}
}
logcat:02-13 19:57:52.713 7432-7432/com.example.handlerstudy D/MainActivity: handleMessage: 1001
Q:代码为什么会有一块是黄色的高亮显示
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_marginStart="78dp"
android:layout_marginLeft="78dp"
android:layout_marginTop="154dp"
android:text="Button"
/>
</RelativeLayout>
MainActivity:新增Button点击事件
package com.example.handlerstudy;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
/*
主线程范围
*/
private static final String TAG="MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView textView=(TextView)findViewById(R.id.textView);
@SuppressLint("Handlerleak")
//创建Handler
final Handler handler=new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
//处理消息
Log.d(TAG, "handleMessage: "+msg.what);
}
};
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
new Thread(new Runnable() {
@Override
public void run() {
textView.setText("imooc");
}
}).start();
}
});
handler.sendEmptyMessage(1001);
}
}
程序出现闪退: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
只能在主线程更新UI
MainActivity修改:
如果是在子线程中,把消息发出去,然后在主线程中拦截该消息,并进行处理
package com.example.handlerstudy;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private static final String TAG="MainActivity";
/**
*UI线程:
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView textView=(TextView)findViewById(R.id.textView);
@SuppressLint("Handlerleak")
//创建Handler
final Handler handler=new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
/**
* 主线程:接到子线程发出的消息,处理
*/
//处理消息
Log.d(TAG, "handleMessage: "+msg.what);
if(msg.what==1001){
textView.setText("imooc");
}
}
};
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
/**
* 子线程:
*/
//有可能做大量耗时操作
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(4*1000);//休眠4秒
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 通知UI更新
*/
handler.sendEmptyMessage(1001);
}
}).start();
}
});
}
}
效果图
五、Handler的发送消息方法
1、Handler.sendMessage()
MainActivity
public class MainActivity extends AppCompatActivity {
private static final String TAG="MainActivity";
/**
*UI线程:
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView textView=(TextView)findViewById(R.id.textView);
@SuppressLint("Handlerleak")
//创建Handler
final Handler handler=new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
/**
* 主线程:接到子线程发出的消息,处理
*/
//处理消息
Log.d(TAG, "handleMessage: "+msg.what);
if(msg.what==1002){
textView.setText("imooc");
Log.d(TAG, "handleMessage: "+msg.arg1);
Log.d(TAG, "handleMessage: "+msg.arg2);
Log.d(TAG, "handleMessage: "+msg.obj);
}
}
};
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
/**
* 子线程:
*/
//有可能做大量耗时操作
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(4*1000);//休眠4秒
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 通知UI更新
*/
Message message=Message.obtain();//不直接使用new
message.what=1002;
message.arg1=1003;
message.arg2=1004;
message.obj=MainActivity.this;
handler.sendMessage(message);
}
}).start();
}
});
}
}
logcat:02-13 20:47:08.195 7817-7817/com.example.handlerstudy D/MainActivity: handleMessage: 1002 02-13 20:47:08.196 7817-7817/com.example.handlerstudy D/MainActivity: handleMessage: 1003 02-13 20:47:08.196 7817-7817/com.example.handlerstudy D/MainActivity: handleMessage: 1004 02-13 20:47:08.196 7817-7817/com.example.handlerstudy D/MainActivity: handleMessage: com.example.handlerstudy.MainActivity@96eaa15
【注意】Message.obtain()方法
做了一个缓存,有的话直接取,没有的话,再new Message()
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
Handler.sendMessageAtTime(Message ,uptimeMillis)
约定一个时间发送消息(绝对的)
handler.sendMessageAtTime(message, SystemClock.uptimeMillis()+3*1000);
Handler.sendMessageDelayed(Message,delayMillis)
2秒后送达(相对的)handler.sendMessageDelayed(message,2*1000);
2、Handler.post()
注意: 在main主线程执行完后立即调用
MainActivity–>onClick()–>new Thead中
Runnable runnable = new Runnable() {
@Override
public void run() {
int a = 1 + 2 + 3;
System.out.println(a);
}
};
handler.post(runnable);
runnable.run();
Handler.postAtTime(Runnable,long)
handler.postAtTime(runnable,4*1000);
Handler.postDelayed(Runnable,long)
延迟4秒
注意: postDelayed的方法意在延迟执行,在main主线程执行完后延迟3秒后开始调用。handler.postDelayed(runnable,4*1000);
六、Handler实践的三种效果
1、异步下载文件更新进度条
具体代码
activity_download.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".DownloadActivity">
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="New Button"
/>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
android:layout_gravity="center_horizontal"
/>
</LinearLayout>
DownloadActivity
package com.example.handlerstudy;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
public class DownloadActivity extends AppCompatActivity {
private static final int DOWNLOAD_MESSAGE_FAIL_CODE = 100002;
private final int DOWNLOAD_MESSAGE_CODE = 100001;
private static Handler mHandler;
private String APP_URL="http://download.sj.qq.com/upload/connAssitantDownload/upload/MobileAssistant_1.apk";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_download);
final ProgressBar progressBar=(ProgressBar)findViewById(R.id.progressBar);
/**
* 主线程 --> start
* 点击按钮 |
* 发起下载 |
* 开启子线程做下载 |
* 下载过程中通知主线程 --> 主线程更新进度条
*/
findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
download(APP_URL);
}
}).start();
}
});
mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what){
case DOWNLOAD_MESSAGE_CODE:
progressBar.setProgress((Integer) msg.obj);
break;
case DOWNLOAD_MESSAGE_FAIL_CODE:
break;
}
}
};
}
private void download(String appUrl) {
try {
URL url=new URL(appUrl);
URLConnection urlConnection=url.openConnection();
/**
* 获取文件的流
*/
InputStream inputStream=urlConnection.getInputStream();
/**
* 获取文件的总长度
*/
int contentLength=urlConnection.getContentLength();
/**
* 获取存储设备的地址
* File.separator:斜杠/
*/
String downloadFolderName= Environment.getExternalStorageDirectory()
+ File.separator+"imooc"+File.separator;
Log.d("哈哈哈哈哈哈:", String.valueOf(Environment.getExternalStorageDirectory()));
File file=new File(downloadFolderName);
if(!file.exists()){
file.mkdir();//创建
}
String fileName=downloadFolderName+"imooc.apk";
File apkFile=new File(fileName);
if(apkFile.exists()){
apkFile.delete();
}
int downloadSzie=0;
byte bytes[]=new byte[1024];//用于缓存
int length=0;
//输出
OutputStream outputStream=new FileOutputStream(fileName);
while ((length=inputStream.read(bytes)) != -1){
outputStream.write(bytes,0,length);
downloadSzie += length;
/**
* update UI
*/
Message message=Message.obtain();
message.obj=downloadSzie * 100 / contentLength ;
message.what= DOWNLOAD_MESSAGE_CODE;
mHandler.sendMessage(message);
}
inputStream.close();
outputStream.close();
} catch (MalformedURLException e) {
NotifyDownloadFailed();
e.printStackTrace();
} catch (IOException e) {
NotifyDownloadFailed();
e.printStackTrace();
}
}
private void NotifyDownloadFailed() {
Message message=Message.obtain();
message.what= DOWNLOAD_MESSAGE_FAIL_CODE;
mHandler.sendMessage(message);
}
}
Q:怎么打开模拟机的文件管理器
A:点击Android Studio侧边的Device File Explorer。
效果图:
Handler部分黄色的高亮显示,有内存泄漏的风险
内存泄漏
原因:如果发件人传入上下文,activity可能已经被销毁,但异步里可能还持有该activity的引用,因为垃圾回收器GC不会把它回收掉
2、倒计时的实现
内存泄漏解决
使用弱引用解决了内存泄漏的风险
具体代码
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:padding="16dp"
>
<!-- 倒计时 -->
<TextView
android:id="@+id/countdownTimeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/maxTime"
android:textSize="36sp"
android:layout_centerInParent="true"
/>
</RelativeLayout>
MainActivity
package com.example.handlerstudy2;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.TextView;
import java.lang.ref.WeakReference;
public class MainActivity extends AppCompatActivity {
/**
* 倒计时标记handler code
*/
public static final int COUNTDOWN_TIME_CODE = 100001;
/**
* 倒计时间隔
*/
public static final int DELAY_MILLIS = 1 * 1000;
/**
* 倒计时最大值
*/
public static final int MAX_COUNT = 10;
private TextView mCountdownTimeTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//得到控件
mCountdownTimeTextView = (TextView) findViewById(R.id.countdownTimeTextView);
//创建了一个handler
CountdownTimeHandler handler = new CountdownTimeHandler(this);
//新建了一个message
Message message = Message.obtain();
message.what = COUNTDOWN_TIME_CODE;
message.arg1 = MAX_COUNT;
//第一次发送message
handler.sendMessageDelayed(message, DELAY_MILLIS);//延迟1秒
}
public static class CountdownTimeHandler extends Handler {
/**
* 倒计时最小值
*/
static final int MIN_COUNT = 0;
final WeakReference<MainActivity> mWeakReference;
CountdownTimeHandler(MainActivity activity) {
mWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
//获取当前activity的弱引用
MainActivity activity = mWeakReference.get();
switch (msg.what) {
case COUNTDOWN_TIME_CODE:
int value = msg.arg1;
activity.mCountdownTimeTextView.setText(String.valueOf(value--));
//循环发的消息控制
if (value >= MIN_COUNT) {
Message message = Message.obtain();
message.what = COUNTDOWN_TIME_CODE;
message.arg1 = value;
sendMessageDelayed(message, DELAY_MILLIS);
}
break;
}
}
}
}
效果图:
10 --> 0 的倒计时
3、用Handler来实现简单打地鼠游戏
具体代码
activity_diglett.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF">
<!-- 地鼠,随机出现 -->
<ImageView
android:id="@+id/image_view"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/diglett"
android:visibility="gone"
/>
<Button
android:id="@+id/start_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:text="@string/begin_game"
/>
<TextView
android:id="@+id/text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:textAppearance="?android:attr/textAppearanceLarge"
/>
</RelativeLayout>
DiglettActivity
package com.example.handlerstudy;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import java.lang.ref.WeakReference;
import java.util.Random;
//Diglett:地鼠
public class DiglettActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener {
private TextView mResultTextView;//显示文本
private ImageView mDiglettImageView;//地鼠图像
private Button mStartButton;//开始按钮
private static final int CODE = 123;
//地鼠出现位置
public int[][] mPosition = new int[][]{
{342, 180}, {432, 880},
{521, 256}, {429, 780},
{456, 976}, {145, 665},
{123, 678}, {564, 567},
};
private int mTotalCount;//所有的数量
private int mSuccessCount;//成功的数量
public static final int MAX_COUNT = 10;//地鼠最大数量
private DiglettHandler mHandler = new DiglettHandler(this);//创建handler
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_diglett);
initView();//初始化
setTitle("打地鼠");
}
/**
* 初始化控件
*/
private void initView() {
mResultTextView = (TextView) findViewById(R.id.text_view);
mDiglettImageView = (ImageView) findViewById(R.id.image_view);
mStartButton = (Button) findViewById(R.id.start_button);
/**
* 用实现的方法实现点击事件
*/
mStartButton.setOnClickListener(this);
mDiglettImageView.setOnTouchListener(this);
}
/**
* 实现View.OnClickListener必须实现的方法
*
* @param v
*/
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.start_button:
start();
break;
}
}
private void start() {
//发送消息 handler.sendmessagedelayed
mResultTextView.setText("开始啦");
mStartButton.setText("游戏中...");
mStartButton.setEnabled(false);//不能按了
next(0);
}
private void next(int delayTime) {
int position = new Random().nextInt(mPosition.length);
Message message = Message.obtain();
message.what = CODE;
message.arg1 = position;
mHandler.sendMessageDelayed(message, delayTime);
mTotalCount++;
}
/**
* 实现View.OnTouchListener必须实现的方法
* @param v
* @param event
* @return
*/
@Override
public boolean onTouch(View v, MotionEvent event) {
v.setVisibility(View.GONE);
mSuccessCount++;
mResultTextView.setText("打到了" + mSuccessCount + "只,共" + MAX_COUNT + "只");
return false;
}
//防止内存泄漏
public static class DiglettHandler extends Handler {
private static final int RANDOM_NUMBER = 500;
public final WeakReference<DiglettActivity> mWeakReference;
public DiglettHandler(DiglettActivity activity) {
mWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//拿到activity
DiglettActivity activity = mWeakReference.get();
switch (msg.what) {
case CODE:
if (activity.mTotalCount > MAX_COUNT) {
activity.clear();
Toast.makeText(activity, "地鼠打完了!", Toast.LENGTH_LONG).show();
return;
}
int position = msg.arg1;
activity.mDiglettImageView.setX(activity.mPosition[position][0]);
activity.mDiglettImageView.setY(activity.mPosition[position][1]);
activity.mDiglettImageView.setVisibility(View.VISIBLE);
int randomTime = new Random().nextInt(RANDOM_NUMBER) + RANDOM_NUMBER;
activity.next(randomTime);
break;
}
}
}
private void clear() {
mTotalCount = 0;
mSuccessCount = 0;
mDiglettImageView.setVisibility(View.GONE);
mStartButton.setText("点击开始");
mStartButton.setEnabled(true);
}
}
效果图: