内容概览
- Handler是什么
- 为什么要使用Handler
- Handler/Looper/MessageQueue/Message
- Handler如何去实现(三种实现:1、下载文件并更新进度条 2、倒计时 3、打地鼠的游戏实现)
- 工作原理
- 如果更好的使用
- 扩展及总结
一、课程背景:
1、UI线程/主线程/AtivityThread
一个应用启动会开启一个主进程,接着这主进程会开启一个主线程,所有东西会开始在主线程中运作
主线程也叫做UI线程,为什么?因为主线程主要负责处理我们UI界面所有消息相关的分发
2、线程不安全
线程安全不安全针对的事多线程的。
假如有个控件是button,多个线程都来更新这个button,这个button的状态会不会混乱
线程安全就是button处理了这种多线程的状态,给button加锁,无论你哪个线程来方法都可以来访问,这就是线程安全,但加锁又导致性能的下降
线程不安全:没有针对多线程进行特别处理;UI线程是线程不安全的
3、消息循环机制(处理各种UI事件)
什么意思?
也就是说我一进入主线程,就开始进入循环,这个循环是一个死循环,用来处理消息、处理操作
当用户在UI线程点了Button按钮,它立马将这个消息发给这个循环,这个循环就开始处理
应用场景:
1.定时任务(消息和可执行的对象在未来的一段时间内执行)
2.线程和线程之间的处理(A线程到B线程中执行某个动作)
二、Handler相关概念简介
1.Handler
2.Looper(循环者)
3.Message
4.MessageQueue(消息队列)
寓意:Looper相当一个循环抽水机,MessageQueue相当于水井,message相当于水井里的水
通过Looper不断的抽出来到handlerMessage再交给handler处理者直接执行run;
- 每个线程里面都有它单独的Looper和MessageQueue
- 主线程默认创建了Looper和MessageQueue
- 子线程不会默认创建;创建方式如下
//子线程不默认开始Looper和MessageQueue
new Thread(new Runnable() {
@Override
public void run() {
try {
//子线程开启的方式
//准备好Looper
Looper.prepare();
Handler handler =new Handler();
//让它开始循环
Looper.loop();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
三、使用Handler的实现和优化
3-1代码实现最简单的Handler
1.Handler.sendMessage();
2.Handler.post();
首先,在主线程中新建一个handler对象实现他的sendMessage()方法,在这个方法里面进行接受处理子线程传来的更新UI的操作
子线程中进行耗时的操作,同时也想进行更新UI,就通过sendEmptyMessage();方法来将消息传递给主线程的handler来处理
handler.sendEmptyMessage(1001);//1001是这个消息的代号
主线程:
//创建一个handler对象
//实现它的handlerMessage方法
final Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//处理消息
Log.d(TAG, "handlerMessage:" + msg.what);
/**
*主线程接到子线程发出来的消息,处理
*/
if (msg.what == 1001)
//执行刷新UI操作
textView.setText("我是代号1001的子线程消息通过子线程发到主线程中处理");
}
};
子线程:
//给button设置监听事件
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//可能要做大量耗时操作
/**
* 子线程
*/
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);//子线程休眠3秒,模仿耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* 通知UI线程更新
*/
handler.sendEmptyMessage(1001);
}
}
).start();
}
});
效果:
3-2Handler常见的发送消息方法
Handler.sendMessage(里面装的是一个消息);message消息进行打包,通过sendMessage()将消息发给主线程
//使用sendMessage()方法前的消息要打包
Message message=Message.obtain();
message.what = 1002;//消息编号
message.arg1 = 1003;
message.arg2 = 1004;
message.obj =MainActivity.this;//当前对象
handler.sendMessage(message);
主线程相当于收到一个打包好的快递,进行拆包
//创建一个handler对象
//实现它的handlerMessage方法
final Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
/**
*主线程接到子线程发出来的消息,处理
*/
if(msg.what ==1002){//拆包
Log.d(TAG, "handlerMessage:" + msg.what);
Log.d(TAG, "handlerMessage:" + msg.arg1);
Log.d(TAG, "handlerMessage:" + msg.arg2);
Log.d(TAG, "handlerMessage:" + msg.obj);
textView.setText("代号1002消息通过子线程sendMessage方法发到主线程处理");
}
}
};
定时任务:
- 定时发送sendMessageAtTime();
- 延迟发送sendMessageDelayed();
//定时发送消息,时间是绝对的
handler.sendMessageAtTime(message,SystemClock.uptimeMillis()+3000);
//延迟发送消息,时间是相对的
handler.sendMessageDelayed(message,2000);
post();方法 它可以直接在run函数中执行你想要做的事情(比如更新UI),他也有postDelayed()和postAtTime()方法
//提交消息,可以直接在run里面做你想做的事情
//更新UI操作转到主线程进行
handler.post(new Runnable() {
@Override
public void run() {
int a=1+2+3;
TextView.setText("xxxxx");
....
}
});
3-3异步下载更新进度条
/**
* 主线程——>start
* 点击按钮 |
* 发起下载 |
* 开启子线程下载 |
* 下载过程中通知主线程 |——>主线程更新UI
*
*/
注意事项:
一、读写文件的时候要获取权限
1.在AndroidManifest.xml中声明权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
2.android6.0之后要在动态申请权限
//android6.0之后要动态获取权限
private void checkPermission(Activity activity) {
// Storage Permissions
final int REQUEST_EXTERNAL_STORAGE = 1;
String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE};
try {
//检测是否有写的权限
int permission = ActivityCompat.checkSelfPermission(DownLoadActivity.this,
"android.permission.WRITE_EXTERNAL_STORAGE");
if (permission != PackageManager.PERMISSION_GRANTED) {
// 没有写的权限,去申请写的权限,会弹出对话框
ActivityCompat.requestPermissions(DownLoadActivity.this, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
}
} catch (Exception e) {
e.printStackTrace();
}
}
全局变量定义
public static final int DOWNLOAD_MESSAGE_CODE = 1001;
private static final int DOWNLOAD_MESSAGE_FAIL_CODE = 1000;
public static final String appUrl = "http://download.sj.qq.com/upload/connAssitantDownload/upload/MobileAssistant_1.apk";
private static Handler handler;
private TextView textview;
private int progress;
public ProgressBar progressBar;
二、主线程进行UI更新
/**
* 主线程进行UI更新
* 进度条的更新
*/
handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case DOWNLOAD_MESSAGE_CODE://下载发过来的msg.what(消息编号1001)
progressBar.setProgress((Integer) msg.obj);
//textview.setText(msg.obj+"%");
break;
case DOWNLOAD_MESSAGE_FAIL_CODE://下载失败时消息编号为1000
Log.i("download", "fail");
Toast.makeText(DownLoadActivity.this,
"下载失败!", Toast.LENGTH_SHORT)
.show();
break;
}
}
};
三、子线程进行耗时操作(下载、网络请求)
findViewById(R.id.button_download).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
/**
* 子线程中进行下载
*/
new Thread(new Runnable() {
@Override
public void run() {
download(appUrl);
}
}).start();//别忘了start()
}
});
四、下载的方法
private void download(String appUrl) {
try {
URL url = new URL(appUrl);
//打开url对象的链接,获得connection链接
URLConnection urlConnection = url.openConnection();
//获取文件的输入流(读取数据)
InputStream inputStream = urlConnection.getInputStream();
//获取文件的总长度
int contentLength = urlConnection.getContentLength();
//创建存储位置,获取sd卡的存储路径/storage/emulated/0/imooc/
String downloadFolderName = Environment.getExternalStorageDirectory()
+ File.separator + "imooc" + File.separator;
//创建这个文件夹
File file = new File(downloadFolderName);
if (!file.exists()) {
file.mkdirs();
}
//再上一个文件夹下又创建一个文件夹
String fileName = downloadFolderName + "imooc.apk";
File apkFile = new File(fileName);
//如果已经有这个文件了,就删除重新下载过
if (apkFile.exists()) {
apkFile.delete();
}
//当前下载的长度处于文件总长度就是progress
int downloadSize = 0;
//建立一个字节数组类似缓存
byte[] bytes = new byte[1024];
int length = 0;
//输出流将数据写入到这个文件里面
OutputStream outputStream = new FileOutputStream(fileName);
//从inputStream中读取数据,当没到文件末尾
while ((length = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, length);
downloadSize += length;//第一次下载1024,第二次又是1024,累加
/**
* 更新UI
*/
Message message = Message.obtain();
//将当前的进度传给主线程更新UI
message.obj = downloadSize * 100 / contentLength;
progress = downloadSize * 100 / contentLength;
message.what = DOWNLOAD_MESSAGE_CODE;
//1将消息发送给主线程
handler.sendMessage(message);
//2用runOnUiThread更新UI,也可以放到主线程里面进行更新
runOnUiThread(new Runnable() {
@Override
public void run() {
textview.setText(progress + "%");
//下载完成提示
if (progress == progressBar.getMax()) {
Toast.makeText(DownLoadActivity.this,
"下载完成!", Toast.LENGTH_SHORT)
.show();
}
}
});
}
inputStream.close();
outputStream.close();
//下载失败也要发消息
} catch (MalformedURLException e) {
notifyDownloadFail();
e.printStackTrace();
} catch (IOException e) {
notifyDownloadFail();
e.printStackTrace();
}
}
//下载失败也要将消息发给主线程处理
private void notifyDownloadFail() {
Message message = Message.obtain();
message.what = DOWNLOAD_MESSAGE_FAIL_CODE;
//将消息发送给主线程
handler.sendMessage(message);
}
效果:
在/storage/emulated/0/imooc/里下载好的imooc.apk
3-4倒计时实现
什么是内存泄漏?
/**内存泄漏:
* Handler通常会伴随着一个耗时的后台线程(例如从网络拉取图片)一起出现,
* 这个后台线程在任务执行完毕(例如图片下载完毕)之后,
* 通过消息机制通知Handler,然后Handler把图片更新到界面
* 然而,如果用户在网络请求过程中关闭了Activity
* 正常情况下,Activity不再被使用,它就有可能在GC检查时被回收掉,但由于这时线程尚未执行完
* 该线程该线程持有Handler的引用,这个Handler又持有Activity的引用,
* 依然持有Activity的引用(TextView)countdowntime
* 所以Activity关闭后,就导致该Activity无法被GC回收(即内存泄露)
*/
方法1:直接使用Handler来处理,缺点:容易造成内存泄漏
Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//循环发消息控制
if(msg!=null&&msg.what==COUNDOWN_TIME_CODE){
int value=9;
countdowntime.setText(value--);
//重写构造一个message,重新发回去(不能直接用msg,否则会闪退)
Message message = Message.obtain();
message.what = COUNDOWN_TIME_CODE;
message.arg1 = value;
sendMessageDelayed(message, 1000);
}
}
};
//创建一个静态的Handler,不会发生内存泄漏
Handler handler = new Handler(this);
Message message = Message.obtain();//从消息池里面拿消息
message.what = COUNDOWN_TIME_CODE;//消息编号
message.arg1 = MAX_COUNT;
//第一次发生message将消息延迟1s发送
handler.sendMessageDelayed(message, DELAY_MILLIS);
}
方法2://创建一个静态的Handler,不会发生内存泄漏
弱引用:
- 可以通过弱引用拿到我们的Activity,再通过Acitivity拿到控件
- 不会内存泄漏的原因
- 因为这个MainActivity是弱引用的
- 当我们持有它,用完之后就会被回收了
public class MainActivity extends AppCompatActivity {
public static final int COUNDOWN_TIME_CODE = 1001;//倒计时handler code
public static final int DELAY_MILLIS = 1000;//倒计时间隔
public static final int MAX_COUNT = 10;//倒计时最大值
private TextView countdowntime;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//得到控件
countdowntime = findViewById(R.id.textview);
//创建一个静态的Handler,不会发生内存泄漏
CountdownTimeHandler handler = new CountdownTimeHandler(this);
Message message = Message.obtain();//从消息池里面拿消息
message.what = COUNDOWN_TIME_CODE;//消息编号
message.arg1 = MAX_COUNT;
//第一次发生message将消息延迟1s发送
handler.sendMessageDelayed(message, DELAY_MILLIS);
}
//直接写一个静态的Handler
public static class CountdownTimeHandler extends Handler {
//弱引用,可以通过弱引用拿到我们的Activity,再通过Acitivity拿到控件
final WeakReference<MainActivity> mWeakReference;
//默认构造方法 command+N 在构造方法中初始化
public CountdownTimeHandler(MainActivity activity) {
/**不会内存泄漏的原因
* 因为这个MainActivity是弱引用的
* 当我们持有它,用完之后就会被回收了
*/
//获取当前Activity的弱引用
mWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//拿到弱引用的MainActivity
MainActivity activity = mWeakReference.get();
switch (msg.what) {
case COUNDOWN_TIME_CODE:
int value = msg.arg1;
//拿到textview
activity.countdowntime.setText(String.valueOf(value--));
//循环发消息控制
if (value >= 0) {
//重写构造一个message,重新发回去(不能直接用msg,否则会闪退)
Message message = Message.obtain();
message.what = COUNDOWN_TIME_CODE;
message.arg1 = value;
sendMessageDelayed(message, 1000);
}
}
}
}
}
结果:
3.5-打地鼠游戏的实现(改)
思路:第一次发送游戏开始消息,之后由自定义的静态Hanlder处理,然后由它循环发送消息编号一致的消息,它自己接受后处理
代码:DigLetAcitivity.java
import android.os.Handler;
import android.os.Message;
import android.os.Vibrator;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
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;
public class DigLetActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener {
private static final int RANDOM_NUMBER = 500;
private final int START_TIME = 3;//开始倒计时
private TextView resultTextView, start_timeTv;
private ImageView diglettImageView;
private Button start_button;
//随机生成地鼠的位置
public int[][] mPosition = new int[][]{
{342, 180}, {432, 880}, {332, 110},
{782, 140}, {466, 380}, {732, 900},
{52, 180}, {562, 80}, {72, 80},
{342, 180}, {432, 880}, {332, 110},
{882, 50}, {652, 130}, {332, 110},
{452, 90}, {132, 340}, {532, 670},
{342, 100}, {132, 280}, {432, 610},
{92, 60}, {832, 880}, {762, 90},
};
private int mTotalCount;//所有地鼠的数量
private int mSuccessCount;//成功的数量
//初始化handler
private DiglettHandler mHandler = new DiglettHandler(this);
//全局变量
public static final int MAX_COUNT = 100;
public static final int NEXT_CODE = 123;
public static final int START_TIME_CODE = 1234;//倒计时编号
private Button start_again;
private static int startTime;
private ImageView girlImageView;
private Vibrator vibrator;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_dig_let);
setTitle("Kiss Game");
initView();
//点击图片手机震动
vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
}
private void initView() {
start_timeTv = findViewById(R.id.start_textview);
resultTextView = findViewById(R.id.text_view);
diglettImageView = findViewById(R.id.image_view_pig);
girlImageView = findViewById(R.id.image_view_girl);
start_again = findViewById(R.id.button_again);
start_button = findViewById(R.id.start_button);
start_button.setOnClickListener(this);
start_again.setOnClickListener(this);
diglettImageView.setOnTouchListener(this);//图片点击事件
girlImageView.setOnTouchListener(this);//图片失败点击事件
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.start_button://开始按钮
start();
break;
case R.id.button_again://重开按钮
clear();
break;
}
}
//第一次发送消息
private void start() {
//发送消息hander.sendMessageDelayer
//初始化
startTime();//倒计时
resultTextView.setText("Start~");
start_button.setText("Playing..");
start_button.setEnabled(false);//设置按钮不能再按了
next(0);
}
//倒计时
private void startTime() {
Message message = Message.obtain();
message.what = START_TIME_CODE;//消息编码
message.arg2 = START_TIME;//倒计时的数
//发消息
mHandler.sendMessageDelayed(message, 1000);
}
//这只打完,下一只出来
private void next(int delayTime) {
//生成地鼠个数,随机选
int position = new Random().nextInt(mPosition.length);
Message message = Message.obtain();
message.what = NEXT_CODE;//消息编码
message.arg1 = position;//发随机地鼠的位置
//发消息
mHandler.sendMessageDelayed(message, delayTime);
mTotalCount++;//每次执行一次next总数累加
}
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (v.getId()) {
case R.id.image_view_pig:
v.setVisibility(View.GONE);//当地鼠被点击后,设置为不可见
mSuccessCount++;//成功点击的数量
break;
case R.id.image_view_girl:
vibrator.vibrate(100);//手机震动100毫秒
v.setVisibility(View.GONE);//当地鼠被点击后,设置为不可见
mSuccessCount--;//点击失败的数量
}
resultTextView.setText(" Kiss: " + mSuccessCount + " Times");
return false;
}
//新建一个静态Handler,防止内存泄漏
public static class DiglettHandler extends Handler {
//弱引用
public final WeakReference<DigLetActivity> mWeakReference;
public DiglettHandler(DigLetActivity activity) {
mWeakReference = new WeakReference<>(activity);
}
//接受到消息进行处理
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//拿到弱引用的Activity
DigLetActivity activity = mWeakReference.get();
switch (msg.what) {
case START_TIME_CODE:
//拿到倒计时时间
startTime = msg.arg2;
if (startTime > 0) {
//显示出来
activity.start_timeTv.setText(String.valueOf(startTime--));
//重写构造一个message,重新发回去(不能直接用msg,否则会闪退)
Message message = Message.obtain();
message.what = START_TIME_CODE;
message.arg2 = startTime;
sendMessageDelayed(message, 1000);
} else {
activity.start_timeTv.setText(String.valueOf(startTime--));
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
activity.start_timeTv.setVisibility(View.GONE);
}
case NEXT_CODE:
//停止条件
if (activity.mTotalCount > MAX_COUNT) {
activity.clear();
Toast.makeText(activity, "Game Over", Toast.LENGTH_SHORT).show();
return;
}
if (startTime < 0) {
//拿到坐标
int position = msg.arg1;
//设置图片的X坐标(二维数组)0是第一个:X。1是第二个:Y
activity.diglettImageView.setX(activity.mPosition[position][0]);
activity.diglettImageView.setY(activity.mPosition[position][1]);
//让图片可见
activity.diglettImageView.setVisibility(View.VISIBLE);
//设置图片的X坐标(二维数组)0是第一个:X。1是第二个:Y
activity.girlImageView.setX(activity.mPosition[position][1]);
activity.girlImageView.setY(activity.mPosition[position][0]);
//让图片可见
activity.girlImageView.setVisibility(View.VISIBLE);
//生成随机时间毫秒为单位
int randomTime = new Random().nextInt(RANDOM_NUMBER) + RANDOM_NUMBER;
activity.next(randomTime);//将时间传给下一个//
}
break;
}
}
}
//清空
public void clear() {
mTotalCount = 0;
mSuccessCount = 0;
diglettImageView.setVisibility(View.GONE);
startTime = 3;
start_timeTv.setText("");
start_timeTv.setVisibility(View.VISIBLE);
resultTextView.setText("");
start_button.setText("Start");
start_button.setEnabled(true);
}
}
鼠标跟踪图片效果: LockScreenView.java
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ImageView;
public class LockScreenView extends ImageView {
public float currentX = 40;
public float currentY = 50;
private Bitmap bmp;
public LockScreenView(Context context) {
super(context);
init();
}
public LockScreenView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public LockScreenView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
//初始化你需要显示的光标样式
private void init() {
if (bmp == null) {
bmp = BitmapFactory.decodeResource(getResources(), R.drawable.kissyou);
}
}
private boolean isClickView = false;//标识是否是人为点击,是则为true
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isClickView == true && bmp != null) {
//创建画笔
Paint p = new Paint();
canvas.drawBitmap(bmp, currentX - (bmp.getWidth() / 2), currentY - (bmp.getHeight() / 2), p);
isClickView = false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//当前组件的currentX、currentY两个属性
this.currentX = event.getX();
this.currentY = event.getY();
isClickView = true;
if (event.getAction() == MotionEvent.ACTION_UP && bmp != null) {
this.currentX = -bmp.getWidth();
this.currentY = -bmp.getHeight();
isClickView = false;
}
//通知改组件重绘
this.invalidate();
//返回true表明处理方法已经处理该事件
return false;
}
}
点错手机震动效果:
1、AndroidManifest.xml获取权限
<uses-permission android:name="android.permission.VIBRATE"/>
2、 创建对象
private Vibrator vibrator;
//点击图片手机震动
vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
点击事件中添加震动时间
vibrator.vibrate(100);//手机震动100毫秒