目录

一.Toast成员变量

二. Toast显示流程

1. Toast makeText(@NonNull Context context, @Nullable Looper looper,@NonNull CharSequence text, @Duration int duration)

2.show()

3.NotificationManagerService

(1)public void enqueueToast(String pkg, ITransientNotification callback, int duration,int displayId)

(2)void showNextToastLocked()

(3)void scheduleDurationReachedLocked(ToastRecord r)

(4)handleDurationReached((ToastRecord) msg.obj)

(5 )void cancelToastLocked(int index)

四、Toast取消流程

五、原生Toast存在的问题

1.重复创建Toast

2.原生Toast的显示的死循环

3.原生Toast为系统级别的Toast


最近项目中出现一个问题,就是有的手机在关闭系统通知,结果项目中使用的原生Toast在有的手机上竟然不显示了,然后就去查系统源码,发现原来原生的Toast是基于NotificaitionManagerService实现的,难怪有些手机不显示。那些显示的手机厂商应该发现了这个问题,在系统修改了源码。特别记录下这个过程,并且附上可以解决这个问题的源码,供大家参考。

通常我们在使用Toast的时候,都是下面的简单的一行代码就可以解决问题,就可以一个Toast显示。从源码的角度来看下这个Toast是怎么一步一步显示出来的。

Toast.makeText(mContext, "原生Toast",Toast.LENGTH_SHORT).show();

Toast的类本身就仅仅是一个没有任何继承的工具类。通过一个内部类TN管理Toast的添加/移除,主要就是对WindowManager上进行添加/移除Toast的View,NotificationManagerService来控制着在什么时候将View添加到WindowManager中,什么时候将View从WindowManager移除。

public class Toast {private static class TN extends ITransientNotification.Stub {}
}

一.Toast成员变量

TN内部类用来维护一个WindowManager来对Toast上面的View进行添加和移除;

mNextView也就是Toast即将要显示的Toast的View,如果调用setView(),就会将该View赋值到mNextView中。

    //维护着一个WindowManager来添加/移除Toast的Viewfinal TN mTN;@UnsupportedAppUsage//延时时间int mDuration;//调用Toast的时候需要显示的View,初始化的时候就是默认的UI,用户也可以调用setView来重新设置这个ViewView mNextView;

二. Toast显示流程

1. Toast makeText(@NonNull Context context, @Nullable Looper looper,@NonNull CharSequence text, @Duration int duration)

主要是初始化Toast的View,默认的就是一个TextView,然后将mDuration、mNextView赋值到内部管理类TN。

    /*** Make a standard toast to display using the specified looper.* If looper is null, Looper.myLooper() is used.* @hide*/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;}

内部类TN本身继承于ITransientNotification.Stub,对Toast的View的进行显示/隐藏的管理,同时NotificationServiceManager可以跨进程访问Toast所在的进程。

  private static class TN extends ITransientNotification.Stub {}

ITransientNotification.aidl文件里面其实就是show()/hide()两个方法,而内部类TN实现了Toast的show()/hide()。

/** @hide */
oneway interface ITransientNotification {void show();void hide();
}

而这两个方法中就是就是发送SHOW和HIDE消息给到Handler。

        /*** schedule handleShow into the right thread*/@Override@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)public void show(IBinder windowToken) {if (localLOGV) Log.v(TAG, "SHOW: " + this);mHandler.obtainMessage(SHOW, windowToken).sendToTarget();}/*** schedule handleHide into the right thread*/@Overridepublic void hide() {if (localLOGV) Log.v(TAG, "HIDE: " + this);mHandler.obtainMessage(HIDE).sendToTarget();}

