架构师学习--Handler及ThreadLocal

首先引入几个问题:

  • Handerl如何避免内存泄漏
  • 更新view,难道只能在UI线程吗?子线程不可以吗?
  • 子线程中为什么不能创建Handler
  • new Handler的两种方式区别
  • ThreadLocal用法及原理

一、Handerl如何避免内存泄漏

1、首先写一段内存泄漏的代码,如下:

public class HandlerActivity extends AppCompatActivity {
    private TextView mTextView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);

        new Thread(new Runnable() {
            @Override
            public void run() {
                Message msg = new Message();
                msg.what = 1;
                msg.obj = "qb";

                //延迟2s发送消息
                SystemClock.sleep(2000);
                mHandler.sendMessage(msg);
            }
        }).start();

    }

    private Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            //处理消息 跳转到另外一个界面
            Log.e("HandlerActivity销毁了销毁了", "接收到消息,仍然跳转");
            startActivity(new Intent(HandlerActivity.this, SecondActivity.class));
            return false;
        }
    });

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.e("onDestroy》》》", "HandlerActivity销毁了");
    }
}

创建一个子线程,在子线程中延迟2s,发送消息。handler接收到消息跳转到另外一个界面。这里我们只需要在2s内推出当前app。等待2s结束后,发现app重新启动,并跳转至第二个界面。说明发生了内存泄漏,运行结果截图如下:

那么如何避免这种内存泄漏呢?

2、解决内存泄漏

(1)尝试在onDestroy方法中移除消息,代码如下:

@Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeMessages(1);
        Log.e("onDestroy》》》", "HandlerActivity销毁了");
    }

运行发现没有效果,这是因为在销毁的时候我们的线程仍在休眠,这个消息并没有压入队列中,所以我们移除的为空。即移除之前并没有调用sendMessage相关方法。

(2)使用sendMessageDelay方法代替sendMessage,onDestroy()方法处理如上。局部代码如下:

new Thread(new Runnable() {
            @Override
            public void run() {
                Message msg = new Message();
                msg.what = 1;
                msg.obj = "qb";

                //延迟2s发送消息
               // SystemClock.sleep(2000);
                //mHandler.sendMessage(msg);

                mHandler.sendMessageDelayed(msg,2000);
            }
        }).start();

调用该方法会把消息压入到队列,并延迟2s进行唤醒。那么在onDestroy方法中就可以将该消息进行移除。

(3)如果强行使用sendMessage()方法,可以使用如下解决方法:

new Thread(new Runnable() {
            @Override
            public void run() {
                Message msg = new Message();
                msg.what = 1;
                msg.obj = "qb";

                //延迟2s发送消息
               SystemClock.sleep(2000);
                if(mHandler != null) mHandler.sendMessage(msg);
            }
        }).start();

@Override
    protected void onDestroy() {
        super.onDestroy();
        //这一行加不加都可以
        mHandler.removeMessages(1);
        mHandler = null;
        Log.e("onDestroy》》》", "HandlerActivity销毁了");
    }

只需要在onDestroy方法中将handler置空,并且线程中加入判断即可。这样当界面销毁的时候就不会将消息加入到消息队列。

(4)尝试2,是否可以定义全局Message,在onDestory方法中回收message?代码如下:

msg = new Message();
        new Thread(new Runnable() {
            @Override
            public void run() {

                msg.what = 1;
                msg.obj = "qb";

                //延迟2s发送消息
                SystemClock.sleep(2000);
                mHandler.sendMessage(msg);

                //if(mHandler != null) mHandler.sendMessageDelayed(msg,2000);
            }
        }).start();


 @Override
    protected void onDestroy() {
        super.onDestroy();
        //这一行加不加都可以
        //mHandler.removeMessages(1);
        //mHandler = null;
        Log.e("onDestroy》》》", "HandlerActivity销毁了");
        msg.recycle();
    }

运行结果截图如下:

这样虽然解决了跳转的问题,但是抛出了异常"This message is already in use."但是这种方式仅在调用sendMessageDelay()方法不会抛出这种异常,具有局限性。不推荐使用。

(5)使用handler.removeCallbacksAndMessages(null)。就是在Acticity退出时最好调用handler.removeCallbacksAndMessages(null),移除handler的所有消息,避免内存泄漏。记住调用handler.removeCallbacksAndMessages(null)只会移除当前handler的所有消息,如何存在多个handler,需要每一个handler都调用一次。

二、更新view,难道只能在UI线程吗?子线程不可以吗?

1、同样写一段测试代码:

public class HandlerActivity extends AppCompatActivity {
    private TextView mTextView;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        mTextView = findViewById(R.id.test_tv);
        mTextView.setText("1111111111");
        new Thread(new Runnable() {
            @Override
            public void run() {

                mTextView.setText("qbbbbbbb");
            }
        }).start();

    }

   
}

在子线程中直接更新textView。运行发现界面更新成功了,但是如果我们在子线程弹出toast,就会抛出异常,截图如下:

同样的,我们可以在更新textView之前加上线程休眠的代码,发现抛出了异常,截图如下:

查看setText()源码。跟踪方法顺序setText()-->...-->checkForRelayout()-->[ requestLayout();invalidate();] 先跟踪requestLayout()方法。最终会调用ViewRootImpl中的requestLayout()方法,代码如下:

 @Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

最总会调用checkThread()方法,检查当前线程如果不是在主线程,就会抛出异常。对于invalidate()方法,它是通知界面刷新方法。也就是说如果当前ui刷新所在的线程和view的创建线程在同一个线程,校验通过。反之,就会抛出异常。这里延迟2s,UI已经加载到窗口,是在主线程中,2s后,发现更新在子线程,所以会抛出异常。这里可以尝试在onCreate()和onResume()方法中不进行延迟2s的更新(没有问题),在onStop()方法中再次更新(发现有问题),根本原因,还是checkThread()发生在布局加载完成后,才进行校验。代码如下:

