我写了一个子线程,在里面做了一些图片的操作,操作结束的时候需要弹出toast来提示一下状态,代码如下:

new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    InputStream is = getAssets().open("picture/"+TEST_FILE_NAME);
                    FileOutputStream fos = new FileOutputStream(testImageOnSdCard);
                    byte[] buffer = new byte[8192];
                    int read;
                    try {
                        while ((read = is.read(buffer)) != -1) {
                            fos.write(buffer, 0, read);
                        }
                        Log.e("test","成功将图片从Asset目录下写入到SD卡目录下");
                        Toast.makeText(AUILActivity.this,"成功将图片从Asset目录下写入到SD卡目录下",Toast.LENGTH_SHORT).show();
                    } finally {
                        fos.flush();
                        fos.close();
                        is.close();
                    }
                } catch (IOException e) {
                    Log.e("test","失败将图片从Asset目录下写入到SD卡目录下");
                    Toast.makeText(AUILActivity.this,"将图片从Asset目录下写入到SD卡目录下失败",Toast.LENGTH_SHORT).show();
                }
            }
        }).start();



然后报了下面这个错误:

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
                                                                    at android.os.Handler.<init>(Handler.java:200)
                                                                    at android.os.Handler.<init>(Handler.java:114)
                                                                    at android.widget.Toast$TN.<init>(Toast.java:354)
                                                                    at android.widget.Toast.<init>(Toast.java:101)
                                                                    at android.widget.Toast.makeText(Toast.java:266)

显而易见,如果没有调用Looper.prepare()这个方法,这无法在线程中创建handler。在Toast.make()中toast的实例化出现了问题

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        Toast result = new Toast(context);//就是他,他出了问题,往下看

        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;
    }

那就再看看Toast的构造方法:

public Toast(Context context) {
        mContext = context;//上下文没什么污点,那问题应该是下面这个兄弟  TN   我差点看成TNT  先看看TN是个什么玩意儿
        mTN = new TN();
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
    }

TN源码

private static class TN extends ITransientNotification.Stub {//这里可以看出TN是继承与Binder,用于和系统进程间进行通讯的
        final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };

        final Runnable mHide = new Runnable() {
            @Override
            public void run() {
                handleHide();
                // Don't do this in handleHide() because it is also invoked by handleShow()
                mNextView = null;
            }
        };

        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
        final Handler mHandler = new Handler();//因为我们报的错误就是没有创建Handler 那么我们再看看handler的创建方法中出了啥子问题

        int mGravity;
        int mX, mY;
        float mHorizontalMargin;
        float mVerticalMargin;


        View mView;
        View mNextView;
        int mDuration;

        WindowManager mWM;

        static final long SHORT_DURATION_TIMEOUT = 5000;
        static final long LONG_DURATION_TIMEOUT = 1000;

        TN() {
            // XXX This should be changed to use a Dialog, with a Theme.Toast
            // defined that sets up the layout params appropriately.
            final WindowManager.LayoutParams params = mParams;
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.format = PixelFormat.TRANSLUCENT;
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            params.setTitle("Toast");
            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
        }

        /**
         * schedule handleShow into the right thread
         */
        @Override
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }

        /**
         * schedule handleHide into the right thread
         */
        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);
        }

        public void handleShow() {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            if (mView != mNextView) {
                // remove the old view if necessary
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                // We can resolve the Gravity here by using the Locale for getting
                // the layout direction
                final Configuration config = mView.getContext().getResources().getConfiguration();
                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
                mParams.gravity = gravity;
                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
                    mParams.horizontalWeight = 1.0f;
                }
                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
                    mParams.verticalWeight = 1.0f;
                }
                mParams.x = mX;
                mParams.y = mY;
                mParams.verticalMargin = mVerticalMargin;
                mParams.horizontalMargin = mHorizontalMargin;
                mParams.packageName = packageName;
                mParams.removeTimeoutMilliseconds = mDuration ==
                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }
                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            }
        }

        private void trySendAccessibilityEvent() {
            AccessibilityManager accessibilityManager =
                    AccessibilityManager.getInstance(mView.getContext());
            if (!accessibilityManager.isEnabled()) {
                return;
            }
            // treat toasts as notifications since they are used to
            // announce a transient piece of information to the user
            AccessibilityEvent event = AccessibilityEvent.obtain(
                    AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
            event.setClassName(getClass().getName());
            event.setPackageName(mView.getContext().getPackageName());
            mView.dispatchPopulateAccessibilityEvent(event);
            accessibilityManager.sendAccessibilityEvent(event);
        }        

        public void handleHide() {
            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
            if (mView != null) {
                // note: checking parent() just to make sure the view has
                // been added...  i have seen cases where we get here when
                // the view isn't yet added, so let's try not to crash.
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeView(mView);
                }

                mView = null;
            }
        }
    }

再贴一下Handler的构造方法

public Handler(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();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");//哈哈,这就是我们要找的错误。隐藏的好深。
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

可以看出,在toast的构造方法的时候需要传入当前线程的Looper对象以便于创建handler来进行通讯,如果该对象不存在,那么就会抛出以上异常。

针对这个问题,我在网上查找了一下资料,发现很多人都用了以下方式来解决这个问题:

Looper.prepare();     
Toast.makeText(aActivity.this,"test",Toast.LENGTH_SHORT).show();  
Looper.loop();

这样的确是可以弹出toast,但是这样也带来的更大的问题。调用Looper.loop();后,

子线程不会终止,会一直运行。如果真需要使用这种方式来弹出toast的话,请在以上的代码后面再加上 Loop.quite()。感谢一口仨馍提出这个问题。

车到山前必有路,病树前头万木春。

总有办法来解决,至少用Handler.postRunable就可以优雅的弹出toast了,或者用handlerMessage也可以。

mHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                Toast.makeText(AUILActivity.this,"成功将图片从Asset目录下写入到SD卡目录下",Toast.LENGTH_SHORT).show();
                            }
                        });

这下子toast终于弹出来了。

此外,还可以用runOnUiThread();这种方式来弹出toast,亲测可用。

runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                Toast.makeText(AUILActivity.this,"成功将图片从Asset目录下写入到SD卡目录下",Toast.LENGTH_SHORT).show();
                            }
                        });

想怎么弹就怎么弹。