Android 5.1 Phone MO(去电)流程分析(Framework层)

2024-02-22 19:59

本文主要是介绍Android 5.1 Phone MO(去电)流程分析(Framework层),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

写在前面的话

本文主要分析MO(去电)的流程,研究的代码是Android 5.1的,目前只关注Framework层,以CDMA为例。
关于应用层的流程,请看《Android 5.1 Phone MO(去电)流程分析(应用层) 》


这里写图片描述
(如果图片看不清的话,可以右键选择在新标签中打开图片,或者把图片另存到自己电脑再查看。)


http://blog.csdn.net/linyongan


步骤1和2:紧接着应用层的流程,在 TelephonyConnectionService.java 的placeOutgoingConnection方法里调用了phone.dial(),我们这里研究的是CDMA,所以下面会进入 CDMAPhone.java 的dial方法:

public Connection dial (String dialString, int videoState, Bundle extras){...return dialInternal(dialString, null, videoState);
}protected Connection dialInternal (String dialString, UUSInfo uusInfo,int videoState) throws CallStateException {// Need to make sure dialString gets parsed properlyString newDialString = PhoneNumberUtils.stripSeparators(dialString);return mCT.dial(newDialString);
}

步骤3和4: mCT是CdmaCallTracker类型的,所以我们进入 CdmaCallTracker.java 的dial方法里:

/*** clirMode is one of the CLIR_ constants*/
Connection dial (String dialString, int clirMode) throws CallStateException {// note that this triggers call state changed notifclearDisconnected();if (!canDial()) {throw new CallStateException("cannot dial in current state");}String origNumber = dialString;String operatorIsoContry = mPhone.getSystemProperty(TelephonyProperties.PROPERTY_OPERATOR_ISO_COUNTRY, "");String simIsoContry = mPhone.getSystemProperty(TelephonyProperties.PROPERTY_ICC_OPERATOR_ISO_COUNTRY, "");boolean internationalRoaming = !TextUtils.isEmpty(operatorIsoContry)&& !TextUtils.isEmpty(simIsoContry)&& !simIsoContry.equals(operatorIsoContry);if (internationalRoaming) {if ("us".equals(simIsoContry)) {internationalRoaming = internationalRoaming && !"vi".equals(operatorIsoContry);} else if ("vi".equals(simIsoContry)) {internationalRoaming = internationalRoaming && !"us".equals(operatorIsoContry);}}if (internationalRoaming) {dialString = convertNumberIfNecessary(mPhone, dialString);}String inEcm=SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE, "false");boolean isPhoneInEcmMode = inEcm.equals("true");boolean isEmergencyCall =PhoneNumberUtils.isLocalEmergencyNumber(mPhone.getContext(), dialString);// Cancel Ecm timer if a second emergency call is originating in Ecm modeif (isPhoneInEcmMode && isEmergencyCall) {handleEcmTimer(CDMAPhone.CANCEL_ECM_TIMER);}// We are initiating a call therefore even if we previously// didn't know the state (i.e. Generic was true) we now know// and therefore can set Generic to false.mForegroundCall.setGeneric(false);// The new call must be assigned to the foreground call.// That call must be idle, so place anything that's// there on holdif (mForegroundCall.getState() == CdmaCall.State.ACTIVE) {return dialThreeWay(dialString);}mPendingMO = new CdmaConnection(mPhone.getContext(), checkForTestEmergencyNumber(dialString),this, mForegroundCall);mHangupPendingMO = false;if ( mPendingMO.getAddress() == null || mPendingMO.getAddress().length() == 0|| mPendingMO.getAddress().indexOf(PhoneNumberUtils.WILD) >= 0 ) {// Phone number is invalidmPendingMO.mCause = DisconnectCause.INVALID_NUMBER;// handlePollCalls() will notice this call not present// and will mark it as dropped.pollCallsWhenSafe();} else {// Always unmute when initiating a new callsetMute(false);// Check data calldisableDataCallInEmergencyCall(dialString);// In Ecm mode, if another emergency call is dialed, Ecm mode will not exit.if(!isPhoneInEcmMode || (isPhoneInEcmMode && isEmergencyCall)) {mCi.dial(mPendingMO.getAddress(), clirMode, obtainCompleteMessage());} else {mPhone.exitEmergencyCallbackMode();mPhone.setOnEcbModeExitResponse(this,EVENT_EXIT_ECM_RESPONSE_CDMA, null);mPendingCallClirMode=clirMode;mPendingCallInEcm=true;}}if (mNumberConverted) {mPendingMO.setConverted(origNumber);mNumberConverted = false;}//更新phone状态updatePhoneState();//发起phone状态变化通知mPhone.notifyPreciseCallStateChanged();//返回通话连接return mPendingMO;}/*** Obtain a message to use for signalling "invoke getCurrentCalls() when* this operation and all other pending operations are complete*/private Message obtainCompleteMessage() {return obtainCompleteMessage(EVENT_OPERATION_COMPLETE);}/*** Obtain a message to use for signalling "invoke getCurrentCalls() when* this operation and all other pending operations are complete*/private Message obtainCompleteMessage(int what) {mPendingOperations++;mLastRelevantPoll = null;mNeedsPoll = true;if (DBG_POLL) log("obtainCompleteMessage: pendingOperations=" +mPendingOperations + ", needsPoll=" + mNeedsPoll);return obtainMessage(what);
}