从TN的构造方法中可以看到,我们可以传进来一个线程的Looper对象,我们就可以在当前线程中 显示Toast。也就是说可以在线程显示Toast,但是必须传入一个当前子线程的Looper对象。

          if (looper == null) {// Use Looper.myLooper() if looper is not specified.looper = Looper.myLooper();if (looper == null) {throw new RuntimeException("Can't toast on a thread that has not called Looper.prepare()");}}

另外注意的是,在子线程中显示Toast的时候,要注意要将Toast的show()方法要在 Looper.loop()之前,否则在Toast中的Handler无法循环取队列中的消息,该Toast就无法显示。

        new Thread() {@Overridepublic void run() {super.run();Looper.prepare();Toast.makeText(ToastActivity.this,"22",Toast.LENGTH_SHORT).show();Looper.loop();}}.start();

在内部类TN中的Handler里面就是去处理不同的消息。最终这些消息的发送是在NotificationManagerService中进行维护的。

mHandler = new Handler(looper, null) {@Overridepublic void handleMessage(Message msg) {switch (msg.what) {case SHOW: {IBinder token = (IBinder) msg.obj;handleShow(token);break;}case HIDE: {handleHide();// Don't do this in handleHide() because it is also invoked by// handleShow()mNextView = null;break;}case CANCEL: {handleHide();// Don't do this in handleHide() because it is also invoked by// handleShow()mNextView = null;try {getService().cancelToast(mPackageName, TN.this);} catch (RemoteException e) {}break;}}}};}

从源码中也可以看到handleShow()/handleHide()就是将Toast中的View从WindowManager中添加/移除。在 handleShow()方法中主要提一个 if (mView != mNextView) 这个逻辑判断,这个mNextView是开发者在调用setView()之后设置的,如果开发者没有主动调用该方法,那么就直接就是在调用makeText()实例化Toast的布局View。

  public void handleShow(IBinder windowToken) {if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView+ " mNextView=" + mNextView);// If a cancel/hide is pending - no need to show - at this point// the window token is already invalid and no need to do any work.if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {return;}if (mView != mNextView) {// remove the old view if necessaryhandleHide();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);// 省略一些代码。。。。主要就是设置mWM中的一些参数//。。。。。。。。。if (mView.getParent() != null) {if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);mWM.removeView(mView);}//将mView添加到WindowManager上面try {mWM.addView(mView, mParams);trySendAccessibilityEvent();} catch (WindowManager.BadTokenException e) {/* ignore */}}}

在 handleHide()中也就是将mView移除,并置空。所以mNextView就是即将显示的View,然后用mView来保存这个View,显示出来。

 @UnsupportedAppUsagepublic 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.//将mView从WindowManager移除if (mView.getParent() != null) {if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);mWM.removeViewImmediate(mView);}// Now that we've removed the view it's safe for the server to release// the resources.try {getService().finishToken(mPackageName, this);} catch (RemoteException e) {}mView = null;}}

2.show()

第一个makeText()方法是实例化Toast对象,那么show()就是将Toast显示出来,我们从源码中看到该主要是通过NotificationManagerService来进行管理所加入的Toast队列

public void show() {if (mNextView == null) {throw new RuntimeException("setView must have been called");}INotificationManager service = getService();String pkg = mContext.getOpPackageName();TN tn = mTN;tn.mNextView = mNextView;final int displayId = mContext.getDisplayId();try {service.enqueueToast(pkg, tn, mDuration, displayId);} catch (RemoteException e) {// Empty}}

我们从源码中可以看到此时已经通过NotificationManagerService来进行显示Toast。

3.NotificationManagerService

(1)public void enqueueToast(String pkg, ITransientNotification callback, int duration,int displayId)

该方法主要的逻辑就是:首先会判断加入的这个Toast,如果之前已经加入过mToastQueue中,则直接更新该Toast的duration(这种情况出现在如果在使用的时候,先把Toast实例化,然后通过该实例在不同的地方分别调用show()的时候,那么此时传入的Toast就会走该逻辑),否则将该Toast加入到mToastQueue中。

当将Toast加入到mToastQueue的时候,也会去判断是否为系统Toast,如果不是系统Toast,那么对应的该应用下,最多同时可加入25个Toast。

最后判断mToastQueue集合中的Toast是否为第一个,只有是集合中的第一个元素的时候,才会显示该Toast。

 @Overridepublic void enqueueToast(String pkg, ITransientNotification callback, int duration,int displayId){if (pkg == null || callback == null) {Slog.e(TAG, "Not enqueuing toast. pkg=" + pkg + " callback=" + callback);return ;}//。。。。省略部分非重要逻辑。。。。//mToastQueue维护着加入到队列中的所有Toast的集合synchronized (mToastQueue) {int callingPid = Binder.getCallingPid();long callingId = Binder.clearCallingIdentity();try {// ToastRecord 是对Toast的封装、含有Toast的所在的包名、延时时间等ToastRecord record;//找到该Toast对应的索引值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.//(1)如果该Toast已经存在在队列中,则只更新Toast显示的时间if (index >= 0) {record = mToastQueue.get(index);record.update(duration);} else {//(2)说明该Toast没有被加入到队列中,后面的逻辑就是将Toast加入到Toast队列 中,并显示第一个Toast//(2.1)如果不是系统的Toast,那么每个应用下只能加入MAX_PACKAGE_NOTIFICATIONS个Toast,超过这个数量之后则不在显示if (!isSystemToast) {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;}}}}(2.2)把符合条件的将Toast封装成ToastRecord,并且加入到mToastQueue中Binder token = new Binder();mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, displayId);record = new ToastRecord(callingPid, pkg, callback, duration, token,displayId);mToastQueue.add(record);//(2.3)把当前的索引值index指向刚加入的这个Toast的位置index = mToastQueue.size() - 1;keepProcessAliveIfNeededLocked(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.//(3)如果刚加入的这个Toast恰好是该队列中的第一个,则将该Toast显示出来if (index == 0) {showNextToastLocked();}} finally {Binder.restoreCallingIdentity(callingId);}}}

那么现在就有一个问题了,那如果我当前Toast没有显示完的时候,又多次调用

Toast.makeText(mContext, "原生Toast",Toast.LENGTH_SHORT).show();

那么此时该mToastQueue集合中 已经有多个Toast,那么其他的Toast怎么显示呢?带着这个疑问,去看下面的代码。

(2)void showNextToastLocked()

从上面的方法中可以看到此时如果此时mToastQueue有一个Toast的话,就会调用该方法来显示Toast。

 void showNextToastLocked() {//(1)取出该Toast封装成的对象ToastRecordToastRecord record = mToastQueue.get(0);//这里使用的是一个无限循环,我觉得是有点浪费资源的,不知道源码在写的时候采用这种方式有什么好处,所以在自定义Toast的时候,已经将该逻辑改掉了。while (record != null) {if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);try {//(2)调用该Toast的内部类TN中的show()显示该Toastrecord.callback.show(record.token);//(3)这个就是向NotificationManagerService中维护的Handler中发送duration消息来隐藏 该ToastscheduleDurationReachedLocked(record);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 dieint index = mToastQueue.indexOf(record);if (index >= 0) {mToastQueue.remove(index);}keepProcessAliveIfNeededLocked(record.pid);if (mToastQueue.size() > 0) {record = mToastQueue.get(0);} else {record = null;}}}}

(3)void scheduleDurationReachedLocked(ToastRecord r)

该方法的代码逻辑很简单,就是根据Toast.LENGTH_LONG还是Toast.LENGTH_SHORT来得到对应的延时时间发送到Handler对象中来隐藏Toast

    private void scheduleDurationReachedLocked(ToastRecord r){mHandler.removeCallbacksAndMessages(r);Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);//(1)根据Toast.LENGTH_LONG还是Toast.LENGTH_SHORT来获得对应的延时时间int delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;// Accessibility users may need longer timeout duration. This api compares original delay// with user's preference and return longer one. It returns original delay if there's no// preference.delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay,AccessibilityManager.FLAG_CONTENT_TEXT);//(2)发送延时消息来来隐藏ToastmHandler.sendMessageDelayed(m, delay);}

那么就看下Handler中对应的内容

(4)handleDurationReached((ToastRecord) msg.obj)

这里的逻辑就是找到对应的Toast的索引值,然后调用 cancelToastLocked(index)方法将该Toast隐藏。

    private void handleDurationReached(ToastRecord record){if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);synchronized (mToastQueue) {int index = indexOfToastLocked(record.pkg, record.callback);if (index >= 0) {cancelToastLocked(index);}}}

(5 )void cancelToastLocked(int index)

主要就是找到对应的ToastRecord,回调到Toast内部类中的hide()方法来隐藏Toast。然后从mToastQueue队列中将Toast移除,将该Toast对应的消息从Handler中移除,最后判断下mToastQueue集合中是否有未显示的Toast,如果还有,则重复第2个方法,依次将集合中的第0个Toast显示完。这里也就回答了在第一个方法enqueueToast()中的疑问,完成mToastQueue集合中的所有的Toast依次显示完的逻辑。

 void cancelToastLocked(int index) {ToastRecord record = mToastQueue.get(index);//(1)找到对应的ToastRecord,然后调用Toast中的hide()try {record.callback.hide();} catch (RemoteException e) {Slog.w(TAG, "Object died trying to hide notification " + record.callback+ " in package " + record.pkg);// don't worry about this, we're about to remove it from// the list anyway}//(2)将该Toast从队列中移除ToastRecord lastToast = mToastQueue.remove(index);mWindowManagerInternal.removeWindowToken(lastToast.token, false /* removeWindows */,lastToast.displayId);// We passed 'false' for 'removeWindows' so that the client has time to stop// rendering (as hide above is a one-way message), otherwise we could crash// a client which was actively using a surface made from the token. However// we need to schedule a timeout to make sure the token is eventually killed// one way or another.//(3)将该Toast对应的消息从Handler中移除scheduleKillTokenTimeout(lastToast);keepProcessAliveIfNeededLocked(record.pid);//(4)如果集合中仍有Toast还没有显示完,那么就在重复第2个方法进行依次显示集合中的第0个元素if (mToastQueue.size() > 0) {// Show the next one. If the callback fails, this will remove// it from the list, so don't assume that the list hasn't changed// after this point.showNextToastLocked();}}

四、Toast取消流程

如果我们在使用Toast的时候,需要在未显示完的时候就取消该Toast,这个时候就需要调用到cancel(),其实最终还是调用到内部管理类TN的cancel()方法

    /*** Close the view if it's showing, or don't show it if it isn't showing yet.* You do not normally have to call this.  Normally view will disappear on its own* after the appropriate duration.*/public void cancel() {mTN.cancel();}

TN中的cancel()方法最终就是发送一个CANCEL消息到Handler,具体还是到NotificationManagerService中的canelToast()。

                    handleHide();// Don't do this in handleHide() because it is also invoked by// handleShow()mNextView = null;try {getService().cancelToast(mPackageName, TN.this);} catch (RemoteException e) {}

从NotificationManagerService中可以看到和前面的Toast在延时之后隐藏Toast的逻辑是一致的,只不过前面的是在延时时间到了之后,去调用 cancelToastLocked(index)方法,而这里是调用cancel()方法的时候,即时就调用cancelToastLocked(index)方法。

  @Overridepublic void cancelToast(String pkg, ITransientNotification callback) {Slog.i(TAG, "cancelToast pkg=" + pkg + " callback=" + callback);if (pkg == null || callback == null) {Slog.e(TAG, "Not cancelling notification. pkg=" + pkg + " callback=" + callback);return ;}synchronized (mToastQueue) {long callingId = Binder.clearCallingIdentity();try {int index = indexOfToastLocked(pkg, callback);if (index >= 0) {cancelToastLocked(index);} else {Slog.w(TAG, "Toast already cancelled. pkg=" + pkg+ " callback=" + callback);}} finally {Binder.restoreCallingIdentity(callingId);}}}

五、原生Toast存在的问题

1.重复创建Toast

通常我们在用Toast的时候,都会直接调用下面一行代码来显示Toast,从我们在第二部分分析的显示流程中我们可以看到:

Toast.makeText(mContext, "原生Toast",Toast.LENGTH_SHORT).show();

每调用一次这行代码,都会实例化一个Toast,然后加入到NotificationServiceManager的mToastQueue队列中。如果恰好在点击按钮时调用这行代码,很容易会多次调用这行代码,引起重复创建Toast。有时候项目中为了避免重复创建Toast,所以通常会创建一个Toast实例,全局调用这一个Toast实例,例如:

private static Toast toast;
public static void showToast(Context context, String content) {if (toast == null) {toast = Toast.makeText(context, content, Toast.LENGTH_SHORT);} else {toast.setText(content);}toast.show();
}

上面的这行代码其实是会有内存泄漏的问题。例如当这个Toast在显示的时候,会持有Activity对象,当还未消失的时候,关闭了该Activity,就会导致Activity对象无法回收,引起Activity的内存泄漏。所以针对这个问题,已经在自定义的Toast中进行了改进。从源码中可以看到,每次显示的时候,其实都是取了mToastQueue中的第0个元素来显示,直到显示完才将该元素从集合中删除,那么我们完全可以在加入Toast之前,先去判断下该Toast的显示的文字内容与当前的Toast的文字内容是否一致,如果一致的话,可以先不加入到mToastQueue队列中。在自定义的代码(代码路径链接  GitHub 地址为https://github.com/wenjing-bonnie/toast.git)中已针对这点做出了优化。具体在PowerfulToastManagerService中:

    protected void enqueueToast(PowerfulToast toast, String content, ITransientPowerfulToast callBack, int duration) {//。。。。。。省略其他代码//如果与正在显示的Toast的内容一致,则不将该Toast加入到Toast队列中;//(1)恰好该workHandler的延时MESSAGE_DURATION_REACHED到了在执行remove操作的时候,此时为null,会向下加入这个Toast//(2)只要这个Toast没有显示完,则取出来的值不为空,则不会加入到显示mToastQueue队列中if (mToastQueue != null && !mToastQueue.isEmpty()) {PowerfulToastRecord curRecord = mToastQueue.get(0);if (curRecord != null && content.equals(curRecord.content)) {return;}}//将新增的toast加入到队列中record = new PowerfulToastRecord(toast, content, callBack, duration);mToastQueue.add(record);//。。。。。。省略其他代码
}

2.原生Toast的显示的死循环

在原生的Toast显示的时候,这里取出mToastQueue的第0个元素,然后显示出来到最后消失的时候,这个循环一直在执行,一直到Toast显示完的时候,这个循环一直存在,其实为什么这里不直接使用一个if(record!=null)来进行判断就可以了呢?这个源码之所以采用这种方式有什么好处,暂时没有想到原因。所以在自定义Toast的时候,已经将该逻辑改了,直接使用的就是if(record!=null)来判断。

    void showNextToastLocked() {ToastRecord record = mToastQueue.get(0);//这块为什么会要用一个死循环的方式呢?while (record != null) {}}

3.原生Toast为系统级别的Toast

原生Toast在显示的时候,设置的WindowManager.LayoutParams的时候,采用的是下面的这种类型,但是对于自定义的Toast的时候,

   params.type = WindowManager.LayoutParams.TYPE_TOAST;

WindowManager的类型分为应用Window、子Window、系统Window。应用Window对应的一个Activity,子Window不能单独存在,必须附属到父Window中,而系统Window在使用的时候,必须声明权限。

所以我们在自定义的Toast的不能采用这种类型,因为通知权限在关闭后设置显示的类型为TYPE_TOAST会抛android.view.WindowManager$BadTokenException这个异常。而系统Window的类型,在使用的时候,会提示用户给到相应的权限,这样在用户体验很差,所以只能采用应用Window,那么使用应用Window类型的时候,就会有另外一个问题,如果在Toast没有消失的时候,关闭Activity的时候,会抛出 android.view.WindowLeaked: Activity。所以为了避免这种情况,所以监听Activity的生命周期,在Activity关闭的时候,取消所有mToastQueue中的Toast。所以需要在使用自定义Toast的时候,需要先注册该Toast。

public class PowerfulToastManagerService implements Application.ActivityLifecycleCallbacks {   /*** 将application传入用来管理Activity的生命周期** @param application*/protected void registerToast(Application application) {application.registerActivityLifecycleCallbacks(this);}/*** {@link android.app.Application.ActivityLifecycleCallbacks}*/@Overridepublic void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {}@Overridepublic void onActivityStarted(@NonNull Activity activity) {}@Overridepublic void onActivityResumed(@NonNull Activity activity) {}@Overridepublic void onActivityPaused(@NonNull Activity activity) {Log.logV(TAG, activity.getClass().getSimpleName() + " , is paused ! " + " , size is " + mToastQueue.size());cancelAllPowerfulToast();}@Overridepublic void onActivityStopped(@NonNull Activity activity) {}@Overridepublic void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}@Overridepublic void onActivityDestroyed(@NonNull Activity activity) {}
}

小小的Toast蕴含大道理(解决关闭通知时原生Toast不显示问题)相关推荐

  1. 解决导入markdown时本地图片无法显示问题——图床

    解决导入markdown时本地图片无法显示问题--图床 图床工具: PicGo 图片服务器:Gitee 辅助工具:Node.js 14.17.6 图床: 一般是指储存图片的服务器,有国内和国外之分.国 ...

  2. 解决关闭hbase时stop-hbase.sh报错stopping hbasecat: /tmp/hbase-xxxx-master.pid: No such file or directory

    完整报错是:stopping hbasecat: /tmp/hbase-xxxx-master.pid: No such file or directory 解决方法 sudo mkdir -p /v ...

  3. 【收藏】解决关闭Hadoop时no namenode to stop异常

    https://blog.csdn.net/GYQJN/article/details/50805472 HADOOP_PID_DIR 变量保存的是pid文件的存储路径.默认存储在/tmp目录中,代码 ...

  4. 解决浏览器打印时,背景色不显示

    只需要在设置背景色的div下面加上以下代码就可以解决 -webkit-print-color-adjust:exact; -moz-print-color-adjust:exact; -ms-prin ...

  5. android toast通知关闭,屏蔽系统通知,Toast无法显示的解决方案 v2.0.0

    为了大家方便沟通和使用,建立了一个QQ群供大家交流,欢迎大家的加入 群名称:EToast交流群 群 号:547279762 更新日志: v2.2.1(2019年5月28日10:24:41) 在2.2. ...

  6. Toast系列(五):还在被关闭通知无法显示Toast所困扰?解决方案来了

    开源库地址:https://github.com/the-pig-of-jungle/smart-show Toast工作原理依赖于通知,关闭应用通知权限后,Toast无法显示.在发布SmartSho ...

  7. CSocket,CAsyncSocket多线程退出时的一些注意事项(解决关闭WinSoket崩溃的问题)

    CSocket,CAsyncSocket多线程退出时的一些注意事项(解决关闭WinSoket崩溃的问题) 参考文章: (1)CSocket,CAsyncSocket多线程退出时的一些注意事项(解决关闭 ...

  8. android关闭本应用的通知声音代码,关于android:当我的应用取消通知时,播放带有FLAG_INSISTENT通知的声音不会停止...

    我有一个具有提醒功能的应用程序.当需要提醒用户某些内容时,我的应用程序会创建一条通知,可能使用FLAG_INSISTENT来确保警报被听到.用户与我的应用程序交互以确认警报后,该应用程序将取消通知. ...

  9. 解决 ”基础连接已经关闭: 发送时发生错误”

    今天上线有一段时间的项目突然报错了,在调用api的时候发生错误:"基础连接已经关闭: 发送时发生错误",上谷歌找了一下原因,分析:因为请求的url是基于https的,所以Post请 ...

最新文章

  1. 【AT2434】JOI 公園 (JOI Park) 最短路+贪心
  2. 排列出所有子串暴力递归
  3. CSS两栏布局之左栏布局
  4. Java变异出现错误:No enclosing instance of type XXX is accessible
  5. 学习:Ubuntu14.04编译caffe问题记录
  6. java解压和压缩cab包 附jar
  7. 多线程- 让程序更高效的运行
  8. c语言结构体平面向量加法公式,高中平面向量学不好怎么办?这些公式帮你秒杀向量题目...
  9. 关于多个ul标签并列时下对齐的问题
  10. 微型计算机原理计算机钢琴,用汇编语言编写计算机钢琴程序.PDF
  11. 高等学校计算机规划教程,操作系统教程(21世纪高等学校计算机规划教材)
  12. jQuery - 基于当前元素的遍历
  13. 软件工程研究领域最顶级的两个期刊
  14. 运维工程师必备Linux常见安全策略与实践
  15. 全国计算机考试cad,国家CAD考试CAD试题库.pdf
  16. java clob类型怎么插入数据库_在Java + Oracle环境下 对于clob类型的数据的插入
  17. 用数据跑路代替群众跑腿 “异地办” 提升幸福感
  18. pig对null的处理(实际,对空文本处理为两种取值null或‘’)
  19. (阿里云笔记)贝勾置阿里云轻量应用服务器CentOS7.6镜像——Linux系统
  20. 台式电脑怎么更换计算机明,电脑上面的cpu能换吗_cpu怎么更换(台式机、笔记本)...

热门文章

  1. 单片机(中断系统-串口通信)
  2. python少儿编程讲师笔试题_小码王教育儿童编程教师面试:做笔试题(填空题和编程题,填空题 - 职朋职业圈...
  3. 杭电数字电路课程设计-实验十-JK触发器设计实验
  4. JavaWeb企业实战项目(一):环境搭建-用户注册-邮件发送
  5. 分享一些数据分析师免费的课程
  6. converting to execution character set: Illegal byte sequence
  7. 用python画路飞代码_python 全栈开发,Day105(路飞其他数据库表结构,立即结算需求)...
  8. [笔记]NFC笔记——WUP_REQ 和 WUP_RES 消息结构
  9. 复旦大学计算机网络专业,复旦大学计算机网络专业计划.doc
  10. Python计算等额本息贷款和等额本金贷款