public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        Toast result = new Toast(context, looper);

        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
        tv.setText(text);

        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }

所以,更新view,不一定在主线程中。子线程中满足一定条件也可以进行界面刷新。

三、子线程中为什么不能直接创建Handler

1、同样写一段测试代码,如下:

new Thread(new Runnable() {
            @Override
            public void run() {

                new Handler();

            }
        }).start();

运行保存,如下截图:

2、跟踪源码进入Handler的构造方法,最终会进入两个参数的构造方法,源码如下:

不难看到,当mLooper==null的时候就会发生上面的异常。那么mLooper从何而来,调用Looper.myLooper()方法。代码如下:

这里的threadLocal后面会讲到,它其实是以键值对的形式存储对象。其中key为当前handler所在线程,value为Looper对象。并且threadLocal是全局唯一的。它的get方法其实是从theadLocalMap中获取Looper对象。在上面的写法中,显然子线程的looper是没有存储到threadLocalMap中的。这也就验证了在子线程找那个是不能直接new Handler()的。

四、new Handler的两种方式区别

1、写一段测试代码,如下:

 @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        mTextView = findViewById(R.id.test_tv);
        mTextView.setText("1111111111");

        //方式1
        Handler handler1 = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
            }
        };


        //方式2
        Handler handler2 = new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                return false;
            }
        });
    }

不难发现,两种方式最终消息处理的地方都在handleMessage()中。我们可以通过源码跟踪。进入Handler.class类中,消息队列取出消息以后,交给handler处理的方法dispatchMessage()方法,代码如下:

/**
     * Handle system messages here.
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

2、if里面的msg.callback其实就是runnable接口(调用handler.post方法),这里先不关心,在else里面,如果我们使用第二种方式那么mCallback(接口)就不为空,就会执行回调方法(mCallback.handleMessage)。否则执行当前类的备用方法handleMessage。这里推荐使用接口的方法。贴上一段回调接口,和备用方法源码:

/**
     * Callback interface you can use when instantiating a Handler to avoid
     * having to implement your own subclass of Handler.
     */
    public interface Callback {
        /**
         * @param msg A {@link android.os.Message Message} object
         * @return True if no further handling is desired
         */
        public boolean handleMessage(Message msg);
    }
    
    /**
     * Subclasses must implement this to receive messages.
     */
    public void handleMessage(Message msg) {
    }

五、ThreadLocal用法及原理

1、写一段单元测试代码,如下:

public class TreadLocalTest {
    @Test
    public void threadLocalTest(){

        //初始哈threadLocal,键为主线程,值默认值为"初始值"
        final ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
            @Override
            protected String initialValue() {
                return "初始值";
            }
        };
        System.out.printf("当前mainThread线程--值:%s\n",threadLocal.get());
        threadLocal.set("主线程");
        System.out.printf("当前mainThread线程--set值:%s\n",threadLocal.get());

        //子线程thread-1
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.printf("当前子线程thread-1--值:%s\n",threadLocal.get());
                threadLocal.set("子线程1");
                System.out.printf("当前子线程thread-1--set值:%s\n",threadLocal.get());
            }
        }).start();

        //子线程thread-2
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.printf("当前子线程thread-2--值:%s\n",threadLocal.get());
                threadLocal.set("子线程2");
                System.out.printf("当前子线程thread-2--set值:%s\n",threadLocal.get());
            }
        }).start();
    }
}

一开始我在主线程中创建了一个threadLocal对象,并且重写了它的initialValue()方法。这里创建的threadLocal对象存放的key是当前线程,value是字符串。执行以上代码,结果如下:

2、分析结果

在主线程中创建threadLocal,调用threadLocal.set()方法,此时key为mainThread,value将会被替换成"主线程"。在子线程中一开始直接调用get方法,发现打印的值为initvaule()返回的值,为什么不是mainThread开始set的值呢?这是因为在子线程中,threadLocal中key为thread-1或thread-2。一开始get是无法取到的,所以返回默认值。当我们重新set以后才可以取到新的值。所以谨记一点就是threadlocal的key永远是当前线程。

3、源码分析

找到threadLocal.get()方法,源码如下:

    
    public T get() {
        //获取当前所在线程
        Thread t = Thread.currentThread();

        //获取当前线程的threadLocalMap
        ThreadLocalMap map = getMap(t);

        //不为空的话从map中取出当前线程的值
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }    

        //为空的话返回initValue值
        return setInitialValue();
    }

    
    private T setInitialValue() {
        //返回initialValue初始值
        T value = initialValue();

        //获取当前线程
        Thread t = Thread.currentThread();

        获取当前线程的threadLocalMap
        ThreadLocalMap map = getMap(t);
    
        //如果不为空直接设置当前线程的默认值为value 否则创建map
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

不难发现无论是主线程还是子线程,一开始threadLocal一定是为空的。那么最终都会调用setInitialValue()方法,将默认值放到map中,并返回默认值,这也就解释了每个线程一开始调用get方法都会返回"初始值"。如果threaLocal不为空就会直接将原来的value值替换成现在的,放到map中。再看一些threaLocal.get()方法,代码如下:

public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        
        //获取当前线程的threadLocalMap
        ThreadLocalMap map = getMap(t);

        //map如果不为空就替换成新的value值 ,为空创建新的map并setValue值
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

当当前线程的threadLocalMap创建成功以后,执行set方法以后会替换新的值,这也就解释了当再次调用set方法以后,get出来的就是当前的最新值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值