步骤5:先看 obtainCompleteMessage 方法,这里调用obtainCompleteMessage带一个参数的方法创建了一个消息类型为EVENT_OPERATION_COMPLETE的Message,这个Message也会传入到mCi.dial方法里,在这里是CdmaCallTracker主动向RILJ(mCi是RILJ的实例对象,后面再解释为什么)发送消息,等RILJ处理完之后通过回调通知CdmaCallTracker,CdmaCallTracker就在handleMessage方法处理。


知识点解析:至于 mCi ,它是CommandsInterface类型的,在CdmaCallTracker的构造方法里通过mCi = phone.mCi;获取:

public CdmaCallTracker(CDMAPhone phone) {...mCi = phone.mCi;...
}

phone是CDMAPhone类型的,在CDMAPhone的构造方法里并没有关于mCi的定义和创建代码,只有super这一行有联系

public CDMAPhone(Context context, CommandsInterface ci, PhoneNotifier notifier,boolean unitTestMode) {super("CDMA", notifier, context, ci, unitTestMode);...
}

我们再进入CDMAPhone的父类PhoneBase的构造方法里,

protected PhoneBase(String name, PhoneNotifier notifier, Context context,CommandsInterface ci,boolean unitTestMode) {...mCi = ci;...
}      

找到mCi的定义和初始化信息:mCi = ci;ci是在CDMAPhone的构造方法里的传递对象,因此,如果找到CDMAPhone创建phone对象的地方,也许可以找到ci的创建。
我们要找CDMAPhone被调用的地方,(在Source Insight这个编译器中,选中CDMAPhone,然后按快捷键Ctrl+/),我们找到 PhoneFactory.java 的makeDefaultPhone方法里:

 public static void makeDefaultPhone(Context context) {...sCommandsInterface = new RIL(context, networkMode, cdmaSubscription);UiccController.make(context, sCommandsInterface);int phoneType = TelephonyManager.getPhoneType(networkMode);if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {Rlog.i(LOG_TAG, "Creating GSMPhone");sProxyPhone = new PhoneProxy(new GSMPhone(context,sCommandsInterface, sPhoneNotifier));}
}   

在创建Phone对象时,传入RILJ类型的sCommandsInterface对象作为参数,因此,我们可以确定CdmaCallTracker.java中使用mCi.dial进行拨号,其实就是调用了 RIL.java 的dial方法。


步骤6: RIL.java 的dial方法

