每篇一格言:
人生就像滚雪球,关键是要找到足够湿的雪,和足够长的坡。
——沃伦巴菲特

目录

  • 1. APN:从概念说起
    • 1.1 从3GPP看APN的定义和角色
    • 1.2 APN包含哪些参数
      • APN的类型:
    • 1.3 APN的存储位置与加载位置
      • 1.3.1 APN的存储位置
      • 1.3.2 APN的加载位置
    • 1.4虚拟运营商的APN
  • 2. APN的实现机制
    • 2.1 APN的创建: 从XML到database
    • 2.2 APN匹配SIM卡与菜单显示
    • 2.3 PDP时APN的选择
    • 2.4 modem中的APN
  • 附录1 从xml加载到database
  • 附录2。APN的读取与菜单显示
  • 附录3 PDP中的APN

1. APN:从概念说起

1.1 从3GPP看APN的定义和角色

Definition of Access Point Name
In the GPRS backbone, an Access Point Name (APN) is a reference to a GGSN. To support inter-PLMN roaming, the internal GPRS DNS functionality is used to translate the APN into the IP address of the GGSN.
——3gpp 23.003

从定义可看出,APN是GGSN的引用,被internal GPRS DNS转换为GGSN的IP地址。

那么GGSN是什么,又是做什么的呢?
GGSN全称Gateway GPRS Support Node, 网关GPRS支持节点

GGSN主要起网关作用,所扮演的角色:
对内:网络传输; (网络接入控制,分组数据的过滤)
对外:路由器(路由选择和分组的转发,IP地址分配)

1.2 APN包含哪些参数

一个典型的APN包含的参数有名称、MCCMNC、接入点、类型。

下面以CMCC APN为例,它包含下面一些参数:
<apn carrier=“连接互联网” //名称
mcc=“460” //MCC
mnc=“07” //MNC
apn=“cmnet” //接入点
type=“default,supl,net” //类型
/>

APN还可以包含其他更多的参数,主要有下面这些:

Note :每个有数据业务的运营商都会设定自己的APN,同一个运营商的APN可能有多条,包括分别用于3G或4G,NET和WAP,不同APN的使用范围和收费会有差别。

APN的类型:

常见的APN类型有下面几种,用途和优先级各有不同。

1.3 APN的存储位置与加载位置

1.3.1 APN的存储位置

APN以XML的格式存储。
文件名
Apns-conf.xml

源码文件路径
MTK平台(通常):alps\mediatek\frameworks\base\telephony\etc
高通平台(通常):android\vendor\qcom\proprietary\telephony-apps\etc

UE文件路径
system/etc/ Apns-conf.xml

当UE开机后,读取XML中的APN并写入到database中。

database中APN 的table名称
content://telephony/carriers
注:有些平台为了适配dual SIM,可能会加上sub1/sub2等。

1.3.2 APN的加载位置

加载到database:
TelephonyProvider读取XML并在database中插入apn的table。

加载到UI菜单:
根据SIM卡的MCCMNC,去匹配database中同样MCCMNC的APN项,并将匹配到的APN填写到菜单列表。

加载到PDP请求:
由DCtracker负责创建/更新waiting APN list,供PDP选用。

1.4虚拟运营商的APN

虚拟运营商(MVNO)没有自营网段,使用了主运营商的网段,因而和主运营商有相同的MCCMNC。为了能够与主运营商区分,虚拟运营商的APN还包含了MVNO参数。MVNO参数分为SPN/PNN/IMSI/GID1,是从SIM卡对应栏位读取的值,目的是从该值中判断该SIM卡是否属于MVNO。

在加载MVNO SIM卡的APN时,会同时去匹配MCCMNC和MVNO参数。

APN的概念部分到此告一段落,下面做个总结:

2. APN的实现机制

2.1 APN的创建: 从XML到database

在上一章中我们讲到,APN的原始参数存放在XML中;
而UE是通过query database的方式使用APN;
因而必然需要将APN从XML录入到database,这一步在telephonyprovider中实现。

为了更清晰的理解实现思路,避免过多的源码干扰阅读节奏,接下来的分析中涉及到的源码部分,都以伪代码的形式给出,详细的代码以附录形式给出。

telephonyprovider中的initDatabase方法的逻辑(伪代码):

