android 在service中使用toast

在service子线程中使用Toast会抛出"Can't create handler inside thread that has not called Looper.prepare()"异常。百思不得其解,后来终于想通了,分享下我的心得。

Toast可以看出是个特殊的(不可操作,不可获焦,不可触摸)UI组件,因此,他的操作必须放在主线程(UI线程)中。网上看到很多种解决方法,其中一种是在子线程中执行Loop.prepare();方法,虽然能解决问题,但是觉得有违操作UI组件的原则。下面通过源码解释一下原因。先从Toast的创建说起。

当我们想要一个Toast的时候,一般使用一下方法创建:

Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_LONG).show();


F3进去看看makeText()方法,源码如下:

public static Toast makeText(Context context, CharSequence text, 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;
        mTN = new TN();
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
    }


发现在构造方法中又创建了TN,TN是干嘛的呢?继续。

private static class TN extends ITransientNotification.Stub {
        //...此处省略其他代码
        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.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                    | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
            params.format = PixelFormat.TRANSLUCENT;
            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
            params.type = WindowManager.LayoutParams.TYPE_TOAST;
            params.setTitle("Toast");
        }
}


请注意TN的类型,后面接着解释。继续看Toast.show();

    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService();
        String pkg = mContext.getPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }


发现toast发给NotificationManager了,继续进去NotificationManagerService中看看调用了showNextToastLocked()方法。

    private void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
            try {
                record.callback.show();
                scheduleTimeoutLocked(record, false);
                return;
            } catch (RemoteException e) {
                Slog.w(TAG, "Object died trying to show notification " + record.callback
                        + " in package " + record.pkg);
                // remove it from the list and let the process die
                int index = mToastQueue.indexOf(record);
                if (index >= 0) {
                    mToastQueue.remove(index);
                }
                keepProcessAliveLocked(record.pid);
                if (mToastQueue.size() > 0) {
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
            }
        }
    }


发现最终显示是在这条语句中:record.callback.show();这个callback是什么?继续

在NotificationManagerService的enqueueToast()

public void enqueueToast(String pkg, ITransientNotification callback, int duration)
    {
        if (DBG) Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback + " duration=" + duration);

        if (pkg == null || callback == null) {
            Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
            return ;
        }

        synchronized (mToastQueue) {
            int callingPid = Binder.getCallingPid();
            long callingId = Binder.clearCallingIdentity();
            try {
                ToastRecord record;
                int index = indexOfToastLocked(pkg, callback);
                // If it's already in the queue, we update it in place, we don't
                // move it to the end of the queue.
                if (index >= 0) {
                    record = mToastQueue.get(index);
                    record.update(duration);
                } else {
                    // Limit the number of toasts that any given package except the android
                    // package can enqueue.  Prevents DOS attacks and deals with leaks.
                    if (!"android".equals(pkg)) {
                        int count = 0;
                        final int N = mToastQueue.size();
                        for (int i=0; i<N; i++) {
                             final ToastRecord r = mToastQueue.get(i);
                             if (r.pkg.equals(pkg)) {
                                 count++;
                                 if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                     Slog.e(TAG, "Package has already posted " + count
                                            + " toasts. Not showing more. Package=" + pkg);
                                     return;
                                 }
                             }
                        }
                    }

                    record = new ToastRecord(callingPid, pkg, callback, duration);
                    mToastQueue.add(record);
                    index = mToastQueue.size() - 1;
                    keepProcessAliveLocked(callingPid);
                }
                // If it's at index 0, it's the current toast.  It doesn't matter if it's
                // new or just been updated.  Call back and tell it to show itself.
                // If the callback fails, this will remove it from the list, so don't
                // assume that it's valid after this.
                if (index == 0) {
                    showNextToastLocked();
                }
            } finally {
                Binder.restoreCallingIdentity(callingId);
            }
        }
    }


原来这个callback就是前面提到的TN,继续进去看看TN.show()

public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }


现在你一下思路,看看Toast从发送到在屏幕上显示的过程,Toast先通过静态方法,创建一个View和回调,然后把这个record发给NotificationManagerService,service仅仅是管理Toast的显示顺序,因为同一时间只能有一个Toast显示在屏幕,最终的显示是通过回调显示的,也就是说显示的操作最会还是在创建Toast的线程中执行的。

看看TN.show(),现在问题来了,TN中的那个Handler是在哪里初始化的呢?继续代码;

final Handler mHandler = new Handler();    


发现就是一个空构造方法。回忆一下Handler的使用,这个构造方法说明handler.post的runnable是在当前的线程中运行,如果我们在子线程中弹一个Toast,那就是说在这个子线程中显示一个View,显然,子线程不能直接操作UI组件。所以问题就出现了。

解决方法:

将Toast的显示放在在主线程中创建的Handler中。

代码如下:

public class MyService extends Service {

	Handler mHandler = new Handler();
	
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		
		// TODO Auto-generated method stub
		new Thread(){
			
			public void run() {
				mHandler.post(new Runnable() {
					
					@Override
					public void run() {
						
						// TODO Auto-generated method stub
						Toast.makeText(getApplicationContext(), "service start", Toast.LENGTH_LONG).show();
					}
				});
			}
		}.start();
		return super.onStartCommand(intent, flags, startId);
		
	}

	@Override
	public IBinder onBind(Intent intent) {
		
		// TODO Auto-generated method stub
		return null;
		
	}

}


 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值