public void dial(String address, int clirMode, UUSInfo uusInfo, Message result) {RILRequest rr = RILRequest.obtain(RIL_REQUEST_DIAL, result);rr.mParcel.writeString(address);rr.mParcel.writeInt(clirMode);if (uusInfo == null) {rr.mParcel.writeInt(0); // UUS information is absent} else {rr.mParcel.writeInt(1); // UUS information is presentrr.mParcel.writeInt(uusInfo.getType());rr.mParcel.writeInt(uusInfo.getDcs());      rr.mParcel.writeByteArray(uusInfo.getUserData());}//打印日志if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));send(rr); 
}

打印出来的log:

08-11 09:39:24.313 D/RILJ    ( 2904): [5503]> DIAL

在这里创建了RILRequest 对象rr,将CdmaCallTracker传递过来的消息类型为EVENT_OPERATION_COMPLETE的Message赋值给rr.mResult,然后RILJ向RILD发送了RIL_REQUEST_DIAL请求。
步骤7:等底层处理完之后,会发送RIL_REQUEST_DIAL消息给RILJ,在RILJ的 processSolicited 方法里接收并处理该消息:

private RILRequest processSolicited (Parcel p) {...case RIL_REQUEST_DIAL: ret =  responseVoid(p); break;...//打印log日志if (RILJ_LOGD) riljLog(rr.serialString() + "< " + requestToString(rr.mRequest)+ " " + retToString(rr.mRequest, ret));if (rr.mResult != null) {AsyncResult.forMessage(rr.mResult, null, tr);rr.mResult.sendToTarget();//发出handler消息通知
}

步骤8,9,10: rr.mResult 就是在CdmaCallTracker中创建的消息类型为EVENT_OPERATION_COMPLETE的Message,所以调用sendToTarget方法,就会把消息发送给CdmaCallTracker,然后在 CdmaCallTracker.java 的handleMessage方法中有对EVENT_OPERATION_COMPLETE的逻辑处理:

public voidhandleMessage (Message msg) {...switch (msg.what) {case EVENT_OPERATION_COMPLETE:operationComplete();break;...
}private void operationComplete() {mPendingOperations--;if (DBG_POLL) log("operationComplete: pendingOperations=" +mPendingOperations + ", needsPoll=" + mNeedsPoll);if (mPendingOperations == 0 && mNeedsPoll) {mLastRelevantPoll = obtainMessage(EVENT_POLL_CALLS_RESULT);mCi.getCurrentCalls(mLastRelevantPoll);} else if (mPendingOperations < 0) {// this should never happenRlog.e(LOG_TAG,"CdmaCallTracker.pendingOperations < 0");mPendingOperations = 0;}
}

步骤11和12:在这里通过 obtainMessage 方法生成一个消息类型为EVENT_POLL_CALLS_RESULT的Message并且作为getCurrentCalls方法的参数传递到mCi,mCi是RIL.java的实例对象,这也就回到了 RIL.java 的getCurrentCalls方法里,getCurrentCalls将RIL_REQUEST_GET_CURRENT_CALLS 消息封装成RILRequest 类型并发送。

public void getCurrentCalls (Message result) {//注意rr对象的消息类型,后面会用到RILRequest rr = RILRequest.obtain(RIL_REQUEST_GET_CURRENT_CALLS, result);//打印log日志if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));send(rr);
}

打印出相应的log是

09-09 17:32:02.179 D/RILJ    ( 2795): [5656]> GET_CURRENT_CALLS

步骤13: RILJ向底层请求查询Call List状态列表,等底层处理完之后,就把结果返回给RILJ,向RILJ发送RIL_REQUEST_GET_CURRENT_CALLS消息,RILJ在 processSolicited 方法里处理它。

private RILRequest processSolicited (Parcel p) {...case RIL_REQUEST_GET_CURRENT_CALLS: ret =  responseCallList(p); break;...//打印log日志if (RILJ_LOGD) riljLog(rr.serialString() + "< " + requestToString(rr.mRequest)+ " " + retToString(rr.mRequest, ret));if (rr.mResult != null) {AsyncResult.forMessage(rr.mResult, null, tr);rr.mResult.sendToTarget();//发出handler消息通知
}

步骤14和15:还是跟上面一样, rr.mResult 就是刚刚在CdmaCallTracker中创建的那个消息类型为EVENT_POLL_CALLS_RESULT的Message,所以在 CdmaCallTracker.java 的handleMessage 方法中会有EVENT_POLL_CALLS_RESULT的逻辑处理

public voidhandleMessage (Message msg) {...switch (msg.what) {case EVENT_POLL_CALLS_RESULT:{//打印log日志Rlog.d(LOG_TAG, "Event EVENT_POLL_CALLS_RESULT Received");ar = (AsyncResult)msg.obj;if(msg == mLastRelevantPoll) {if(DBG_POLL) log("handle EVENT_POLL_CALL_RESULT: set needsPoll=F");mNeedsPoll = false;mLastRelevantPoll = null;handlePollCalls((AsyncResult)msg.obj);}}break;...
}

步骤16:最后,会进入 handlePollCalls 方法,关于handlePollCalls方法,《 handlePollCalls方法详解 》这篇文章讲得比较详细,对于我们这里,是打电话流程中,第一次查询Call List列表,所以会进入这里:

//conn代表旧的通话连接的基本信息,dc代表新的。出现新的通话连接
if (conn == null && dc != null) {//主动发起拨号请求后,第一次查询到Call List后,进入这里// Connection appeared in CLCC response that we don't know aboutif (mPendingMO != null && mPendingMO.compareTo(dc)) {if (DBG_POLL) log("poll: pendingMO=" + mPendingMO);// It's our pending mobile originating callmConnections[i] = mPendingMO;//把i的值赋值给indexmPendingMO.mIndex = i;mPendingMO.update(dc);mPendingMO = null;// Someone has already asked to hangup this callif (mHangupPendingMO) {mHangupPendingMO = false;// Re-start Ecm timer when an uncompleted emergency call endsif (mIsEcmTimerCanceled) {handleEcmTimer(CDMAPhone.RESTART_ECM_TIMER);}try {if (Phone.DEBUG_PHONE) log("poll: hangupPendingMO, hangup conn " + i);hangup(mConnections[i]);} catch (CallStateException ex) {Rlog.e(LOG_TAG, "unexpected error on hangup");}// Do not continue processing this poll// Wait for hangup and repollreturn;}
} 

好像也没有多大作用,就是把mPendingMO的值赋值给mConnections和把i的值赋值给mIndex 。

步骤17和18:接着,底层又会上报RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED消息给RILJ,这是底层主动上报的消息,所以RILJ会在 processUnsolicited 方法里进行处理

private void processUnsolicited (Parcel p) {...try {switch(response) {...case RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED: ret =  responseVoid(p); break;...}switch(response) {...case RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED:if (RILJ_LOGD) unsljLog(response);//打印log日志//发出通知(RegistrantList消息处理机制)mCallStateRegistrants.notifyRegistrants(new AsyncResult(null, null, null));...}
}

(在Source Insight这个编译器中,选中mCallStateRegistrants,然后按快捷键Ctrl+/),我们找到BaseCommands.java的registerForCallStateChanged方法,接着继续找它的调用者,最后我们来到CdmaCallTracker.java的构造方法里

public CdmaCallTracker(CDMAPhone phone) {...mCi.registerForCallStateChanged(this, EVENT_CALL_STATE_CHANGE, null);...
}

发觉是CdmaCallTracker.java向RIL注册了一个EVENT_CALL_STATE_CHANGE类型的Handler消息。
步骤19,20,21:因此,我们在 CdmaCallTracker.java 的handleMessage方法里可以找到响应EVENT_CALL_STATE_CHANGE消息类型的处理逻辑,如下:

 public voidhandleMessage (Message msg) {...case EVENT_CALL_STATE_CHANGE://调用父类CallTracker查询Call List方法pollCallsWhenSafe();break;...
}protected void pollCallsWhenSafe() {...if (checkNoOperationsPending()) {//注意mLastRelevantPoll对象的消息类型,后面会用到mLastRelevantPoll = obtainMessage(EVENT_POLL_CALLS_RESULT);mCi.getCurrentCalls(mLastRelevantPoll);}
}

步骤22和23:又是RILJ请求查询Call List状态列表。。。老样子,查完还是交给 CdmaCallTracker去处理。这时候Call的状态还是DIALING
步骤24~27:等Call的状态变成ACTIVE后,底层又会上报RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED消息给RILJ,然后RILJ又请求查询查询Call List状态列表,CdmaCallTracker又处理。。。
到这里,电话已接通,打电话的流程就这么多了。

下面贴出打电话流程的log片段

09-09 17:32:01.730 D/RILJ    ( 2795): [5655]> DIAL
09-09 17:32:01.810 D/RILJ    ( 2795): [5655]< DIAL 
09-09 17:32:01.935 D/RILJ    ( 2795): [UNSL]< UNSOL_RESPONSE_CALL_STATE_CHANGED
09-09 17:32:02.179 D/RILJ    ( 2795): [5656]> GET_CURRENT_CALLS
09-09 17:32:02.183 V/RILJ    ( 2795): Incoming UUS : NOT present!
09-09 17:32:02.183 D/RILJ    ( 2795): InCall VoicePrivacy is disabled
09-09 17:32:02.183 D/RILJ    ( 2795): [5656]< GET_CURRENT_CALLS  
[id=1,DIALING,toa=129,norm,mo,0,voc,noevp,,cli=1,,0] 
09-09 17:32:02.196 D/RILJ    ( 2795): [5657]> GET_CURRENT_CALLS
09-09 17:32:02.201 V/RILJ    ( 2795): Incoming UUS : NOT present!
09-09 17:32:02.201 D/RILJ    ( 2795): InCall VoicePrivacy is disabled
09-09 17:32:02.201 D/RILJ    ( 2795): [5657]< GET_CURRENT_CALLS  
[id=1,DIALING,toa=129,norm,mo,0,voc,noevp,,cli=1,,0] 
09-09 17:32:02.306 D/CdmaCallTracker( 2795): Event EVENT_POLL_CALLS_RESULT Received
09-09 17:32:02.306 D/CdmaCallTracker( 2795): Event EVENT_POLL_CALLS_RESULT Received
09-09 17:32:03.607 D/RILJ    ( 2795): [UNSL]< UNSOL_RESPONSE_CALL_STATE_CHANGED
09-09 17:32:03.686 D/RILJ    ( 2795): [5658]> GET_CURRENT_CALLS
09-09 17:32:03.688 D/RILJ    ( 2795): [5659]> GET_CURRENT_CALLS
09-09 17:32:03.691 D/RilRequest( 2795): [5659]< GET_CURRENT_CALLS 
error: com.android.internal.telephony.CommandException: GENERIC_FAILURE ret=
09-09 17:32:03.696 D/RILJ    ( 2795): [5658]< GET_CURRENT_CALLS  
[id=1,ACTIVE,toa=129,norm,mo,0,voc,noevp,,cli=1,,0] 
09-09 17:32:03.745 D/CdmaCallTracker( 2795): Event EVENT_POLL_CALLS_RESULT Received
09-09 17:32:03.745 D/CdmaCallTracker( 2795): Event EVENT_POLL_CALLS_RESULT Received
09-09 17:32:03.995 D/RILJ    ( 2795): [5660]> GET_CURRENT_CALLS
09-09 17:32:04.002 D/RILJ    ( 2795): [5660]< GET_CURRENT_CALLS  
[id=1,ACTIVE,toa=129,norm,mo,0,voc,noevp,,cli=1,,0] 
09-09 17:32:04.002 D/CdmaCallTracker( 2795): Event EVENT_POLL_CALLS_RESULT Received
09-09 17:32:04.004 D/CallStateMonitor( 2795): handleMessage(10)
09-09 17:32:04.004 D/CallNotifier( 2795): PHONE_ENHANCED_VP_OFF...
09-09 17:32:04.005 D/CallStateMonitor( 2795): handleMessage(1)

补充:看到上面的log中出现了GET_CURRENT_CALLS
error,对于这种情况的后续处理,请查看《“RILJ多次发出GET_CURRENT_CALLS请求”问题分析 》

这篇关于Android 5.1 Phone MO(去电)流程分析(Framework层)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/736338

相关文章

[职场] 公务员的利弊分析 #知识分享#经验分享#其他

公务员的利弊分析     公务员作为一种稳定的职业选择,一直备受人们的关注。然而,就像任何其他职业一样,公务员职位也有其利与弊。本文将对公务员的利弊进行分析,帮助读者更好地了解这一职业的特点。 利: 1. 稳定的职业:公务员职位通常具有较高的稳定性,一旦进入公务员队伍,往往可以享受到稳定的工作环境和薪资待遇。这对于那些追求稳定的人来说,是一个很大的优势。 2. 薪资福利优厚:公务员的薪资和

工作流Activiti初体验—流程撤回【二】

已经玩工作流了,打算还是研究一下撤回的功能。但是流程图里面并不带撤回的组件,所以需要自己动态改造一下,还是延续上一个流程继续试验撤回功能。《工作流Activiti初体验【一】》 完整流程图 我们研究一下分发任务撤回到发起任务,其他环节的撤回类似 撤回的原理大概如下: 将分发任务后面的方向清空,把发起任务拼接到原来的判断网关,然后结束分发任务,这样流程就到发起任务了 此时的流程如上图,

ROS话题通信流程自定义数据格式

ROS话题通信流程自定义数据格式 需求流程实现步骤定义msg文件编辑配置文件编译 在 ROS 通信协议中,数据载体是一个较为重要组成部分,ROS 中通过 std_msgs 封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty… 但是,这些数据一般只包含一个 data 字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,比如:

Eclipse+ADT与Android Studio开发的区别

下文的EA指Eclipse+ADT,AS就是指Android Studio。 就编写界面布局来说AS可以边开发边预览(所见即所得,以及多个屏幕预览),这个优势比较大。AS运行时占的内存比EA的要小。AS创建项目时要创建gradle项目框架,so,创建项目时AS比较慢。android studio基于gradle构建项目,你无法同时集中管理和维护多个项目的源码,而eclipse ADT可以同时打开

android 免费短信验证功能

没有太复杂的使用的话,功能实现比较简单粗暴。 在www.mob.com网站中可以申请使用免费短信验证功能。 步骤: 1.注册登录。 2.选择“短信验证码SDK” 3.下载对应的sdk包,我这是选studio的。 4.从头像那进入后台并创建短信验证应用,获取到key跟secret 5.根据技术文档操作(initSDK方法写在setContentView上面) 6.关键:在有用到的Mo

android一键分享功能部分实现

为什么叫做部分实现呢,其实是我只实现一部分的分享。如新浪微博,那还有没去实现的是微信分享。还有一部分奇怪的问题:我QQ分享跟QQ空间的分享功能,我都没配置key那些都是原本集成就有的key也可以实现分享,谁清楚的麻烦详解下。 实现分享功能我们可以去www.mob.com这个网站集成。免费的,而且还有短信验证功能。等这分享研究完后就研究下短信验证功能。 开始实现步骤(新浪分享,以下是本人自己实现

Android我的二维码扫描功能发展史(完整)

最近在研究下二维码扫描功能,跟据从网上查阅的资料到自己勉强已实现扫描功能来一一介绍我的二维码扫描功能实现的发展历程: 首页通过网络搜索发现做android二维码扫描功能看去都是基于google的ZXing项目开发。 2、搜索怎么使用ZXing实现自己的二维码扫描:从网上下载ZXing-2.2.zip以及core-2.2-source.jar文件,分别解压两个文件。然后把.jar解压出来的整个c

android 带与不带logo的二维码生成

该代码基于ZXing项目,这个网上能下载得到。 定义的控件以及属性: public static final int SCAN_CODE = 1;private ImageView iv;private EditText et;private Button qr_btn,add_logo;private Bitmap logo,bitmap,bmp; //logo图标private st

Android多线程下载见解

通过for循环开启N个线程,这是多线程,但每次循环都new一个线程肯定很耗内存的。那可以改用线程池来。 就以我个人对多线程下载的理解是开启一个线程后: 1.通过HttpUrlConnection对象获取要下载文件的总长度 2.通过RandomAccessFile流对象在本地创建一个跟远程文件长度一样大小的空文件。 3.通过文件总长度/线程个数=得到每个线程大概要下载的量(线程块大小)。

Windows中,.net framework 3.5安装

安装.net framework,目前已知2种方法,如下: 一、在MSDN下载对应的安装包,安装,这种可能无法安装成功,概率很大,不成功使用第二种方法,基本上没问题。 二、win8/8.1/10 下安装 .net framework 3.5.1: 1. 打开 win8/8.1/10 安装盘(这里指系统安装镜像文件),提取 sources\sxs 文件夹到 X:\sources\sxs (X代