private void initDatabase(SQLiteDatabase db) {1.打开APN xml文件etc/apns-conf.xml2. 获得文件句柄后,使用FileReader得到文件字符流3. 检查APN version一致性4.加载XMl中的数据到database,具体见loadApns方法
}

源码点这里
对第3点:“检查APN version一致性” 的说明:
随着OS升级,APN字段也在更新,因此检查APN version的目的是为了避免XML与OS不兼容。若version不一致,抛出异常:

// Sanity check. Force internal version and confidential versions to agreeint confversion = Integer.parseInt(confparser.getAttributeValue(null, "version"));if (publicversion != confversion) {throw new IllegalStateException("Internal APNS file version doesn't match "+ confFile.getAbsolutePath());}

loadApns的逻辑(伪代码)

private void loadApns(SQLiteDatabase db, XmlPullParser parser) {
1。每次读取parser中的一个element,也就是一条APN数据
2.通过getrow方法将APN转换为Contentvalues;
3.最后通过insertAddingDefaults将键值写入database:
}

写入的table名是CARRIERS_TABLE,该table由createCarriersTable方法创建。
该table url是:content://telephony/carriers/

table 创建时机:
telephonyprovider的内部类DatabaseHelper在oncreate时创建APN table。
DatabaseHelper类负责APN database的增删改查工作。

2.2 APN匹配SIM卡与菜单显示

设备插入SIM后,设置菜单中会显示该SIM卡对应的APN菜单。
这部分在ApnSettings.java (android\packages\apps\settings\src\com\android\settings) 中的fillList方法实现的。该方法主要根据SIM卡的mccmnc去query db并将APN填入菜单中。感兴趣可自行查看,不再具体讨论。

对于MVNO SIM卡,除了mccmnc还需根据MVNO type和MVNO value过滤。详细见mvnoMatches方法。

备注1.
SIM卡的mccmnc获取的是PROPERTY_ICC_OPERATOR_NUMERIC的值,而PROPERTY_ICC_OPERATOR_NUMERIC是在SIM loaded后根据IMSI得到。(取前5位or前6位)

备注2.
mvno type举例:SPN类型,通过getServiceProviderName获取,其值来自SIM卡中的EF_SPN:分为SIM (EF_SPN=0x6F46)或 RUIM (EF_RUIM_SPN=0x6F41)。

有关SIM卡加载的更多内容,请移步这里:
Android:USIM软件架构与加载流程

2.3 PDP时APN的选择

UE插入SIM卡后,telephony根据SIM卡的mccmnc,先创建一个包含该SIM卡所有APN的列表(AllApnList)。

这一步在DcTracker中实现。Call flow见下图:

下面列出了createAllApnList的实现步骤(伪代码):

protected void createAllApnList() {
1。根据SIM卡mccmnc,创建APN list
2。设置preferred APN
3。Set DataProfile in modem
}

有了AllApnList,在setup data时,根据数据业务类型(上网/彩信/…),从all apn list中筛选出“waiting apn list”,用于建立PDP.

private ArrayList<ApnSetting> buildWaitingApns(String requestedApnType, int radioTech) {
//根据requestedApnType和radioTech筛选waiting apn
}

2.4 modem中的APN

modem中同样存储了APN,详细内容可参考我的另一篇文章:
Modem2G/3G/4G/5G:APN:使用AT+CGDCONT命令设置modem默认APN(CID1)

你可能想知道更多详细内容,下面源码以附录形式给出。

附录1 从xml加载到database

