(M)SIM卡开机流程分析之SubscriptionController类分析
来源:互联网 发布:招聘程序员 编辑:程序博客网 时间:2024/05/19 12:18
首先,看看google对于SubscriptionController类的说明
/** * SubscriptionController to provide an inter-process communication to * access Sms in Icc. * * Any setters which take subId, slotId or phoneId as a parameter will throw an exception if the * parameter equals the corresponding INVALID_XXX_ID or DEFAULT_XXX_ID. * * All getters will lookup the corresponding default if the parameter is DEFAULT_XXX_ID. Ie calling * getPhoneId(DEFAULT_SUB_ID) will return the same as getPhoneId(getDefaultSubId()). * * Finally, any getters which perform the mapping between subscriptions, slots and phones will * return the corresponding INVALID_XXX_ID if the parameter is INVALID_XXX_ID. All other getters * will fail and return the appropriate error value. Ie calling getSlotId(INVALID_SUBSCRIPTION_ID) * will return INVALID_SLOT_ID and calling getSubInfoForSubscriber(INVALID_SUBSCRIPTION_ID) * will return null. * */public class SubscriptionController extends ISub.Stub { ......}再来看看,其是如何获取到对象的,那么首先我们肯定需要看看其构造方法,搜索发现,其只有两个构造方法,如下
private SubscriptionController(Context c) { mContext = c; mCM = CallManager.getInstance(); mTelephonyManager = TelephonyManager.from(mContext); mAppOps = mContext.getSystemService(AppOpsManager.class); if(ServiceManager.getService("isub") == null) { ServiceManager.addService("isub", this); } if (DBG) logdl("[SubscriptionController] init by Context");}
private SubscriptionController(Phone phone) { mContext = phone.getContext(); mCM = CallManager.getInstance(); mAppOps = mContext.getSystemService(AppOpsManager.class); if(ServiceManager.getService("isub") == null) { ServiceManager.addService("isub", this); } if (DBG) logdl("[SubscriptionController] init by Phone");}这两个构造方法中主要是做了获取一些参数,并新建CallManager对象,TelephonyManager对象,AppOpsManager对象等
这两个构造方法全都是private的,那么从外部是如何获得其对象的呢?
public static SubscriptionController getInstance() { if (sInstance == null) { Log.wtf(LOG_TAG, "getInstance null"); } return sInstance;}getInstance方法中获取全局static变量sInstance,但是依旧没有新建对象,搜索sInstance新建位置
// Leo, 构造函数,第二个对象是RIL对象数组,单例模式public static SubscriptionController init(Context c, CommandsInterface[] ci) { synchronized (SubscriptionController.class) { if (sInstance == null) { sInstance = new SubscriptionController(c); } else { Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance); } return sInstance; }}原来是在init方法中进行新建的啊,而而且是单例模式
从以上可以看出,如果从外部需要获取SubscriptionController对象,我们可以通过两种方法获取,一种是调用其static init方法,另一个是getInstance方法,但是,如果我们使用getInstance方法,在此之前,一定已经调用过init方法了。
再来看看,这个类中究竟有哪些方法
public void notifySubscriptionInfoChanged() { ITelephonyRegistry tr = ITelephonyRegistry.Stub.asInterface(ServiceManager.getService( "telephony.registry")); try { if (DBG) logd("notifySubscriptionInfoChanged:"); tr.notifySubscriptionInfoChanged(); } catch (RemoteException ex) { // Should never happen because its always available. } // FIXME: Remove if listener technique accepted. broadcastSimInfoContentChanged(); }
/** * Broadcast when SubscriptionInfo has changed * FIXME: Hopefully removed if the API council accepts SubscriptionInfoListener */ private void broadcastSimInfoContentChanged() { Intent intent = new Intent(TelephonyIntents.ACTION_SUBINFO_CONTENT_CHANGE); mContext.sendBroadcast(intent); intent = new Intent(TelephonyIntents.ACTION_SUBINFO_RECORD_UPDATED); mContext.sendBroadcast(intent); }当SubscriptionInfo数据变化后,发送广播通知
/** * Get the SubInfoRecord(s) of the currently inserted SIM(s) * @param callingPackage The package making the IPC. * @return Array list of currently inserted SubInfoRecord(s) */@Overridepublic List<SubscriptionInfo> getActiveSubscriptionInfoList(String callingPackage) { ...... if (!canReadPhoneState(callingPackage, "getActiveSubscriptionInfoList")) { return null; } // Now that all security checks passes, perform the operation as ourselves. final long identity = Binder.clearCallingIdentity(); try { if (!isSubInfoReady()) { ...... return null; } List<SubscriptionInfo> subList = getSubInfo( SubscriptionManager.SIM_SLOT_INDEX + ">=0", null); if (subList != null) { // FIXME: Unnecessary when an insertion sort is used! Collections.sort(subList, new Comparator<SubscriptionInfo>() { @Override public int compare(SubscriptionInfo arg0, SubscriptionInfo arg1) { // Primary sort key on SimSlotIndex int flag = arg0.getSimSlotIndex() - arg1.getSimSlotIndex(); if (flag == 0) { // Secondary sort on SubscriptionId return arg0.getSubscriptionId() - arg1.getSubscriptionId(); } return flag; } }); ...... } else { ...... } return subList; } finally { Binder.restoreCallingIdentity(identity); }}
/** * Query SubInfoRecord(s) from subinfo database * @param selection A filter declaring which rows to return * @param queryKey query key content * @return Array list of queried result from database */ private List<SubscriptionInfo> getSubInfo(String selection, Object queryKey) { ...... String[] selectionArgs = null; if (queryKey != null) { selectionArgs = new String[] {queryKey.toString()}; } ArrayList<SubscriptionInfo> subList = null; Cursor cursor = mContext.getContentResolver().query(SubscriptionManager.CONTENT_URI, null, selection, selectionArgs, null); try { if (cursor != null) { while (cursor.moveToNext()) { SubscriptionInfo subInfo = getSubInfoRecord(cursor); if (subInfo != null) { if (subList == null) { subList = new ArrayList<SubscriptionInfo>(); } subList.add(subInfo); } } } else { if (DBG) logd("Query fail"); } } finally { if (cursor != null) { cursor.close(); } } return subList;}
/** * New SubInfoRecord instance and fill in detail info * @param cursor * @return the query result of desired SubInfoRecord */private SubscriptionInfo getSubInfoRecord(Cursor cursor) { int id = cursor.getInt(cursor.getColumnIndexOrThrow( SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID)); String iccId = cursor.getString(cursor.getColumnIndexOrThrow( SubscriptionManager.ICC_ID)); int simSlotIndex = cursor.getInt(cursor.getColumnIndexOrThrow( SubscriptionManager.SIM_SLOT_INDEX)); String displayName = cursor.getString(cursor.getColumnIndexOrThrow( SubscriptionManager.DISPLAY_NAME)); String carrierName = cursor.getString(cursor.getColumnIndexOrThrow( SubscriptionManager.CARRIER_NAME)); int nameSource = cursor.getInt(cursor.getColumnIndexOrThrow( SubscriptionManager.NAME_SOURCE)); int iconTint = cursor.getInt(cursor.getColumnIndexOrThrow( SubscriptionManager.COLOR)); String number = cursor.getString(cursor.getColumnIndexOrThrow( SubscriptionManager.NUMBER)); int dataRoaming = cursor.getInt(cursor.getColumnIndexOrThrow( SubscriptionManager.DATA_ROAMING)); // Get the blank bitmap for this SubInfoRecord Bitmap iconBitmap = BitmapFactory.decodeResource(mContext.getResources(), com.android.internal.R.drawable.ic_sim_card_multi_24px_clr); int mcc = cursor.getInt(cursor.getColumnIndexOrThrow( SubscriptionManager.MCC)); int mnc = cursor.getInt(cursor.getColumnIndexOrThrow( SubscriptionManager.MNC)); // FIXME: consider stick this into database too String countryIso = getSubscriptionCountryIso(id); ...... // If line1number has been set to a different number, use it instead. String line1Number = mTelephonyManager.getLine1NumberForSubscriber(id); if (!TextUtils.isEmpty(line1Number) && !line1Number.equals(number)) { number = line1Number; } return new SubscriptionInfo(id, iccId, simSlotIndex, displayName, carrierName, nameSource, iconTint, number, dataRoaming, iconBitmap, mcc, mnc, countryIso);}获取content://telephony/siminfo数据库中保存的siminfo的数据信息
/** * Get the active SubscriptionInfo with the subId key * @param subId The unique SubscriptionInfo key in database * @param callingPackage The package making the IPC. * @return SubscriptionInfo, maybe null if its not active */@Overridepublic SubscriptionInfo getActiveSubscriptionInfo(int subId, String callingPackage) { ......}通过subId获取SubscriptionInfo
/** * Get the active SubscriptionInfo associated with the iccId * @param iccId the IccId of SIM card * @param callingPackage The package making the IPC. * @return SubscriptionInfo, maybe null if its not active */@Overridepublic SubscriptionInfo getActiveSubscriptionInfoForIccId(String iccId, String callingPackage) { ......}通过iccId获取SubscriptionInfo
/** * Get the active SubscriptionInfo associated with the slotIdx * @param slotIdx the slot which the subscription is inserted * @param callingPackage The package making the IPC. * @return SubscriptionInfo, maybe null if its not active */@Overridepublic SubscriptionInfo getActiveSubscriptionInfoForSimSlotIndex(int slotIdx, String callingPackage) { ......}通过slotIdx获取SubsciptionInfo
/** * @param callingPackage The package making the IPC. * @return List of all SubscriptionInfo records in database, * include those that were inserted before, maybe empty but not null. * @hide */@Overridepublic List<SubscriptionInfo> getAllSubInfoList(String callingPackage) { ......}获取所有的SubscriptionInfo
/** * Get the SUB count of active SUB(s) * @param callingPackage The package making the IPC. * @return active SIM count */@Overridepublic int getActiveSubInfoCount(String callingPackage) { ......}获取有效的SubscriptionInfo数量,即当前插入手机的有效Sim卡数量
/** * Get the SUB count of all SUB(s) in SubscriptoinInfo database * @param callingPackage The package making the IPC. * @return all SIM count in database, include what was inserted before */@Overridepublic int getAllSubInfoCount(String callingPackage) { ......}获取所有的SubscriptionInfo的数量
/** * @return the maximum number of subscriptions this device will support at any one time. */@Overridepublic int getActiveSubInfoCountMax() { ......}获取能够支持的SubscriptionInfo的最大数量
/** * Add a new SubInfoRecord to subinfo database if needed * @param iccId the IccId of the SIM card * @param slotId the slot which the SIM is inserted * @return 0 if success, < 0 on error. */@Overridepublic int addSubInfoRecord(String iccId, int slotId) { if (DBG) logdl("[addSubInfoRecord]+ iccId:" + iccId + " slotId:" + slotId); enforceModifyPhoneState("addSubInfoRecord"); // Now that all security checks passes, perform the operation as ourselves. final long identity = Binder.clearCallingIdentity(); try { if (iccId == null) { if (DBG) logdl("[addSubInfoRecord]- null iccId"); return -1; } ContentResolver resolver = mContext.getContentResolver(); // 根据iccId,从数据库中查找 Cursor cursor = resolver.query(SubscriptionManager.CONTENT_URI, new String[]{SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID, SubscriptionManager.SIM_SLOT_INDEX, SubscriptionManager.NAME_SOURCE}, SubscriptionManager.ICC_ID + "=?", new String[]{iccId}, null); // 获取SIM卡代表的颜色 int color = getUnusedColor(mContext.getOpPackageName()); boolean setDisplayName = false; try { if (cursor == null || !cursor.moveToFirst()) { // 若数据库中无此SIM卡,即此前并未插入过此SIM卡,则将这张SIM卡信息插入到数据库中 setDisplayName = true; ContentValues value = new ContentValues(); value.put(SubscriptionManager.ICC_ID, iccId); // default SIM color differs between slots value.put(SubscriptionManager.COLOR, color); value.put(SubscriptionManager.SIM_SLOT_INDEX, slotId); value.put(SubscriptionManager.CARRIER_NAME, ""); Uri uri = resolver.insert(SubscriptionManager.CONTENT_URI, value); if (DBG) logdl("[addSubInfoRecord] New record created: " + uri); } else { // 若此前已保存有此SIM卡信息,则更新此SIM卡的信息 int subId = cursor.getInt(0); int oldSimInfoId = cursor.getInt(1); int nameSource = cursor.getInt(2); ContentValues value = new ContentValues(); if (slotId != oldSimInfoId) { value.put(SubscriptionManager.SIM_SLOT_INDEX, slotId); } // 若displayName和此前已经保存的数据不一致,则将setDisplayName的值,切换为true if (nameSource != SubscriptionManager.NAME_SOURCE_USER_INPUT) { setDisplayName = true; } if (value.size() > 0) { resolver.update(SubscriptionManager.CONTENT_URI, value, SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=" + Long.toString(subId), null); } if (DBG) logdl("[addSubInfoRecord] Record already exists"); } } finally { if (cursor != null) { cursor.close(); } } // 根据SIM卡的slot Index,查询数据库中是否已有此SIM卡信息 cursor = resolver.query(SubscriptionManager.CONTENT_URI, null, SubscriptionManager.SIM_SLOT_INDEX + "=?", new String[] {String.valueOf(slotId)}, null); try { if (cursor != null && cursor.moveToFirst()) { // 若已有SIM卡信息,则做一些操作 do { int subId = cursor.getInt(cursor.getColumnIndexOrThrow( SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID)); // If sSlotIdxToSubId already has a valid subId for a slotId/phoneId, // do not add another subId for same slotId/phoneId. Integer currentSubId = sSlotIdxToSubId.get(slotId); if (currentSubId == null || !SubscriptionManager.isValidSubscriptionId(currentSubId)) { // TODO While two subs active, if user deactivats first // one, need to update the default subId with second one. // FIXME: Currently we assume phoneId == slotId which in the future // may not be true, for instance with multiple subs per slot. // But is true at the moment. sSlotIdxToSubId.put(slotId, subId); int subIdCountMax = getActiveSubInfoCountMax(); int defaultSubId = getDefaultSubId(); ...... // Set the default sub if not set or if single sim device if (!SubscriptionManager.isValidSubscriptionId(defaultSubId) || subIdCountMax == 1) { setDefaultFallbackSubId(subId); } // If single sim device, set this subscription as the default for everything // 若为单卡手机,则更新其默认数据连接卡,默认短信卡,默认语音通话卡 if (subIdCountMax == 1) { ...... setDefaultDataSubId(subId); setDefaultSmsSubId(subId); setDefaultVoiceSubId(subId); } } else { ...... } ...... } while (cursor.moveToNext()); } } finally { if (cursor != null) { cursor.close(); } } // Set Display name after sub id is set above so as to get valid simCarrierName int[] subIds = getSubId(slotId); if (subIds == null || subIds.length == 0) { ...... return -1; } if (setDisplayName) { // 获取SIM卡的CarrierName String simCarrierName = mTelephonyManager.getSimOperatorNameForSubscription(subIds[0]); String nameToSet; if (!TextUtils.isEmpty(simCarrierName)) { nameToSet = simCarrierName; } else { nameToSet = "CARD " + Integer.toString(slotId + 1); } ContentValues value = new ContentValues(); value.put(SubscriptionManager.DISPLAY_NAME, nameToSet); resolver.update(SubscriptionManager.CONTENT_URI, value, SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=" + Long.toString(subIds[0]), null); ...... } // Once the records are loaded, notify DcTracker // 通知DcTracker updateAllDataConnectionTrackers(); ..... } finally { Binder.restoreCallingIdentity(identity); } return 0;}此方法主要是做了
1)根据要插入数据库的SIM卡的iccId,从数据库中查询,此前是否有插入过此SIM卡,若没有,则插入,若有,则更新SIM卡信息,确认是否需要更新其displayName
2)根据slot index,查找数据库,确认数据库中已有该SIM卡信息,对于该SIM卡进行必要的操作,如若是单卡机器,则将其默认数据连接卡/默认短信卡/默认语音通话卡设置为该卡等
3)确认是否需要更新其displayName,此处可以更改设置下SIM卡的显示名称
/** * Generate and set carrier text based on input parameters * @param showPlmn flag to indicate if plmn should be included in carrier text * @param plmn plmn to be included in carrier text * @param showSpn flag to indicate if spn should be included in carrier text * @param spn spn to be included in carrier text * @return true if carrier text is set, false otherwise */public boolean setPlmnSpn(int slotId, boolean showPlmn, String plmn, boolean showSpn, String spn) { synchronized (mLock) { int[] subIds = getSubId(slotId); if (mContext.getPackageManager().resolveContentProvider( SubscriptionManager.CONTENT_URI.getAuthority(), 0) == null || subIds == null || !SubscriptionManager.isValidSubscriptionId(subIds[0])) { // No place to store this info. Notify registrants of the change anyway as they // might retrieve the SPN/PLMN text from the SST sticky broadcast. // TODO: This can be removed once SubscriptionController is not running on devices // that don't need it, such as TVs. if (DBG) logd("[setPlmnSpn] No valid subscription to store info"); notifySubscriptionInfoChanged(); return false; } String carrierText = ""; if (showPlmn) { carrierText = plmn; if (showSpn) { // Need to show both plmn and spn if both are not same. if(!Objects.equals(spn, plmn)) { String separator = mContext.getString( com.android.internal.R.string.kg_text_message_separator).toString(); carrierText = new StringBuilder().append(carrierText).append(separator) .append(spn).toString(); } } } else if (showSpn) { carrierText = spn; } for (int i = 0; i < subIds.length; i++) { setCarrierText(carrierText, subIds[i]); } return true; }}此方法是确认carrierText的显示方式,是显示spn,还是显示plmn
/** * Set display name by simInfo index * @param displayName the display name of SIM card * @param subId the unique SubInfoRecord index in database * @return the number of records updated */@Overridepublic int setDisplayName(String displayName, int subId) { return setDisplayNameUsingSrc(displayName, subId, -1);}
/** * Set display name by simInfo index with name source * @param displayName the display name of SIM card * @param subId the unique SubInfoRecord index in database * @param nameSource 0: NAME_SOURCE_DEFAULT_SOURCE, 1: NAME_SOURCE_SIM_SOURCE, * 2: NAME_SOURCE_USER_INPUT, -1 NAME_SOURCE_UNDEFINED * @return the number of records updated */@Overridepublic int setDisplayNameUsingSrc(String displayName, int subId, long nameSource) { ...... // Now that all security checks passes, perform the operation as ourselves. final long identity = Binder.clearCallingIdentity(); try { validateSubId(subId); String nameToSet; if (displayName == null) { nameToSet = mContext.getString(SubscriptionManager.DEFAULT_NAME_RES); } else { nameToSet = displayName; } ContentValues value = new ContentValues(1); value.put(SubscriptionManager.DISPLAY_NAME, nameToSet); if (nameSource >= SubscriptionManager.NAME_SOURCE_DEFAULT_SOURCE) { ...... value.put(SubscriptionManager.NAME_SOURCE, nameSource); } ...... int result = mContext.getContentResolver().update(SubscriptionManager.CONTENT_URI, value, SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=" + Long.toString(subId), null); notifySubscriptionInfoChanged(); return result; } finally { Binder.restoreCallingIdentity(identity); }}设置SIM卡信息中的displayName,并更新数据库
/** * Set phone number by subId * @param number the phone number of the SIM * @param subId the unique SubInfoRecord index in database * @return the number of records updated */@Overridepublic int setDisplayNumber(String number, int subId) { ......}
/** * Set MCC/MNC by subscription ID * @param mccMnc MCC/MNC associated with the subscription * @param subId the unique SubInfoRecord index in database * @return the number of records updated */public int setMccMnc(String mccMnc, int subId) { ......}
/** * Set MCC/MNC by subscription ID * @param mccMnc MCC/MNC associated with the subscription * @param subId the unique SubInfoRecord index in database * @return the number of records updated */public int setMccMnc(String mccMnc, int subId) { ......}设置SIM卡信息的一些数据,并存入数据库
@Overridepublic void setDefaultSmsSubId(int subId) { enforceModifyPhoneState("setDefaultSmsSubId"); if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) { throw new RuntimeException("setDefaultSmsSubId called with DEFAULT_SUB_ID"); } if (DBG) logdl("[setDefaultSmsSubId] subId=" + subId); Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.MULTI_SIM_SMS_SUBSCRIPTION, subId); broadcastDefaultSmsSubIdChanged(subId);}设置默认的短信SIM卡
@Overridepublic void setDefaultVoiceSubId(int subId) { enforceModifyPhoneState("setDefaultVoiceSubId"); if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) { throw new RuntimeException("setDefaultVoiceSubId called with DEFAULT_SUB_ID"); } if (DBG) logdl("[setDefaultVoiceSubId] subId=" + subId); Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.MULTI_SIM_VOICE_CALL_SUBSCRIPTION, subId); broadcastDefaultVoiceSubIdChanged(subId);}设置默认的语音通话卡
@Overridepublic void setDefaultDataSubId(int subId) { enforceModifyPhoneState("setDefaultDataSubId"); if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) { throw new RuntimeException("setDefaultDataSubId called with DEFAULT_SUB_ID"); } if (DBG) logdl("[setDefaultDataSubId] subId=" + subId); ProxyController proxyController = ProxyController.getInstance(); int len = sProxyPhones.length; logdl("[setDefaultDataSubId] num phones=" + len); if (SubscriptionManager.isValidSubscriptionId(subId)) { // Only re-map modems if the new default data sub is valid RadioAccessFamily[] rafs = new RadioAccessFamily[len]; boolean atLeastOneMatch = false; for (int phoneId = 0; phoneId < len; phoneId++) { PhoneProxy phone = sProxyPhones[phoneId]; int raf; int id = phone.getSubId(); if (id == subId) { // TODO Handle the general case of N modems and M subscriptions. raf = proxyController.getMaxRafSupported(); atLeastOneMatch = true; } else { // TODO Handle the general case of N modems and M subscriptions. raf = proxyController.getMinRafSupported(); } logdl("[setDefaultDataSubId] phoneId=" + phoneId + " subId=" + id + " RAF=" + raf); rafs[phoneId] = new RadioAccessFamily(phoneId, raf); } if (atLeastOneMatch) { proxyController.setRadioCapability(rafs); } else { if (DBG) logdl("[setDefaultDataSubId] no valid subId's found - not updating."); } } // FIXME is this still needed? updateAllDataConnectionTrackers(); Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION, subId); broadcastDefaultDataSubIdChanged(subId);}设置默认的数据链接卡
0 0
- (M)SIM卡开机流程分析之SubscriptionController类分析
- (M)SIM卡开机流程分析之主线分析
- (M)SIM卡开机流程分析之SPN加载
- (M)SIM卡开机流程分析之TelephonyDevController类分析
- (M)SIM卡开机流程分析之TelephonyManager类分析
- (M)SIM卡开机流程分析之UiccController类分析
- (M)SIM卡开机流程分析之RIL类分析
- (M)SIM卡开机流程分析之DefaultPhoneNotifier类分析
- (M)SIM卡开机流程分析之默认APN设置
- (M)SIM卡开机流程分析之显示名称加载
- 开机导入Sim卡联系人流程分析
- android之Sim Tool Kit流程分析
- Android4.4 Telephony流程分析——SIM卡开机时的初始化
- Android4.4 Telephony流程分析——SIM卡开机时的数据加载
- Android 4.4Telephony流程分析SIM卡开机时的数据加载
- Android 4.4Telephony流程分析SIM卡开机时的初始化
- Android4.4 Telephony流程分析——SIM卡开机时的初始化
- 联系人开机自动导入SIM卡联系人分析
- mysql存emoji表情报错处理
- Leetcode040--是否是平衡二叉树
- 算法之路(一)
- 测试各种类型所占内存的大小
- android 学习中实用(一)
- (M)SIM卡开机流程分析之SubscriptionController类分析
- 进程调度方式
- 在[Linux]下 PHP程序员如何玩转Linux系列-lnmp环境的搭建
- JavaWeb学习----JSP简介及入门(含Eclipse for Java EE及Tomcat的配置)
- 在测试中mock的作用
- 什么是小程序
- phpMyAdmin中修改用户名和密码
- Hive快速入门
- C++中多态的几大关键点