记为什么在子线程中 Toast.makeText 会报错?

下面是okhttp的一个简单的异步调用,onResponse 是在子线程中回调的,使用Toast弹出消息时抛出异常

Can't toast on a thread that has not called Looper.prepare()
OkHttpClient okHttpClient=new OkHttpClient.Builder().build();
Request request=new Request.Builder().url("https://www.jianshu.com/p/b1e631a3ea2f").get().build();
Call call=okHttpClient.newCall(request);
call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {

    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {

        //Looper.prepare();
        Toast.makeText(MainActivity.this,response.body().string(),Toast.LENGTH_SHORT).show();
        //Looper.loop();
    }
});

这个异常相信大家不在陌生吧? 那么为什么了?

当我们点开一个app应用时,它的真正程序入口 是在 ActivityThread 类中 的 mian方法

public static void main(String[] args) {
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");

    // Install selective syscall interception
    AndroidOs.install();

    // CloseGuard defaults to true and can be quite spammy.  We
    // disable it here, but selectively enable it later (via
    // StrictMode) on debug builds, but using DropBox, not logs.
    CloseGuard.setEnabled(false);

    Environment.initForCurrentUser();

    // Make sure TrustedCertificateStore looks in the right place for CA certificates
    final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
    TrustedCertificateStore.setDefaultUserDirectory(configDir);

    Process.setArgV0("<pre-initialized>");

    Looper.prepareMainLooper();//重点 =================

    // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
    // It will be in the format "seq=114"
    long startSeq = 0;
    if (args != null) {
        for (int i = args.length - 1; i >= 0; --i) {
            if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
                startSeq = Long.parseLong(

ActivityThread 它所处的是一个主线程,当app启动时,会执行里面的main()方法,执行 main()方法中的Looper.prepareMainLooper(),创建一个消息循环对象,并且这个 Looper对象是全局唯一的

public static void prepareMainLooper() {
    prepare(false);//继续执行这个方法 创建全局唯一 主线程Looper对象  
    synchronized (Looper.class) {
        if (sMainLooper != null) {//如果sMainLooper  早已初始化 不为空 就会抛出这个异常
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        sMainLooper = myLooper();
    }
}
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {//首先从sThreadLocal去取 sMainLooper  对象,如果不为空 就抛出异常 这说明它已经被初始化过一次了  想要诱发这个异常 只需要 在Activity中(主线程中),我们就在oncreate()方法中 调用 Looper.prepare(); 这行代码 就会触发这个异常
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));//new 一个Looper对象 把它添加到 sThreadLocal当中去
}
public void set(T value) {
    Thread t = Thread.currentThread();//得到当前线程的name值 此时我们app启动时  这时候是主线程  当然其他情况下 这个 Thread 也会是子线程的情况
    ThreadLocalMap map = getMap(t);//把 t当作key值取值  
    if (map != null)//如果这个 ThreadLocalMap  不为空 就是已经初始化过一次了
        map.set(this, value);//就直接把这个 looper对象添加进去  当前线程name值作为key值 方便后面取looper
    else
        createMap(t, value);//否则 新创建一个ThreadLocalMap   并把 looper对象添加进去 当前线程name值作为key值 方便后面取looper
}

至此 我们主线程(全局唯一的一个Looper消息循环对象)就已经创建好了 并且 把它添加进了ThreadLocalMap 这个集合中去

当我们在子线程中 使用 Toast 弹出消息时,

Toast的源码里面 有创建Handler对象,因为我们在子线程中使用Toast,所以new handler()也是在子线程中

mHandler = new Handler(looper, null) {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case SHOW: {
                IBinder token = (IBinder) msg.obj;
                handleShow(token);
                break;
            }
            case HIDE: {
                handleHide();

我们看看 new Handler() 到底干了什么事情?

public Handler() {
    this(null, false);
}

再执行下面的构造方法

public Handler(@Nullable Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper();//当 new handler()时,就会执行这句代码 显然是 得到一个Looper对象
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread " + Thread.currentThread()
                    + " that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}
public static @Nullable Looper myLooper() {
    return sThreadLocal.get();//这个Looper对象 是从sThreadLocal里面去取
}

最终执行到这里来 是在这里取Looper对象的

public T get() {
    Thread t = Thread.currentThread();//此时 我们在子线程中使用Toast 这里的t 就是子线程
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

由于我们 只存过 主线程的Looper对象 ,子线程根本没有初始化 就直接取 肯定取不到 ,所以就会抛出此异常,这就是Toast不能直接在子线程中使用,如果想要使用 就添加 Looper.prepare(); 这行代码即可

mLooper = Looper.myLooper();
if (mLooper == null) {
    throw new RuntimeException(
        "Can't create handler inside thread " + Thread.currentThread()
                + " that has not called Looper.prepare()");
}
call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {

    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {

        //Looper.prepare();
        Toast.makeText(MainActivity.this,response.body().string(),Toast.LENGTH_SHORT).show();
        //Looper.loop();
    }
});
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值