TelephonyProvider.java (amss\linux\android\packages\providers\telephonyprovider\src\com\android\providers\telephony)

        private void initDatabase(SQLiteDatabase db) {if (VDBG) log("dbh.initDatabase:+ db=" + db);// Read internal APNS dataResources r = mContext.getResources();XmlResourceParser parser = r.getXml(com.android.internal.R.xml.apns);int publicversion = -1;try {XmlUtils.beginDocument(parser, "apns");publicversion = Integer.parseInt(parser.getAttributeValue(null, "version"));loadApns(db, parser);} catch (Exception e) {loge("Got exception while loading APN database." + e);} finally {parser.close();}// Read external APNS data (partner-provided)XmlPullParser confparser = null;File confFile = getApnConfFile();FileReader confreader = null;if (DBG) log("confFile = " + confFile);try {confreader = new FileReader(confFile);confparser = Xml.newPullParser();confparser.setInput(confreader);XmlUtils.beginDocument(confparser, "apns");// Sanity check. Force internal version and confidential versions to agreeint confversion = Integer.parseInt(confparser.getAttributeValue(null, "version"));if (publicversion != confversion) {log("initDatabase: throwing exception due to version mismatch");throw new IllegalStateException("Internal APNS file version doesn't match "+ confFile.getAbsolutePath());}loadApns(db, confparser);} catch (FileNotFoundException e) {// It's ok if the file isn't found. It means there isn't a confidential file// Log.e(TAG, "File not found: '" + confFile.getAbsolutePath() + "'");} catch (Exception e) {loge("initDatabase: Exception while parsing '" + confFile.getAbsolutePath() + "'" +e);} finally {// Get rid of user/carrier deleted entries that are not present in apn xml file.// Those entries have edited value USER_DELETED/CARRIER_DELETED.if (VDBG) {log("initDatabase: deleting USER_DELETED and replacing "+ "DELETED_BUT_PRESENT_IN_XML with DELETED");}// Delete USER_DELETEDdb.delete(CARRIERS_TABLE, IS_USER_DELETED + " or " + IS_CARRIER_DELETED, null);// Change USER_DELETED_BUT_PRESENT_IN_XML to USER_DELETEDContentValues cv = new ContentValues();cv.put(EDITED, USER_DELETED);db.update(CARRIERS_TABLE, cv, IS_USER_DELETED_BUT_PRESENT_IN_XML, null);// Change CARRIER_DELETED_BUT_PRESENT_IN_XML to CARRIER_DELETEDcv = new ContentValues();cv.put(EDITED, CARRIER_DELETED);db.update(CARRIERS_TABLE, cv, IS_CARRIER_DELETED_BUT_PRESENT_IN_XML, null);if (confreader != null) {try {confreader.close();} catch (IOException e) {// do nothing}}// Update the stored checksumsetApnConfChecksum(getChecksum(confFile));}if (VDBG) log("dbh.initDatabase:- db=" + db);}
private void loadApns(SQLiteDatabase db, XmlPullParser parser) {if (parser != null) {try {db.beginTransaction();XmlUtils.nextElement(parser);while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {ContentValues row = getRow(parser);if (row == null) {throw new XmlPullParserException("Expected 'apn' tag", parser, null);}insertAddingDefaults(db, row);XmlUtils.nextElement(parser);}db.setTransactionSuccessful();} catch (XmlPullParserException e) {loge("Got XmlPullParserException while loading apns." + e);} catch (IOException e) {loge("Got IOException while loading apns." + e);} catch (SQLException e) {loge("Got SQLException while loading apns." + e);} finally {db.endTransaction();}}}

附录2。APN的读取与菜单显示

ApnSettings.java (amss\linux\android\packages\apps\settings\src\com\android\settings)

private void fillList() {final TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);final int subId = mSubscriptionInfo != null ? mSubscriptionInfo.getSubscriptionId(): SubscriptionManager.INVALID_SUBSCRIPTION_ID;final String mccmnc = mSubscriptionInfo == null ? "" : tm.getSimOperator(subId);Log.d(TAG, "mccmnc = " + mccmnc);StringBuilder where = new StringBuilder("numeric=\"" + mccmnc +"\" AND NOT (type='ia' AND (apn=\"\" OR apn IS NULL)) AND user_visible!=0");if (mHideImsApn) {where.append(" AND NOT (type='ims')");}appendFilter(where);Log.d(TAG, "where = " + where.toString());Cursor cursor = getContentResolver().query(Telephony.Carriers.CONTENT_URI, new String[] {"_id", "name", "apn", "type", "mvno_type", "mvno_match_data", "bearer", "bearer_bitmask"}, where.toString(),null, Telephony.Carriers.DEFAULT_SORT_ORDER);if (cursor != null) {IccRecords r = null;if (mUiccController != null && mSubscriptionInfo != null) {r = mUiccController.getIccRecords(SubscriptionManager.getPhoneId(subId), UiccController.APP_FAM_3GPP);}PreferenceGroup apnList = (PreferenceGroup) findPreference("apn_list");apnList.removeAll();ArrayList<ApnPreference> mnoApnList = new ArrayList<ApnPreference>();ArrayList<ApnPreference> mvnoApnList = new ArrayList<ApnPreference>();ArrayList<ApnPreference> mnoMmsApnList = new ArrayList<ApnPreference>();ArrayList<ApnPreference> mvnoMmsApnList = new ArrayList<ApnPreference>();mSelectedKey = getSelectedApnKey();cursor.moveToFirst();while (!cursor.isAfterLast()) {String name = cursor.getString(NAME_INDEX);String apn = cursor.getString(APN_INDEX);String key = cursor.getString(ID_INDEX);String type = cursor.getString(TYPES_INDEX);String mvnoType = cursor.getString(MVNO_TYPE_INDEX);String mvnoMatchData = cursor.getString(MVNO_MATCH_DATA_INDEX);//Special requirement of some operators, need change APN name follow language.String localizedName = Utils.getLocalizedName(getActivity(), cursor, NAME_INDEX);if (!TextUtils.isEmpty(localizedName)) {name = localizedName;}int bearer = cursor.getInt(BEARER_INDEX);int bearerBitMask = cursor.getInt(BEARER_BITMASK_INDEX);int fullBearer = ServiceState.getBitmaskForTech(bearer) | bearerBitMask;int radioTech = networkTypeToRilRidioTechnology(TelephonyManager.getDefault().getDataNetworkType(subId));if (!ServiceState.bitmaskHasTech(fullBearer, radioTech)&& (bearer != 0 || bearerBitMask != 0)) {// In OOS, show APN with bearer as defaultif ((radioTech != ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN) || (bearer == 0&& radioTech == ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN)) {cursor.moveToNext();continue;}}ApnPreference pref = new ApnPreference(getPrefContext());pref.setKey(key);pref.setTitle(name);pref.setSummary(apn);pref.setPersistent(false);pref.setOnPreferenceChangeListener(this);pref.setSubId(subId);boolean selectable = ((type == null) || !type.equals("mms"));pref.setSelectable(selectable);if (selectable) {if ((mSelectedKey != null) && mSelectedKey.equals(key)) {pref.setChecked();}addApnToList(pref, mnoApnList, mvnoApnList, r, mvnoType, mvnoMatchData);} else {addApnToList(pref, mnoMmsApnList, mvnoMmsApnList, r, mvnoType, mvnoMatchData);}cursor.moveToNext();}cursor.close();if (!mvnoApnList.isEmpty()) {mnoApnList = mvnoApnList;mnoMmsApnList = mvnoMmsApnList;// Also save the mvno info}for (Preference preference : mnoApnList) {apnList.addPreference(preference);}for (Preference preference : mnoMmsApnList) {apnList.addPreference(preference);}}}

MVNO的处理:

private void addApnToList(ApnPreference pref, ArrayList<ApnPreference> mnoList,ArrayList<ApnPreference> mvnoList, IccRecords r, String mvnoType,String mvnoMatchData) {if (r != null && !TextUtils.isEmpty(mvnoType) && !TextUtils.isEmpty(mvnoMatchData)) {if (ApnSetting.mvnoMatches(r, mvnoType, mvnoMatchData)) {mvnoList.add(pref);// Since adding to mvno list, save mvno infomMvnoType = mvnoType;mMvnoMatchData = mvnoMatchData;}} else {mnoList.add(pref);}}

附录3 PDP中的APN

DcTracker.java
createAllApn:

protected void createAllApnList() {mMvnoMatched = false;mAllApnSettings = new ArrayList<>();IccRecords r = mIccRecords.get();String operator = mPhone.getOperatorNumeric();if (operator != null) {String selection = Telephony.Carriers.NUMERIC + " = '" + operator + "'";// query only enabled apn.// carrier_enabled : 1 means enabled apn, 0 disabled apn.// selection += " and carrier_enabled = 1";if (DBG) log("createAllApnList: selection=" + selection);// ORDER BY Telephony.Carriers._ID ("_id")Cursor cursor = mPhone.getContext().getContentResolver().query(Telephony.Carriers.CONTENT_URI, null, selection, null, Telephony.Carriers._ID);if (cursor != null) {if (cursor.getCount() > 0) {mAllApnSettings = createApnList(cursor);}cursor.close();}}addEmergencyApnSetting();dedupeApnSettings();if (mAllApnSettings.isEmpty()) {if (DBG) log("createAllApnList: No APN found for carrier: " + operator);mPreferredApn = null;// TODO: What is the right behavior?//notifyNoData(DataConnection.FailCause.MISSING_UNKNOWN_APN);} else {mPreferredApn = getPreferredApn();if (mPreferredApn != null && !mPreferredApn.numeric.equals(operator)) {mPreferredApn = null;setPreferredApn(-1);}if (DBG) log("createAllApnList: mPreferredApn=" + mPreferredApn);}if (DBG) log("createAllApnList: X mAllApnSettings=" + mAllApnSettings);setDataProfilesAsNeeded();}

筛选候选apn用于PDP

private ArrayList<ApnSetting> buildWaitingApns(String requestedApnType, int radioTech) {if (DBG) log("buildWaitingApns: E requestedApnType=" + requestedApnType);ArrayList<ApnSetting> apnList = new ArrayList<ApnSetting>();if (requestedApnType.equals(PhoneConstants.APN_TYPE_DUN)) {ApnSetting dun = fetchDunApn();if (dun != null) {apnList.add(dun);if (DBG) log("buildWaitingApns: X added APN_TYPE_DUN apnList=" + apnList);return apnList;}}String operator = mPhone.getOperatorNumeric();// This is a workaround for a bug (7305641) where we don't failover to other// suitable APNs if our preferred APN fails.  On prepaid ATT sims we need to// failover to a provisioning APN, but once we've used their default data// connection we are locked to it for life.  This change allows ATT devices// to say they don't want to use preferred at all.boolean usePreferred = true;try {usePreferred = ! mPhone.getContext().getResources().getBoolean(com.android.internal.R.bool.config_dontPreferApn);} catch (Resources.NotFoundException e) {if (DBG) log("buildWaitingApns: usePreferred NotFoundException set to true");usePreferred = true;}if (usePreferred) {mPreferredApn = getPreferredApn();}if (DBG) {log("buildWaitingApns: usePreferred=" + usePreferred+ " canSetPreferApn=" + mCanSetPreferApn+ " mPreferredApn=" + mPreferredApn+ " operator=" + operator + " radioTech=" + radioTech+ " IccRecords r=" + mIccRecords);}if (usePreferred && mCanSetPreferApn && mPreferredApn != null &&mPreferredApn.canHandleType(requestedApnType)) {if (DBG) {log("buildWaitingApns: Preferred APN:" + operator + ":"+ mPreferredApn.numeric + ":" + mPreferredApn);}if (mPreferredApn.numeric.equals(operator)) {if (ServiceState.bitmaskHasTech(mPreferredApn.bearerBitmask, radioTech)) {apnList.add(mPreferredApn);if (DBG) log("buildWaitingApns: X added preferred apnList=" + apnList);return apnList;} else {if (DBG) log("buildWaitingApns: no preferred APN");setPreferredApn(-1);mPreferredApn = null;}} else {if (DBG) log("buildWaitingApns: no preferred APN");setPreferredApn(-1);mPreferredApn = null;}}if (mAllApnSettings != null) {if (DBG) log("buildWaitingApns: mAllApnSettings=" + mAllApnSettings);for (ApnSetting apn : mAllApnSettings) {if (apn.canHandleType(requestedApnType)) {if (ServiceState.bitmaskHasTech(apn.bearerBitmask, radioTech)) {if (DBG) log("buildWaitingApns: adding apn=" + apn);apnList.add(apn);} else {if (DBG) {log("buildWaitingApns: bearerBitmask:" + apn.bearerBitmask + " does " +"not include radioTech:" + radioTech);}}} else if (DBG) {log("buildWaitingApns: couldn't handle requested ApnType="+ requestedApnType);}}} else {loge("mAllApnSettings is null!");}if (DBG) log("buildWaitingApns: " + apnList.size() + " APNs in the list: " + apnList);return apnList;}

相关章节:
全面&系统的解析android RSSI信号显示与刷新,总篇

本文为原创文章。看完点赞,每天进步一点点~

Android:一篇就够!全面详细解析APN(涉及内容:GGSN,authtype,MVNO,pdp,Apns-conf,supl,hipri,dun)相关推荐

  1. 从新手到Flutter架构师,一篇就够!深度解析,值得收藏

    前言 今天想停下代码,写点脑袋里不断浮现出来的一些看法. 也就是最近在微博和知乎上老看到"互联网寒冬"的说法.要么是看到啥公司薪水无法如期发放了,要么是看到别人说什么"裁 ...

  2. Android性能优化:布局优化 详细解析(含include、ViewStub、merge讲解 )

    1. 影响的性能 布局性能的好坏 主要影响 :Android应用中的页面显示速度 2. 如何影响性能 布局影响Android性能的实质:页面的测量 & 绘制时间 1个页面通过递归 完成测量 & ...

  3. android布局时长分析,Android性能优化:布局优化 详细解析(含、、讲解 )

    前言 在 Android开发中,性能优化策略十分重要 本文主要讲解性能优化中的布局优化,希望你们会喜欢. 目录 1. 影响的性能 布局性能的好坏 主要影响 :Android应用中的页面显示速度 2. ...

  4. MySQL一篇就够(详细)

    转载原文链接在文章末尾 文章目录 前言 一.SQL简述 1.SQL的概述 2.SQL的优点 3.SQL的分类 二.数据库的三大范式 三.数据库的数据类型 1.整数类型 2.浮点数类型和定点数类型 3. ...

  5. C++算法篇:DFS超详细解析(2)--- tarjan算法求无向图割边

    <<<上一篇 系列文章目录 ①:无向图基本概念 ②:tarjan算法求无向图割边 前言 第一次写算法,讲得肯不透彻,有误还请指教awa 文章目录 系列文章目录 一.回顾 二.tarj ...

  6. C++算法篇:DFS超详细解析(1)--- 无向图基本概念

    系列文章目录 ①:无向图基本概念 ②:tarjan算法求无向图割边 文章目录 系列文章目录 一.DFS是什么? 二.DFS的基本框架 三.DFS-tree 四.图的基本知识 一.DFS是什么?     ...

  7. HDMI EDID详细解析——C代码实现

    继上一篇<HDMI EDID详细解析> https://blog.csdn.net/cfl927096306/article/details/108017501 现在用C代码来实现解析HD ...

  8. python装饰器功能是冒泡排序怎么做_传说中Python最难理解的点|看这完篇就够了(装饰器)...

    https://mp.weixin.qq.com/s/B6pEZLrayqzJfMtLqiAfpQ 1.什么是装饰器 网上有人是这么评价装饰器的,我觉得写的很有趣,比喻的很形象 每个人都有的内裤主要是 ...

  9. mysql 复制 二进制文件命令_Mysql中复制详细解析

    原标题:Mysql中复制详细解析 1.mysql复制概念 指将主数据库的DDL和DML操作通过二进制日志传到复制服务器上,然后在复制服务器上将这些日志文件重新执行,从而使复制服务器和主服务器的数据保持 ...

最新文章

  1. HASH算法不是加密算法
  2. store_coding_state (cs_cm)的作用
  3. zookeeper下载安装过程
  4. python3 重新运行本程序_python3+PyQt5重新实现QT事件处理程序
  5. 如何为Linux安装Go语言
  6. 变体类型Variant
  7. 2021全国大学生数学建模竞赛E题思路
  8. 目前航信版开票软件自身导入文本数据的问题
  9. 为什么有些蓝牙耳机有底噪?高音质便宜实惠的蓝牙耳机分享
  10. vs2010的Visual Assist X破解版安装
  11. 数据库开发工程师岗位职责and技能要求
  12. Python开发基础----数据类型----[列表]
  13. Hadoop集群之开启kerberos安全认证
  14. 大数据面前,统计学的价值在哪里
  15. imagemagick替换图片指定区域颜色
  16. 手机摄像头驱动_前9月手机镜头12强名单曝光 7家出货量突破2亿颗
  17. three.js的着色器(巨详细 初学者 大白话)
  18. 华为matepad切换电脑模式_打开华为MatePad Pro电脑模式,四舍五入约等于拥有一台电脑?...
  19. 网红的冬天四季如春,人间百味自有芬芳
  20. 深入理解SSD-导读

热门文章

  1. 选择题快速求解AOE网的关键路径
  2. 用户在图片上点选并标记位置,js实现
  3. 网络编程-HTTP编程
  4. 网络服务——生成树技术STP的BPDU报文详解
  5. python解决猴子偷桃问题_猴子偷桃蟠桃园土地是知道的,不举报是不敢吗?
  6. 使用VS2012调试ReactOS源码
  7. ReactOS 代码更新后的编译安装
  8. Pyecharts树状图:地理图表
  9. 华为认证云服务工程师(HCIA-Cloud ServiceV3.0)-- 认证介绍
  10. 清音驱腐启鸿蒙,竹韵清音-格律诗词41期