Android進階:網路與資料儲存—步驟1:Android網路與通訊(第2小節:Handler)
內容概覽
- 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;
三、使用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毫秒