关于Notification的一点愚见(PendingIntent工作流程)
来源:互联网 发布:搞笑的网络短剧2016 编辑:程序博客网 时间:2024/06/05 16:59
PendingIntent,顾名思义,延迟的Intent。那么肯定会产生一个疑问,那就是PendingIntent究竟和Intent的区别在哪里呢?
其实PendingIntent其实就是对Intent的一个封装,不过Intent是立即触发效果,就像我们常用的这样:
Intent intent = new Intent(MainActivity.this,DemoActivity1.class); startActivity(intent);
而PendingIntent触发的时间点不太一样,一般是触发效果的地点不在函数中,而是在其他方法处被调用,所以看起来就像被延迟了一样。
PendingIntent的声明
我们可以知道PendingIntent可以支持三种意图:启动Activity,启动Service,启动Broadcast。我们取出其中一个场景来讨论,其他两个场景是类似的。
上一章中,有如下这段:
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
这一段就是PendingIntent的声明,调用PendingIntent中的getActivity()的方法返回实例。
下面是getActivity的源码:
public static PendingIntent getActivity(Context context, int requestCode, Intent intent, @Flags int flags) { return getActivity(context, requestCode, intent, flags, null); }
这里又调用了:
public static PendingIntent getActivity(Context context, int requestCode, @NonNull Intent intent, @Flags int flags, @Nullable Bundle options) { String packageName = context.getPackageName(); String resolvedType = intent != null ? intent.resolveTypeIfNeeded( context.getContentResolver()) : null; try { intent.migrateExtraStreamToClipData(); intent.prepareToLeaveProcess(); IIntentSender target = ActivityManagerNative.getDefault().getIntentSender( ActivityManager.INTENT_SENDER_ACTIVITY, packageName, null, null, requestCode, new Intent[] { intent }, resolvedType != null ? new String[] { resolvedType } : null, flags, options, UserHandle.myUserId()); return target != null ? new PendingIntent(target) : null; } catch (RemoteException e) { } return null; }
其中核心代码:
IIntentSender target = ActivityManagerNative.getDefault().getIntentSender( ActivityManager.INTENT_SENDER_ACTIVITY, packageName, null, null, requestCode, new Intent[] { intent }, resolvedType != null ? new String[] { resolvedType } : null, flags, options, UserHandle.myUserId()); return target != null ? new PendingIntent(target) : null;
而这里ActivityManagerNative.getDefault()是指的是AMS(ActivityManagerService)这个类,这个类可以说是贯穿了Android整个应用层的结构。它在Android系统中,四大组件的行为和它密切相关。
而AMS是属于原进程之外,那么我们就必须完成进程间通讯的动作。而同时Activity是继承了Binder实现了IActivityManager的接口。之前的讲述Binder的时候也说过了,在Android的进程间通讯,Binder起到了至关重要的作用。
于此同时,也要说明一下IIntentSender这个类其实是继承了IInterface抽象类的接口。
public interface IIntentSender extends android.os.IInterface
AMS中PendingIntent的工作
我们下去AMS的getIntentSender()看看这个函数的究竟。
@Override public IIntentSender getIntentSender(int type, String packageName, IBinder token, String resultWho, int requestCode, Intent[] intents, String[] resolvedTypes, int flags, Bundle options, int userId) {....//获取Intent对象,存入数组并且对非法的intent做出处理 synchronized(this) { int callingUid = Binder.getCallingUid(); int origUserId = userId; userId = handleIncomingUser(Binder.getCallingPid(), callingUid, userId, type == ActivityManager.INTENT_SENDER_BROADCAST, ALLOW_NON_FULL, "getIntentSender", null); if (origUserId == UserHandle.USER_CURRENT) { // We don't want to evaluate this until the pending intent is // actually executed. However, we do want to always do the // security checking for it above. userId = UserHandle.USER_CURRENT; } try { if (callingUid != 0 && callingUid != Process.SYSTEM_UID) { int uid = AppGlobals.getPackageManager() .getPackageUid(packageName, UserHandle.getUserId(callingUid)); if (!UserHandle.isSameApp(callingUid, uid)) { String msg = "Permission Denial: getIntentSender() from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() + ", (need uid=" + uid + ")" + " is not allowed to send as package " + packageName; Slog.w(TAG, msg); throw new SecurityException(msg); } } return getIntentSenderLocked(type, packageName, callingUid, userId, token, resultWho, requestCode, intents, resolvedTypes, flags, options); } catch (RemoteException e) { throw new SecurityException(e); } } }
我们主要看这一段线程同步的代码,最后你会发现获取完uid之类的信息之后就会回调函数getIntentSenderLocked(type, packageName, callingUid, userId,
token, resultWho, requestCode, intents, resolvedTypes, flags, options);这个才是真正处理问题的函数。
IIntentSender getIntentSenderLocked(int type, String packageName, int callingUid, int userId, IBinder token, String resultWho, int requestCode, Intent[] intents, String[] resolvedTypes, int flags, Bundle options) { if (DEBUG_MU) Slog.v(TAG_MU, "getIntentSenderLocked(): uid=" + callingUid); ActivityRecord activity = null; if (type == ActivityManager.INTENT_SENDER_ACTIVITY_RESULT) { activity = ActivityRecord.isInStackLocked(token); if (activity == null) { return null; } if (activity.finishing) { return null; } } final boolean noCreate = (flags&PendingIntent.FLAG_NO_CREATE) != 0; final boolean cancelCurrent = (flags&PendingIntent.FLAG_CANCEL_CURRENT) != 0; final boolean updateCurrent = (flags&PendingIntent.FLAG_UPDATE_CURRENT) != 0; flags &= ~(PendingIntent.FLAG_NO_CREATE|PendingIntent.FLAG_CANCEL_CURRENT |PendingIntent.FLAG_UPDATE_CURRENT); PendingIntentRecord.Key key = new PendingIntentRecord.Key( type, packageName, activity, resultWho, requestCode, intents, resolvedTypes, flags, options, userId); WeakReference<PendingIntentRecord> ref; ref = mIntentSenderRecords.get(key); PendingIntentRecord rec = ref != null ? ref.get() : null; if (rec != null) { if (!cancelCurrent) { if (updateCurrent) { if (rec.key.requestIntent != null) { rec.key.requestIntent.replaceExtras(intents != null ? intents[intents.length - 1] : null); } if (intents != null) { intents[intents.length-1] = rec.key.requestIntent; rec.key.allIntents = intents; rec.key.allResolvedTypes = resolvedTypes; } else { rec.key.allIntents = null; rec.key.allResolvedTypes = null; } } return rec; } rec.canceled = true; mIntentSenderRecords.remove(key); } if (noCreate) { return rec; } rec = new PendingIntentRecord(this, key, callingUid); mIntentSenderRecords.put(key, rec.ref); if (type == ActivityManager.INTENT_SENDER_ACTIVITY_RESULT) { if (activity.pendingResults == null) { activity.pendingResults = new HashSet<WeakReference<PendingIntentRecord>>(); } activity.pendingResults.add(rec.ref); } return rec; }
这一段做的工作有一下几点:
1.假若flags是type == ActivityManager.INTENT_SENDER_ACTIVITY_RESULT。则说明是从Activity应答的PendingIntent,我们不做讨论。
2.假如PendingIntentRecord不为空,根据PendingIntent的标志位,Intent数组的做出调整来达到PendingIntent的效果。
这里先说一下PendingIntent,有四个标志位:
1)FLAG_ONE_SHOT :当前描述的PendingIntent只能被使用一次,然后它就会调用cancel删除。如果还有后续相同的PendingIntent,那么他们的send方法将会调用失败。
对于通知栏而言,采用此标志位,同类通知只能使用一次,后续点击通知会无效。
2)FLAG_NO_CREATE:当前描述的PendingIntent不会主动创建,如果当前PendingIntent之前不存在,则所有的getActivity,getService,getBroadcast返回null,获取PendingIntent失败。这个很少用,暂时不讨论。
3)FLAG_CANCEL_CURRENT:当前描述的PendingIntent已经存在了,那么他们会被cancel,然后系统创建一个新的PendingIntent。对于通知栏而言,被cancel后的消息单击无效。
4)FLAG_UPDATE_CURRENT:如果当前的PendingIntent已经存在,则它们都会被更新,即里面的PendingIntent都换为最新。
3.假如PendingIntentRecord为空则创建一个新的PendingIntentRecord。并且存入Activity中PendingIntentRecord的HashSet中。最后返回PendingIntentRecord。
这个时候,我们发现返回了PendingIntentRecord,说明这个类正是实现了IIntentSender。让我们看看这个类是怎么回事吧。
final class PendingIntentRecord extends IIntentSender.Stub
看到了吧,这个类正是实现了IIntentSender.Stub的类。
final class PendingIntentRecord extends IIntentSender.Stub
是不是觉得和眼熟呢?没错就是我们之前分析的AIDL,在这个过程AMS相当于中转器,就像设计模式中的中介者一样。将函数转移到远程端的实现。这种思想在探索四大组件的时候经常会用到。
PendingIntentRecord中工作
在这个类中完成的事情只有一件,那就是将数据存储进来。
PendingIntentRecord(ActivityManagerService _owner, Key _k, int _u) { owner = _owner; key = _k; uid = _u; ref = new WeakReference<PendingIntentRecord>(this); }//内部类key的定义 Key(int _t, String _p, ActivityRecord _a, String _w, int _r, Intent[] _i, String[] _it, int _f, Bundle _o, int _userId) { type = _t; packageName = _p; activity = _a; who = _w; requestCode = _r; requestIntent = _i != null ? _i[_i.length-1] : null; requestResolvedType = _it != null ? _it[_it.length-1] : null; allIntents = _i; allResolvedTypes = _it; flags = _f; options = _o; userId = _userId; int hash = 23; hash = (ODD_PRIME_NUMBER*hash) + _f; hash = (ODD_PRIME_NUMBER*hash) + _r; hash = (ODD_PRIME_NUMBER*hash) + _userId; if (_w != null) { hash = (ODD_PRIME_NUMBER*hash) + _w.hashCode(); } if (_a != null) { hash = (ODD_PRIME_NUMBER*hash) + _a.hashCode(); } if (requestIntent != null) { hash = (ODD_PRIME_NUMBER*hash) + requestIntent.filterHashCode(); } if (requestResolvedType != null) { hash = (ODD_PRIME_NUMBER*hash) + requestResolvedType.hashCode(); } hash = (ODD_PRIME_NUMBER*hash) + _p.hashCode(); hash = (ODD_PRIME_NUMBER*hash) + _t; hashCode = hash; //Slog.i(ActivityManagerService.TAG, this + " hashCode=0x" // + Integer.toHexString(hashCode)); }
在这一段代码中,很好的解释了为什么我们在声明PendingIntent的时候,请求码一般为0,在上面hashcode的数值和请求码requestCode进行相加的运算获得新的请求码。假如请求码变动了,hashcode也变动了,那么在AMS中建立的HashSet的检索策略不同了就会造成寻找到的Intent不同,对错误的Intent处理了错误标志位。
到这里,PendingIntent已经存储,完毕正在等待用户的调度。
NotificationManager的调用
我们这里分析Notification中PendingIntent的情况,其实在AppWidget中调用PendingIntent的情景也是十分类似。
我们在制作通知的时候,最后会声明一个NotificationManager的notify去唤醒PendingIntent并且执行。
让我们去NotificationManager中看看,notify方法吧。
我们可以知道, public void notify(int id, Notification notification)又会调用 public void notify(String tag, int id, Notification notification)
public void notify(String tag, int id, Notification notification) { int[] idOut = new int[1]; INotificationManager service = getService(); String pkg = mContext.getPackageName(); if (notification.sound != null) { notification.sound = notification.sound.getCanonicalUri(); if (StrictMode.vmFileUriExposureEnabled()) { notification.sound.checkFileUriExposed("Notification.sound"); } } if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")"); Notification stripped = notification.clone(); Builder.stripForDelivery(stripped); try { service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id, stripped, idOut, UserHandle.myUserId()); if (id != idOut[0]) { Log.w(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]); } } catch (RemoteException e) { } }
在这一段中,我们只用注意这个函数service.enqueueNotificationWithTag(pkg, mContext.getOpPack-ageName(), tag, id,stripped, idOut, UserHandle.myUserId());
这个函数也是进程间的通讯,透过INotificationManager这个AIDL实现的类,远程调用NotificationManagerService中方法。这个过程也是AIDL实现的Binder弯完成的。
NotificationManagerService中的工作流程
在这里,声明了一个IBinder来实现Stub:
private final IBinder mService = new INotificationManager.Stub()
而函数enqueueNotificationWithTag又会调用:
void enqueueNotificationInternal(final String pkg, final String opPkg, final int callingUid, final int callingPid, final String tag, final int id, final Notification notification, int[] idOut, int incomingUserId) { ... //从NotificationList中获取通知存入NotificationRecord,以及对Notification中 //的一些非法额度参数进行异常处理 mHandler.post(new Runnable() { @Override public void run() { synchronized (mNotificationList) { // === Scoring === // 0. Sanitize inputs notification.priority = clamp(notification.priority, Notification.PRIORITY_MIN, Notification.PRIORITY_MAX); // Migrate notification flags to scores if (0 != (notification.flags & Notification.FLAG_HIGH_PRIORITY)) { if (notification.priority < Notification.PRIORITY_MAX) { notification.priority = Notification.PRIORITY_MAX; } } else if (SCORE_ONGOING_HIGHER && 0 != (notification.flags & Notification.FLAG_ONGOING_EVENT)) { if (notification.priority < Notification.PRIORITY_HIGH) { notification.priority = Notification.PRIORITY_HIGH; } } // 1. initial score: buckets of 10, around the app [-20..20] final int score = notification.priority * NOTIFICATION_PRIORITY_MULTIPLIER; // 2. extract ranking signals from the notification data final StatusBarNotification n = new StatusBarNotification( pkg, opPkg, id, tag, callingUid, callingPid, score, notification, user); NotificationRecord r = new NotificationRecord(n, score); NotificationRecord old = mNotificationsByKey.get(n.getKey()); if (old != null) { // Retain ranking information from previous record r.copyRankingInformation(old); } mRankingHelper.extractSignals(r); // 3. Apply local rules // blocked apps if (ENABLE_BLOCKED_NOTIFICATIONS && !noteNotificationOp(pkg, callingUid)) { if (!isSystemNotification) { r.score = JUNK_SCORE; Slog.e(TAG, "Suppressing notification from package " + pkg + " by user request."); } } if (r.score < SCORE_DISPLAY_THRESHOLD) { // Notification will be blocked because the score is too low. return; } // Clear out group children of the old notification if the update causes the // group summary to go away. This happens when the old notification was a // summary and the new one isn't, or when the old notification was a summary // and its group key changed. if (old != null && old.getNotification().isGroupSummary() && (!notification.isGroupSummary() || !old.getGroupKey().equals(r.getGroupKey()))) { cancelGroupChildrenLocked(old, callingUid, callingPid, null); } int index = indexOfNotificationLocked(n.getKey()); if (index < 0) { mNotificationList.add(r); mUsageStats.registerPostedByApp(r); } else { old = mNotificationList.get(index); mNotificationList.set(index, r); mUsageStats.registerUpdatedByApp(r, old); // Make sure we don't lose the foreground service state. notification.flags |= old.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE; r.isUpdate = true; } mNotificationsByKey.put(n.getKey(), r); // Ensure if this is a foreground service that the proper additional // flags are set. if ((notification.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) { notification.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR; } applyZenModeLocked(r); mRankingHelper.sort(mNotificationList); if (notification.icon != 0) { StatusBarNotification oldSbn = (old != null) ? old.sbn : null; mListeners.notifyPostedLocked(n, oldSbn); } else { Slog.e(TAG, "Not posting notification with icon==0: " + notification); if (old != null && !old.isCanceled) { mListeners.notifyRemovedLocked(n); } // ATTENTION: in a future release we will bail out here // so that we do not play sounds, show lights, etc. for invalid // notifications Slog.e(TAG, "WARNING: In a future release this will crash the app: " + n.getPackageName()); } buzzBeepBlinkLocked(r); } } }); idOut[0] = id;
我们主要看Handler.post方法中的Runnable方法。
这么最主要的工作就是这个函数:
if (old != null && old.getNotification().isGroupSummary() && (!notification.isGroupSummary() || !old.getGroupKey().equals(r.getGroupKey()))) { cancelGroupChildrenLocked(old, callingUid, callingPid, null); }
这一段的意思就是清掉前一个通知,如果更新而导致数组的大小出现了变化的话。这个发生在过去的通知是个所有的通知的集合,或者当过去的通知是一个集合但是他的key发生了改变。
而函数cancelGroupChildrenLocked中做了一个动作那就是去除尾部的通知同时执行尾部的通知,接下来又调用了cancelNotificationLocked(childR, false, REASON_GROUP_SUMMARY_CANCELED);
private void cancelNotificationLocked(NotificationRecord r, boolean sendDelete, int reason) { if (sendDelete) { if (r.getNotification().deleteIntent != null) { try { r.getNotification().deleteIntent.send(); } catch (PendingIntent.CanceledException ex) { // do nothing - there's no relevant way to recover, and // no reason to let this propagate Slog.w(TAG, "canceled PendingIntent for " + r.sbn.getPackageName(), ex); } } } .... }
我们只需要注重上面那一段通过Notification的deleteIntent(上面在显示Notification的时候已经显示了deleteIntent是PendingIntent)的send方法,执行了PendingIntent。
PendingIntent的send
我们接着看PendingIntent的send的方法,在这里send被重载了好几种方法,最终会到这个send执行:
public void send(Context context, int code, Intent intent, OnFinished onFinished, Handler handler, String requiredPermission) throws CanceledException { try { String resolvedType = intent != null ? intent.resolveTypeIfNeeded(context.getContentResolver()) : null; int res = mTarget.send(code, intent, resolvedType, onFinished != null ? new FinishedDispatcher(this, onFinished, handler) : null, requiredPermission); if (res < 0) { throw new CanceledException(); } } catch (RemoteException e) { throw new CanceledException(e); } }
最终会由mTarget.send调用,而mTarget则是IInterSender,这时又会回到PendingIntentRecord。这个时候就可以调用之前存进来的PendingIntent了。
最后由函数sendInner完成:
int sendInner(int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, IBinder resultTo, String resultWho, int requestCode, int flagsMask, int flagsValues, Bundle options, IActivityContainer container) { synchronized(owner) { if (!canceled) { sent = true; if ((key.flags&PendingIntent.FLAG_ONE_SHOT) != 0) { owner.cancelIntentSenderLocked(this, true); canceled = true; } Intent finalIntent = key.requestIntent != null ? new Intent(key.requestIntent) : new Intent(); if (intent != null) { int changes = finalIntent.fillIn(intent, key.flags); if ((changes&Intent.FILL_IN_DATA) == 0) { resolvedType = key.requestResolvedType; } } else { resolvedType = key.requestResolvedType; } flagsMask &= ~Intent.IMMUTABLE_FLAGS; flagsValues &= flagsMask; finalIntent.setFlags((finalIntent.getFlags()&~flagsMask) | flagsValues); final long origId = Binder.clearCallingIdentity(); boolean sendFinish = finishedReceiver != null; int userId = key.userId; if (userId == UserHandle.USER_CURRENT) { userId = owner.getCurrentUserIdLocked(); } switch (key.type) { case ActivityManager.INTENT_SENDER_ACTIVITY: if (options == null) { options = key.options; } else if (key.options != null) { Bundle opts = new Bundle(key.options); opts.putAll(options); options = opts; } try { if (key.allIntents != null && key.allIntents.length > 1) { Intent[] allIntents = new Intent[key.allIntents.length]; String[] allResolvedTypes = new String[key.allIntents.length]; System.arraycopy(key.allIntents, 0, allIntents, 0, key.allIntents.length); if (key.allResolvedTypes != null) { System.arraycopy(key.allResolvedTypes, 0, allResolvedTypes, 0, key.allResolvedTypes.length); } allIntents[allIntents.length-1] = finalIntent; allResolvedTypes[allResolvedTypes.length-1] = resolvedType; owner.startActivitiesInPackage(uid, key.packageName, allIntents, allResolvedTypes, resultTo, options, userId); } else { owner.startActivityInPackage(uid, key.packageName, finalIntent, resolvedType, resultTo, resultWho, requestCode, 0, options, userId, container, null); } } catch (RuntimeException e) { Slog.w(ActivityManagerService.TAG, "Unable to send startActivity intent", e); } break; case ActivityManager.INTENT_SENDER_ACTIVITY_RESULT: key.activity.task.stack.sendActivityResultLocked(-1, key.activity, key.who, key.requestCode, code, finalIntent); break; case ActivityManager.INTENT_SENDER_BROADCAST: try { // If a completion callback has been requested, require // that the broadcast be delivered synchronously owner.broadcastIntentInPackage(key.packageName, uid, finalIntent, resolvedType, finishedReceiver, code, null, null, requiredPermission, (finishedReceiver != null), false, userId); sendFinish = false; } catch (RuntimeException e) { Slog.w(ActivityManagerService.TAG, "Unable to send startActivity intent", e); } break; case ActivityManager.INTENT_SENDER_SERVICE: try { owner.startServiceInPackage(uid, finalIntent, resolvedType, userId); } catch (RuntimeException e) { Slog.w(ActivityManagerService.TAG, "Unable to send startService intent", e); } break; } if (sendFinish) { try { finishedReceiver.performReceive(new Intent(finalIntent), 0, null, null, false, false, key.userId); } catch (RemoteException e) { } } Binder.restoreCallingIdentity(origId); return 0; } } return ActivityManager.START_CANCELED; }
其实写的这么长,我们最终只有这么一句有用:
owner.startActivityInPackage(uid, key.packageName, finalIntent, resolvedType, resultTo, resultWho, requestCode, 0, options, userId, container, null);
这里的owner又是指AMS,我们回到AMS去看看,又是调用了什么:
final int startActivityInPackage(int uid, String callingPackage, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, Bundle options, int userId, IActivityContainer container, TaskRecord inTask) { userId = handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId, false, ALLOW_FULL_ONLY, "startActivityInPackage", null); // TODO: Switch to user app stacks here. int ret = mStackSupervisor.startActivityMayWait(null, uid, callingPackage, intent, resolvedType, null, null, resultTo, resultWho, requestCode, startFlags, null, null, null, options, userId, container, inTask); return ret; }
看到 mStackSupervisor.startActivityMayWait(null, uid, callingPackage, intent,resolvedType, null, null, resultTo, resultWho, requestCode, startFlags,null, null, null, options, userId, container, inTask);
这个函数,我,恍然大悟,这不就是Activity启动流程中的一部分吗,通过Activity监听器来执行在Activity在任务栈中启动过程。
至此PendingIntent是如何启动四大组件的Activity就分析完毕了。
流程图
感谢任玉刚大神的android开发探索艺术,以及下面这位大神
PendingIntent的工作原理
- 关于Notification的一点愚见(PendingIntent工作流程)
- 关于RemoteView的一点愚见(RemoteView在AppWidget中的工作流程)
- 关于activity启动流程的一点愚见
- 关于Notification通知的一点愚见(自定义通知栏)
- 关于Window创建流程的一点愚见(Activity启动过程中)
- 关于AsyncTask的一点愚见
- 关于IPC的一点愚见
- 关于Activity的一点愚见
- 关于RemoteView的一点愚见(实现桌面小部件)
- 关于数模中编程的一点愚见
- 关于数模中编程的一点愚见
- 关于Android动画的一点愚见
- 关于滚动图片停在浏览器边界上的一点解决办法(个人愚见)。。
- 关于Binder在Java上工作的一点愚见(一)
- 关于Binder在Java上工作的一点愚见(二)
- 关于Window和WindowManager的一点愚见(添加,删除,更新)
- 关于list的愚见
- 关于动态数组的愚见
- sqlite 数据库 支持的数据类型 以及常用的函数
- ABAP RANGE 实现 SELECTION-OPTION 功能
- ActiveMQ的简单使用
- WMI相关
- C语言入门-第八周:作业01
- 关于Notification的一点愚见(PendingIntent工作流程)
- 基于mybatis的shard方案(分表分库、读写分离)
- day1_初识html
- oracle 树查询
- PHP遍历文件夹下所有文件
- Struts学习总结(八)---使用Struts2实现用户信息的CRUD操作(二:添加操作)
- linux交互式输入自动化 read & expect
- win7下安装myODBC和使用
- 【NOIP提高A组模拟】艾比所特