Android之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);
    }
}

效果圖:
在這裏插入圖片描述在這裏插入圖片描述

發佈了64 篇原創文章 · 獲贊 5 · 訪問量 9310
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章