首先我要从SmsManager开始一步步深入了解,相信大家在学习Android基础的时候接触过这个类。它在/frameworks/opt/telephony/src/java/android/telephony路径下,SmsManager:提供管理短信操作,如发送数据,文本和PDU短信。通过调用静态方法SmsManager.getDefault() 获取此对象。它里面提供了一系列发送短信的方法,我们就从sendTextMessage()方法说起,首先我们来看看这个方法:

public void sendTextMessage( String destinationAddress, String scAddress, String text, PendingIntent sentIntent, PendingIntent deliveryIntent)
{if (TextUtils.isEmpty(destinationAddress)){throw new IllegalArgumentException("Invalid destinationAddress");}if (TextUtils.isEmpty(text)){throw new IllegalArgumentException("Invalid message body");}try{ISms iccISms = getISmsServiceOrThrow();iccISms.sendTextForSubscriber(getSubscriptionId(), ActivityThread.currentPackageName(),destinationAddress,scAddress, text, sentIntent, deliveryIntent);} catch (RemoteException ex){// ignore it}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

先判断地址和短信内容是否为空,并且抛出异常信息,然后通过ISms这样一个东西调用sendTextForSubscriber()方法将短信往下一个类进行传递。

getISmsServiceOrThrow():获取ISms服务。 
destinationAddress:收短信人的地址。 
scAddress:短信号码中心,如果传null则为默认短信号码中心。 
text:短信内容。 
sentIntent:短信发送成功或者失败的广播。 
deliveryIntent:对方收到短信时候的广播。 
getSubscriptionId():获取订阅id。 
ActivityThread.currentPackageName():当前的包名。

在上面接触到ISms这样一个东西,那么他是干嘛的呢?其实它一个接口,在frameworks/base/telephony/java/com/android/internal/telephony下面,我们会找到它,会发现它是是一个aidl文件,打开它是一个接口(interface),这是我们就要去找另外一个类了,在android5.1.1中有能力完成短信发送任务的系统服务它就是UiccSmsController.java。它在/frameworks/opt/telephony/src/java/com/android/internal/telephony路径下面,UiccSmsController:提供一个进程间通信访问ICC中的短信。打开它会发现这样的继承关系:

public class UiccSmsController extends ISms.Stub
  • 1
  • 1

里面有这样的一段代码:

public void sendText(String callingPackage, String destAddr, String scAddr,String text, PendingIntent sentIntent, PendingIntent deliveryIntent){sendTextForSubscriber(getDefaultSmsSubId(), callingPackage, destAddr,scAddr, text, sentIntent, deliveryIntent);}public void sendTextForSubscriber(int subId, String callingPackage,String destAddr, String scAddr,String text,PendingIntent sentIntent, PendingIntent deliveryIntent){sendTextWithOptionsUsingSubscriber(subId, callingPackage, destAddr,scAddr, text, sentIntent, deliveryIntent, -1, false, -1);}public void sendTextWithOptionsUsingSubscriber(int subId,String callingPackage, String destAddr,String scAddr, String text,PendingIntent sentIntent, PendingIntent deliveryIntent,int priority, boolean isExpectMore, int validityPeriod){mContext.enforceCallingPermission(android.Manifest.permission.SEND_SMS,"Sending SMS message");IccSmsInterfaceManager iccSmsIntMgr = getIccSmsInterfaceManager(subId);if (iccSmsIntMgr.isShortSMSCode(destAddr)){iccSmsIntMgr.sendTextWithOptions(callingPackage, destAddr, scAddr,text, sentIntent, deliveryIntent, priority, isExpectMore,validityPeriod);return;}ArrayList<String> parts = new ArrayList<String>();parts.add(text);ArrayList<PendingIntent> sentIntents = new ArrayList<PendingIntent>();sentIntents.add(sentIntent);ArrayList<PendingIntent> deliveryIntents = new ArrayList<PendingIntent>();deliveryIntents.add(deliveryIntent);broadcastOutgoingSms(subId, callingPackage, destAddr, scAddr, false,parts, sentIntents, deliveryIntents, priority, isExpectMore,validityPeriod);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

可以看出现进行权限的操作,然后调用了IccSmsInterfaceManager 的sendTextWithOptions方法将短信进一步传递,后面进行广播的处理。通过getDefaultSmsSubId()获得了一个手机卡的默认SubId,在同级路径下找到IccSmsInterfaceManager 类打开之后,又会发现,它调用SMSDispatcher的sendText()方法将短信进一步传递。在同级路径下我们打开SMSDispatcher 会发现它是一个抽象类,并且继承了Handler。如下:

public abstract class SMSDispatcher extends Handler 
  • 1
  • 1

既然是抽象类,那肯定就有实现它的派生类,在Android5.1.1中我找到了三个派生类:CdmaSMSDispatcher、GsmSMSDispatcher、ImsSmsDispatcher。但是在IccSmsInterfaceManager 中只创建了ImsSmsDispatcher。

protected IccSmsInterfaceManager(PhoneBase phone){mPhone = phone;mContext = phone.getContext();mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);mDispatcher = new ImsSMSDispatcher(phone, phone.mSmsStorageMonitor,phone.mSmsUsageMonitor);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在同级路径下打开ImsSmsDispatcher.java我们会发现,ImsSmsDispatcher持有了CdmaSMSDispatcher、GsmSMSDispatcher这两个对象的实例:

private SMSDispatcher mCdmaDispatcher;
private SMSDispatcher mGsmDispatcher;
  • 1
  • 2
  • 1
  • 2

通过判断网络制式,分别调用mCdmaDispatcher或者mGsmDispatcher的sendText()方法。 
判断网络制式:

 /*** Determines whether or not to use CDMA format for MO SMS. If SMS over IMS* is supported, then format is based on IMS SMS format, otherwise format is* based on current phone type.** @return true if Cdma format should be used for MO SMS, false otherwise.*/private boolean isCdmaMo(){if (!isIms() || !shouldSendSmsOverIms()){// Either IMS is not registered or there is an active 1x voice call// while on eHRPD, use Voice technology to determine SMS format.return (PhoneConstants.PHONE_TYPE_CDMA == mPhone.getPhoneType());}// IMS is registered with SMS supportreturn isCdmaFormat(mImsSmsFormat);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

这儿我们以mGsmDispatcher作为下一步讲解对象,继续研究短信的发送。在frameworks/opt/telephony/src/java/com/android/internal/telephony/gsm路径下打开GsmSMSDispatcher,找到sendText()方法,如下:

@Overrideprotected void sendText(String destAddr, String scAddr, String text,PendingIntent sentIntent,PendingIntent deliveryIntent,Uri messageUri, String callingPkg, int priority,boolean isExpectMore, int validityPeriod){SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(scAddr, destAddr,text,(deliveryIntent != null), validityPeriod);if (pdu != null){HashMap map = getSmsTrackerMap(destAddr, scAddr, text, pdu);SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent,getFormat(),messageUri, isExpectMore,text /* fullMessageText */, true /* isText */,validityPeriod);String carrierPackage = getCarrierAppPackageName();if (carrierPackage != null){Rlog.d(TAG, "Found carrier package.");TextSmsSender smsSender = new TextSmsSender(tracker);smsSender.sendSmsByCarrierApp(carrierPackage,new SmsSenderCallback(smsSender));} else{Rlog.v(TAG, "No carrier package.");sendRawPdu(tracker);}} else{Rlog.e(TAG,"GsmSMSDispatcher.sendText(): getSubmitPdu() returned null");}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

我们会发现,首先将短信组装成pdu格式,然后进一步将pdu组装成为一个SmsTracker ,getCarrierAppPackageName():获取手机内置的载体app,一般手机厂商都不会内置这个,所以一般都会走sendRawPdu()这个方法。这个方法在父类SMSDispatcher 中,所以我们又得去父类看:

protected void sendRawPdu(SmsTracker tracker){HashMap map = tracker.mData;byte pdu[] = (byte[]) map.get("pdu");if (mSmsSendDisabled){Rlog.e(TAG, "Device does not support sending sms.");tracker.onFailed(mContext, RESULT_ERROR_NO_SERVICE, 0/* errorCode */);return;}if (pdu == null){Rlog.e(TAG, "Empty PDU");tracker.onFailed(mContext, RESULT_ERROR_NULL_PDU, 0/* errorCode */);return;}PendingIntent sentIntent = tracker.mSentIntent;// Get calling app package name via UID from Binder callPackageManager pm = mContext.getPackageManager();int callingUid = Binder.getCallingUid();// Special case: We're being proxied by the telephony stack itself,// so use the intent generator's UID if one existsString[] packageNames;if (callingUid == android.os.Process.PHONE_UID && sentIntent != null&& sentIntent.getCreatorPackage() != null){packageNames = new String[]{ sentIntent.getCreatorPackage() };} else{packageNames = pm.getPackagesForUid(callingUid);}if (packageNames == null || packageNames.length == 0){// Refuse to send SMS if we can't get the calling package name.Rlog.e(TAG,"Can't get calling app package name: refusing to send SMS");tracker.onFailed(mContext, RESULT_ERROR_GENERIC_FAILURE, 0/* errorCode */);return;}// Get package info via packagemanagerPackageInfo appInfo;try{// XXX this is lossy- apps can share a UIDappInfo = pm.getPackageInfo(packageNames[0],PackageManager.GET_SIGNATURES);} catch (PackageManager.NameNotFoundException e){Rlog.e(TAG,"Can't get calling app package info: refusing to send SMS");tracker.onFailed(mContext, RESULT_ERROR_GENERIC_FAILURE, 0/* errorCode */);return;}// checkDestination() returns true if the destination is not a premium// short code or the// sending app is approved to send to short codes. Otherwise, a message// is sent to our// handler with the SmsTracker to request user confirmation before// sending.if (checkDestination(tracker)){// check for excessive outgoing SMS usage by this appif (!mUsageMonitor.check(appInfo.packageName, SINGLE_PART_SMS)){sendMessage(obtainMessage(EVENT_SEND_LIMIT_REACHED_CONFIRMATION, tracker));return;}sendSms(tracker);}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

从上面可以得知,先从tracker中取出data,之后再取出pdu。 
判断短信发送是否可用 
pdu是否为空。 
checkDestination(tracker):判断内置的short code和发送的short code是否一致。 
然后调用GsmSMSDispatcher的sendSms()方法:

@Overrideprotected void sendSms(SmsTracker tracker){HashMap<String, Object> map = tracker.mData;byte pdu[] = (byte[]) map.get("pdu");if (tracker.mRetryCount > 0){Rlog.d(TAG, "sendSms: " + " mRetryCount=" + tracker.mRetryCount+ " mMessageRef=" + tracker.mMessageRef + " SS="+ mPhone.getServiceState().getState());// per TS 23.040 Section 9.2.3.6: If TP-MTI SMS-SUBMIT (0x01) type// TP-RD (bit 2) is 1 for retry// and TP-MR is set to previously failed sms TP-MRif (((0x01 & pdu[0]) == 0x01)){pdu[0] |= 0x04; // TP-RDpdu[1] = (byte) tracker.mMessageRef; // TP-MR}}Rlog.d(TAG, "sendSms: " + " isIms()=" + isIms() + " mRetryCount="+ tracker.mRetryCount + " mImsRetry=" + tracker.mImsRetry+ " mMessageRef=" + tracker.mMessageRef + " SS="+ mPhone.getServiceState().getState());sendSmsByPstn(tracker);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

可以看到上面需要对短信进行一个处理,判断重试次数,如果大于0就将第一个字节赋值为0x04 至于为什么是0x04我也不知道, 
TP-RD:是否拒绝相同重复消息。 
TP-MR:消息基准值。 
现在来看GsmSMSDispatcher的sendSmsByPstn()方法又干了些什么事情。

@Overrideprotected void sendSmsByPstn(SmsTracker tracker){int ss = mPhone.getServiceState().getState();// if sms over IMS is not supported on data and voice is not// available...if (!isIms() && ss != ServiceState.STATE_IN_SERVICE){tracker.onFailed(mContext, getNotInServiceError(ss), 0/* errorCode */);return;}HashMap<String, Object> map = tracker.mData;byte smsc[] = (byte[]) map.get("smsc");byte[] pdu = (byte[]) map.get("pdu");Message reply = obtainMessage(EVENT_SEND_SMS_COMPLETE, tracker);// sms over gsm is used:// if sms over IMS is not supported AND// this is not a retry case after sms over IMS failed// indicated by mImsRetry > 0if (0 == tracker.mImsRetry && !isIms()){if (tracker.mRetryCount > 0){// per TS 23.040 Section 9.2.3.6: If TP-MTI SMS-SUBMIT (0x01)// type// TP-RD (bit 2) is 1 for retry// and TP-MR is set to previously failed sms TP-MRif (((0x01 & pdu[0]) == 0x01)){pdu[0] |= 0x04; // TP-RDpdu[1] = (byte) tracker.mMessageRef; // TP-MR}}if (tracker.mRetryCount == 0 && tracker.mExpectMore){mCi.sendSMSExpectMore(IccUtils.bytesToHexString(smsc),IccUtils.bytesToHexString(pdu), reply);} else{mCi.sendSMS(IccUtils.bytesToHexString(smsc),IccUtils.bytesToHexString(pdu), reply);}} else{mCi.sendImsGsmSms(IccUtils.bytesToHexString(smsc),IccUtils.bytesToHexString(pdu), tracker.mImsRetry,tracker.mMessageRef, reply);// increment it here, so in case of SMS_FAIL_RETRY over IMS// next retry will be sent using IMS request again.tracker.mImsRetry++;}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52

先是对手机状态进行判断,然后又将pdu取出来,然后弄了一个发送完成的消息。 
isIms():判断IMS是否注册,和SMS是否支持。自此就开始调用mCi了,也就是CommandsInterface类了,这个类在/frameworks/opt/telephony/src/java/com/android/internal/telephony路径下,这个就进入额RIL层了,也就不是我研究的范围了。其实在我修改过中是没有修改到这一层,基本修改操作基本都是在调用sendRawPdu()方法之前完成。是不是已经脑壳都看糊了?好了,下面我们来看一张图,总结一下,短信发送的流程! 

自此,希望大家能对短信的发送有个大体的认识,如果需要详细的了解短信发送的每个细节,可以下载源代码观看。结合源代码,观看本文效果更佳!

原文地址: http://blog.csdn.net/poison_h/article/details/50972149

Android开发之Android5.1.1(CM12.1)源码中短信发送流程解析相关推荐

  1. Android开发之Theme、Style探索及源码浅析

    1 背景 前段时间群里有伙伴问到了关于Android开发中Theme与Style的问题,当然,这类东西在网上随便一搜一大把模板,所以关于怎么用的问题我想这里也就不做太多的说明了,我们这里把重点放在理解 ...

  2. Android 开发之Windows环境下Android Studio安装和使用教程(图文详细步骤)

    鉴于谷歌最新推出的Android Studio备受开发者的推崇,所以也跟着体验一下. 一.介绍Android Studio  Android Studio 是一个Android开发环境,基于Intel ...

  3. android开发之Intent.setFlags()_让Android点击通知栏信息后返回正在运行的程序

    android开发之Intent.setFlags()_让Android点击通知栏信息后返回正在运行的程序   在应用里使用了后台服务,并且在通知栏推送了消息,希望点击这个消息回到activity, ...

  4. Android开发之TextView高级应用

    Android开发之TextView高级应用 我们平时使用TextView往往让它作为一个显示文字的容器,但TextView的功能并不局限于此.以下就和大家分享一下TextView的一些使用技巧. A ...

  5. android之json解析优化,Android开发之json解析

    目前正在尝试着写app,发现看懂代码和能写出来差距很大,最关键的是java基础比较的差,因为只会python,java基础只学习了一个礼拜就过了.感觉java写出来的代码不如python简单明了. 上 ...

  6. android注册弹窗,Android开发之PopupWindow创建弹窗、对话框的方法详解

    本文实例讲述了Android开发之PopupWindow创建弹窗.对话框的方法.分享给大家供大家参考,具体如下: 简介: PopupWindow 可创建类似对话框风格的窗口 效果: 使用方法: 使用P ...

  7. Android开发之旅:组件生命周期(二)

    引言 应用程序组件有一个生命周期--一开始Android实例化他们响应意图,直到结束实例被销毁.在这期间,他们有时候处于激活状态,有时候处于非激活状态:对于活动,对用户有时候可见,有时候不可见.组件生 ...

  8. Android开发之SpannableString具体解释

    在实际的应用开发过程中常常会遇到.在文本的不同部分显示一些不同的字体风格的信息如:文本的字体.大小.颜色.样式.以及超级链接等. 普通情况下,TextView中的文本都是一个样式.对于类似的情况.能够 ...

  9. Android开发之2048安卓版

    之前是在eclipse上写的,后面换成了android sudio. 2048游戏的UI整体可以采用线性布局,即LinearLayout,其中嵌套一个线性布局和一个GridLayout,内嵌的线性布局 ...

最新文章

  1. jsp写入mysql数据库时出现乱码
  2. 自定义Activity的启动关闭动画
  3. python学习-练习题9*9乘法表巩固
  4. Python中调用Linux命令并获取返回值
  5. 4代hiv检测50元_闲置甩干机卖了50元!大爷突然想起:里面还有4根金条
  6. CCIE-LAB-第十二篇-EIGRP+EIGRP末节区域+leak map+分发列表
  7. 特斯拉地图数据服务以后由百度地图提供
  8. java connection 单例_Java设计模式之单例模式详解
  9. python透明图片合并_如何使用PIL将透明png图像与另一个图像合并
  10. 通过minicom传送文件的相关配置及使用方法
  11. 腾讯内部出品Android编程入门教程,快快收藏吧!!!
  12. DDoS原理、分类与防御
  13. 从零开始学统计 05 | 技术重复和生物学重复
  14. BeautifulSoup 使用经验总结
  15. rancher添加镜像库
  16. base scroll横向滚动_微信小程序实践:2.3 可滚动的容器组件之 scroll-view
  17. 数字联盟可信ID 3.0 正式上线升级 用真实数据助力企业增长
  18. 质量控制新七工具:亲和图、过程决策程序图、关联图、树形图、优先矩阵、活动网络图和矩阵图
  19. mysql报错1357_mysql8 参考手册--错误代码1343-1367
  20. 简单明了的LSTM/GRU应用实例(Tensorflow版)

热门文章

  1. distinct的用法
  2. 分数运算C++代码实现
  3. 『转载』|收藏|一张看懂AI知识体系
  4. MTM:matlab实现2参数解析
  5. Python学习笔记:错误,测试,调试(起)
  6. spring_整体系统
  7. 最长重复子串(Rabin-Karp算法)
  8. [云炬创业基础笔记] 第四章测试9
  9. 科大星云诗社动态20201123
  10. linux卸载python3.6,当python3.6位于/usr/local/bin/python3.6时,如何在ubuntu上卸载它