文章目錄
一、課程背景
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
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);
}
}
效果圖: