前面我们已经分析了android在进行数据业务拨号前,进行相关准备工作的流程,现在我们可以分析一下整个数据业务长连接拨号在框架部分的流程。

长连接的“长”,是相对于终端进行彩信发送等操作时,建立的临时数据连接而言的(这种临时数据连接在业务执行完毕后,会主动断开),是能够长时间存在的数据连接。

1 CellDataPreference
我们从点击UI界面的数据拨号开关开始分析整个流程。
在原生的Android代码中,数据开关作为设置的一部分,相关的操作定义于CellDataPreference.java中,定义于packages/apps/settings/src/com/android/settings/datausage目录下。

我们看看处理点击操作的performClick函数:

@Override
protected void performClick(View view) {.............//开关处于开启状态if (mChecked) {//当前subId对应的卡信息(卡需要处于激活状态,即相关信息已经加载)final SubscriptionInfo currentSir = mSubscriptionManager.getActiveSubscriptionInfo(mSubId);//默认数据卡对应的卡状态final SubscriptionInfo nextSir = mSubscriptionManager.getDefaultDataSubscriptionInfo();//showSimCardTile判断手机是否支持多卡,支持的话返回true//整个If的含义就是:仅支持单卡,或者默认数据卡与当前的卡信息一致if (!Utils.showSimCardTile(getContext()) || (nextSir != null && currentSir != null &&currentSir.getSubscriptionId() == nextSir.getSubscriptionId())) {//关闭数据业务(开关处于开启态,再点击一次,变成关闭态)setMobileDataEnabled(false);if (nextSir != null && currentSir != null &&currentSir.getSubscriptionId() == nextSir.getSubscriptionId()) {//双卡的情况下,还要关闭另一张卡的数据业务(当前卡为默认数据卡,这里是以防万一)disableDataForOtherSubscriptions(mSubId);}return;}............super.performClick(view);} else {//这里是从关到开的过程,多卡的情况if (Utils.showSimCardTile(getContext())) {//将标志位置为truemMultiSimDialog = true;//调用父类方法;在父类方法中最终将调用子类实现的onClick方法super.performClick(view);} else {//单卡时直接开始拨号setMobileDataEnabled(true);}}
}

从上面的代码我们可以看出,在多卡的情况下,将开关从关闭置为打开,将由CellDataPreference的onClick函数进行处理:

@Override
protected void onClick(DialogInterface dialog, int which) {if (which != DialogInterface.BUTTON_POSITIVE) {return;}//在前面的代码中,mMultiSimDialog已经置为true,表示手机支持多卡if (mMultiSimDialog) {//将当前CellDataPreference对应卡设为默认数据卡mSubscriptionManager.setDefaultDataSubId(mSubId);//开始数据拨号setMobileDataEnabled(true);//关闭另一张卡的数据业务disableDataForOtherSubscriptions(mSubId);} else {// TODO: extend to modify policy enabled flag.setMobileDataEnabled(false);}
}private void setMobileDataEnabled(boolean enabled) {if (DataUsageSummary.LOGD) Log.d(TAG, "setMobileDataEnabled(" + enabled + "," + mSubId + ")");//调用TelephonyManager的接口mTelephonyManager.setDataEnabled(mSubId, enabled);//更改界面setChecked(enabled);
}

2 TelephonyManager
根据上文的代码,我们知道设置界面最终通过调用TelephonyManager开启拨号流程。

//传入参数subId为数据卡对应的subId
//enable为true表示开启数据业务;false表示关闭数据业务
@SystemApi
public void setDataEnabled(int subId, boolean enable) {try {Log.d(TAG, "setDataEnabled: enabled=" + enable);//获取binder代理对象ITelephony telephony = getITelephony();if (telephony != null)//通过binder通信调用接口telephony.setDataEnabled(subId, enable);} catch (RemoteException e) {Log.e(TAG, "Error calling ITelephony#setDataEnabled", e);}
}private ITelephony getITelephony() {//Context.TELEPHONY_SERVICE对应字符串"phone"return ITelephony.Stub.asInterface(ServiceManager.getService(Context.TELEPHONY_SERVICE));
}

上面的代码较为简单,唯一值得关注的是找到binder通信对应的服务提供者。
实际上我们在之前的blog中已经提到过了,在PhoneApp启动时会创建PhoneGlobals,而PhoneGlobals会创建PhoneInterfaceManager:

3 PhoneInterfaceManager

.......
phoneMgr = PhoneInterfaceManager.init(this, PhoneFactory.getDefaultPhone());
......

我们来看看PhoneInterfaceManager的定义:

//可以看到PhoneInterfaceManager继承ITelephony.Stub,与前面呼应起来了
public class PhoneInterfaceManager extends ITelephony.Stub {.............static PhoneInterfaceManager init(PhoneGlobals app, Phone phone) {synchronized (PhoneInterfaceManager.class) {if (sInstance == null) {//创建PhoneInterfaceManagersInstance = new PhoneInterfaceManager(app, phone);} else {Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);}return sInstance;}}private PhoneInterfaceManager(PhoneGlobals app, Phone phone) {...........publish();}private void publish() {if (DBG) log("publish: " + this);//publish服务名为"phone",与前面对应起来了ServiceManager.addService("phone", this);}...........
}

至此,我们知道了TelephonyManager通过Binder通信调用的实际上是PhoneInterfaceManager中的接口。

@Override
public void setDataEnabled(int subId, boolean enable) {//检查权限enforceModifyPermission();//利用subId映射得到phoneIdint phoneId = mSubscriptionController.getPhoneId(subId);if (DBG) log("getDataEnabled: subId=" + subId + " phoneId=" + phoneId);//根据phoneId得到PhoneFactory中记录的phone对象Phone phone = PhoneFactory.getPhone(phoneId);if (phone != null) {if (DBG) log("setDataEnabled: subId=" + subId + " enable=" + enable);//调用phone对象的setDataEnabled方法phone.setDataEnabled(enable);} else {loge("setDataEnabled: no phone for subId=" + subId);}
}

4 GsmCdmaPhone
与之前的版本不一样,Android 7.0中新增了GsmCdmaPhone对象,并且将Phone变成了一个抽象类,新增了PhoneInternaInterface接口:

public abstract class Phone extends Handler implements PhoneInternalInterface {...........
}
public class GsmCdmaPhone extends Phone {...........
}

这种继承结构的变化带来的影响,自己目前也还没有深入研究,今后有流程涉及时,再做进一步的分析。
我们目前还是关注数据拨号流程:

@Override
public void setDataEnabled(boolean enable) {mDcTracker.setDataEnabled(enable);
}

可以看到在拨号流程中,GsmCdmaPhone的工作很简单,直接调用DcTracker的接口即可。

5 DcTracker
在Android 7.0中,去掉了DcTrackerBase对象,DcTracker直接继承Handler。

public class DcTracker extends Handler {.......public void setDataEnabled(boolean enable) {Message msg = obtainMessage(DctConstants.CMD_SET_USER_DATA_ENABLE);msg.arg1 = enable ? 1 : 0;if (DBG) log("setDataEnabled: sendMessage: enable=" + enable);//发送消息给自己,将调用onSetUserDataEnabled进行处理sendMessage(msg);}........private void onSetUserDataEnabled(boolean enabled) {synchronized (mDataEnabledLock) {//新设定的状态,与旧状态不一样时,才需要继续处理if (mUserDataEnabled != enabled) {mUserDataEnabled = enabled;// 更新数据库,注意到单、双卡更新字段的区别if (TelephonyManager.getDefault().getSimCount() == 1) {Settings.Global.putInt(mResolver, Settings.Global.MOBILE_DATA, enabled ? 1 : 0);} else {int phoneSubId = mPhone.getSubId();Settings.Global.putInt(mResolver, Settings.Global.MOBILE_DATA + phoneSubId,enabled ? 1 : 0);}//根据系统属性判断终端是否允许在漫游状态使用数据业务if (getDataOnRoamingEnabled() == false &&mPhone.getServiceState().getDataRoaming() == true) {if (enabled) {//仅为不可用的APN发送通知notifyOffApnsOfAvailability(Phone.REASON_ROAMING_ON);} else {notifyOffApnsOfAvailability(Phone.REASON_DATA_DISABLED);}}if (enabled) {//开启数据业务时,调用该函数onTrySetupData(Phone.REASON_DATA_ENABLED);} else {//关闭数据业务时,调用该函数onCleanUpAllConnections(Phone.REASON_DATA_SPECIFIC_DISABLED);}}}}..............
}

onTrySetupData的内容较为简单,直接调用setupDataOnConnectableApns:

private boolean onTrySetupData(String reason) {if (DBG) log("onTrySetupData: reason=" + reason);//顾名思义,将利用可连接的APN进行拨号setupDataOnConnectableApns(reason);return true;
}private void setupDataOnConnectableApns(String reason) {//这里RetryFailures.ALWAYS表示连网失败话,会一直重试setupDataOnConnectableApns(reason, RetryFailures.ALWAYS);
}private void setupDataOnConnectableApns(String reason, RetryFailures retryFailures) {...............//介绍Phone拨号前的准备工作时,我们已经已经mPrioritySortedApnContexts是通过解析xml文件形成的for (ApnContext apnContext : mPrioritySortedApnContexts) {//如果apnContext之前用过,不处于Idle态(apnContext初始时处于Idle态),那么按需释放对应的数据连接,这一部分我们目前不用太关注......................//注意到apnContext的isConnectable返回true时,拨号流程才能继续下去if (apnContext.isConnectable()) {log("isConnectable() call trySetupData");apnContext.setReason(reason);//首次使用的ApnContext, waitingApns为nulltrySetupData(apnContext, waitingApns);}}
}

我们看看ApnContext中的isConnectable等函数:

public boolean isConnectable() {return isReady() && ((mState == DctConstants.State.IDLE)|| (mState == DctConstants.State.SCANNING)|| (mState == DctConstants.State.RETRYING)|| (mState == DctConstants.State.FAILED));
}public boolean isReady() {//ApnContext被激活时,mDataEnabled才会变为true;mDependencyMet从配置中读出来,衡为truereturn mDataEnabled.get() && mDependencyMet.get();
}

根据前面的数据业务拨号准备工作的流程,我们知道ConnectivityService中default NetworkRequest对应类型的ApnContext被激活了,也就是Default Type的APN被激活了。
因此,我们至少有一个connectable的APN可以使用, 可以继续拨号流程。

trySetupData函数中主要根据当前终端的运行状态,判断框架是否应该继续拨号。

private boolean trySetupData(ApnContext apnContext, ArrayList<ApnSetting> waitingApns) {//判断拨号条件..........//整个判断能否拨号的过程比较复杂,涉及了很多细节//本来打算详细写一下,但写了一部分后,发现太乱了//这里就大概描述一下:其实这里就是检查之前的准备工作是否完成,主要是结合APN类型、数据能力是否激活及数据开关是否打开等,判断处能否继续拨号if (......../*满足拨号条件*/) {............if (apnContext.getState() == DctConstants.State.IDLE) {if (waitingApns == null) {//结合激活的apnContext的type,例如default类型,以及底层使用的无线技术(从servicestate获取),从Dctracker加载卡对应的所有apn中,得到可以使用的ApnwaitingApns = buildWaitingApns(apnContext.getApnType(), radioTech);}if (waitingApns.isEmpty()) {//没获取到可用apn........return false;} else {//将所有可用的apn存入apnContextapnContext.setWaitingApns(waitingApns);.............}}//继续拨号boolean retValue = setupData(apnContext, radioTech);...........return retValue;} else {//打印不允许拨号的原因.................}
}

之前写blog总想尽可能的详细,以便以后查阅,但这次真是被trySetupData打败了。回过头来想想,自己之前的想法可能确实是有问题的,对于整个Framework而言,重要的是架构和主要流程的脉络,自己过于注重细节,反而影响阅读和记录的效率。可能自己需要分析Framework的bug,因此很多时候不得不关注细节,于是产生了现在的毛病,以后行文风格要力求简洁。

现在,我们回过头来看看setupData:

private boolean setupData(ApnContext apnContext, int radioTech) {..........//用于连接DcTracker和DataConnection(后文描述)DcAsyncChannel dcac = null;//从apncontext取出可用的apnapnSetting = apnContext.getNextApnSetting();.............//得到profileId,这个需要传递给modemint profileId = apnSetting.profileId;if (profileId == 0) {profileId = getApnProfileID(apnContext.getApnType());}//不同类型Apn的profileId定义于RILConstants中//例如://public static final int DATA_PROFILE_DEFAULT   = 0;//public static final int DATA_PROFILE_TETHERED  = 1;//public static final int DATA_PROFILE_IMS       = 2;if (dcac == null) {//根据无线技术,判断是否只允许建立一个DataConnection//无线技术能否支持多个数据连接,由frameworks/base/core/res/res/values/config.xml决定if (isOnlySingleDcAllowed(radioTech)) {//当无线技术仅支持单连接时,若有高优先级的APN被激活,那么此次拨号无法继续//举例来说,就是某些无线技术下,彩信发送时,default数据不能拨号//目前原生中,仅支持单连接的无线技术为IS95A,IS95B,1xRTT,EVDO_0,EVDO_A,EVDO_Bif (isHigherPriorityApnContextActive(apnContext)) {return false;}//在仅支持单连接的情况下,拨号前需要清楚所有已建立的连接//代码走到这里,说明当前APN的优先级是最高的,需要清除低优先级的连接//举例来说,在上述无线技术下,建立彩信时,会断开已连接的default数据业务;当然彩信发送完毕后,会自动重新建立default的数据连接if (cleanUpAllConnections(true, Phone.REASON_SINGLE_PDN_ARBITRATION)) {return false;}}//判断能否复用dataConnectiondcac = findFreeDataConnection();if (dcac == null) {//不能复用则创建新的dataConnection和dcAsyncChanneldcac = createDataConnection();}..........}//下面均是更新ApnContext的状态final int generation = apnContext.incAndGetConnectionGeneration();.............apnContext.setDataConnectionAc(dcac);apnContext.setApnSetting(apnSetting);apnContext.setState(DctConstants.State.CONNECTING);Message msg = obtainMessage();//注意此msg类型;当dataConnection拨号成功后,将会返回此消息给dcTrackermsg.what = DctConstants.EVENT_DATA_SETUP_COMPLETE;msg.obj = new Pair<ApnContext, Integer>(apnContext, generation);//调用dcAsyncChannel的bringUpdcac.bringUp(apnContext, profileId, radioTech, msg, generation);if (DBG) log("setupData: initing!");return true;
}

根据上面的代码,我们知道了当DcTracker判断出拨号的准备工作OK时,将创建出DataConnection对象,然后调用DcAsyncChannel的bringUp函数。

6 DataConnection

6.1 创建过程
我们先看看DcTracker中创建DataConnection的过程:

private DcAsyncChannel createDataConnection() {..........//每个DataConnection有唯一的id号int id = mUniqueIdGenerator.getAndIncrement();//创建dataconnection,注意this为dctrackerDataConnection conn = DataConnection.makeDataConnection(mPhone, id, this, mDcTesterFailBringUpAll, mDcc);//dctracker保存dataconnectionmDataConnections.put(id, conn);//创建DcAsyncChannelDcAsyncChannel dcac = new DcAsyncChannel(conn, LOG_TAG);//其中调用AsyncChannel中的方法,完成dctracker与dataconnection之间handler的绑定;如同connectivityService与NetworkFactory的绑定一样int status = dcac.fullyConnectSync(mPhone.getContext(), this, conn.getHandler());if (status == AsyncChannel.STATUS_SUCCESSFUL) {//dctracker保存DcAsyncChannel,键值为对应dataConnection的idmDataConnectionAcHashMap.put(dcac.getDataConnectionIdSync(), dcac);} else {.........}return dcac;
}

现在我们看看makeDataConnection方法:

public static DataConnection makeDataConnection(.....) {DataConnection dc = new DataConnection(phone,"DC-" + mInstanceNumber.incrementAndGet(), id, dct, failBringUpAll, dcc);//DataConnection是个状态机,因此需要startdc.start();return dc;
}

最后,看看DataConnection的构造函数:

//继承状态机
public class DataConnection extends StateMachine {......private DataConnection(......) {...........//拨号成功后,将利用NetworkInfo构造NetworkAgent,注册到ConnectivityServicemNetworkInfo = new NetworkInfo(ConnectivityManager.TYPE_MOBILE,networkType, NETWORK_TYPE, TelephonyManager.getNetworkTypeName(networkType));...........//调用状态机中的addState方法addState(mDefaultState);//后面的状态是前面的父状态addState(mInactiveState, mDefaultState);addState(mActivatingState, mDefaultState);addState(mActiveState, mDefaultState);addState(mDisconnectingState, mDefaultState);addState(mDisconnectingErrorCreatingConnection, mDefaultState);//初始态为InactiveStatesetInitialState(mInactiveState);...........}
}

DataConnection是一个状态机,android中状态机的实现原理,以后再单独分析。这里我们只需要知道,状态机内部有自己的Handler,收到消息时由当前状态进行处理;若当前状态无法处理,则递交给父状态进行处理。当从一个状态离开时,将调用该状态的exit函数(可以是空实现);当进入到一个状态时,将调用该状态的enter函数(可以是空实现)。

6.2 DataConnection拨号
前文已经描述,在DcTracker创建完DataConnection和DcAysncChannel后,调用了DcAsyncChannel的bringUp函数:

public void bringUp(ApnContext apnContext, int profileId, int rilRadioTechnology,Message onCompletedMsg, int connectionGeneration) {//调用AsyncChannel的sendMessage方法,将消息发送给dstMessenger,也就是dataConnectionsendMessage(DataConnection.EVENT_CONNECT,new ConnectionParams(apnContext, profileId, rilRadioTechnology,onCompletedMsg, connectionGeneration));
}

6.2.1 DcInactiveState
根据DataConnection的构造函数,我们知道DataConnection初始时处于DcInactiveState,于是应该由DcInactiveState处理EVENT_CONNECT事件:

private class DcInactiveState extends State {.........@Overridepublic boolean processMessage(Message msg) {.........switch (msg.what) {...........case EVENT_CONNECT:ConnectionParams cp = (ConnectionParams) msg.obj;//判断参数的有效性if (initConnection(cp)) {//进行实际的拨号操作onConnect(mConnectionParams);//dataConnection迁移到Activating状态transitionTo(mActivatingState);} else {//通知DcTracker拨号失败notifyConnectCompleted(cp, DcFailCause.UNACCEPTABLE_NETWORK_PARAMETER, false);}break;}}
}

我们看看onConnect函数:

private void onConnect(ConnectionParams cp) {...........//拨号返回时的消息为EVENT_SETUP_DATA_CONNECTION_DONEMessage msg = obtainMessage(EVENT_SETUP_DATA_CONNECTION_DONE, cp);msg.obj = cp;...........//通过RIL将消息发送给modemmPhone.mCi.setupDataCall(cp.mRilRat,cp.mProfileId,mApnSetting.apn, mApnSetting.user, mApnSetting.password,authType,protocol, msg);
}

6.2.2 DcActivatingState
根据前面的代码,我们知道DataConnection在DcInactiveState状态,利用RIL向modem发送拨号请求后,进入到了DcActivatingState;同时,RIL收到modem拨号返回的消息后,将向DataConnection发送EVENT_SETUP_DATA_CONNECTION_DONE的消息。

根据状态机的原理,我们看看DcActivatingState处理消息的代码:

private class DcActivatingState extends State {@Overridepublic boolean processMessage(Message msg) {.........switch (msg.what) {...........case EVENT_SETUP_DATA_CONNECTION_DONE:ar = (AsyncResult) msg.obj;cp = (ConnectionParams) ar.userObj;//从RIL返回的msg中取出拨号的结果;当结果正常时,内部还调用了updateLinkProperty获取了链路信息DataCallResponse.SetupResult result = onSetupConnectionCompleted(ar);........switch (result) {case SUCCESS:mDcFailCause = DcFailCause.NONE;transitionTo(mActiveState);break;//拨号出现异常时,将根据结果判断是否需要重新拨号,还是打印log,停止拨号..............}retVal = HANDLED;break;................}}
}

从上面的代码可以看出,当底层返回拨号成功的消息后,DataConnection将进入到DcActiveState。

6.2.3 DcActiveState
DcActiveState实现了自己的enter函数,因此从DcActivatingState迁入时,首先将调用该enter函数:

private class DcActiveState extends State {@Override public void enter() {boolean createNetworkAgent = true;//如果队列中有断开连接的消息待处理,则不创建NetworkAgentif (hasMessages(EVENT_DISCONNECT) ||hasMessages(EVENT_DISCONNECT_ALL) ||hasDeferredMessages(EVENT_DISCONNECT) ||hasDeferredMessages(EVENT_DISCONNECT_ALL)) {log("DcActiveState: skipping notifyAllOfConnected()");createNetworkAgent = false;} else {//通知DcTracker拨号完成,消息为EVENT_DATA_SETUP_COMPLETEnotifyAllOfConnected(Phone.REASON_CONNECTED);}//注册监听通话的开始和结束;由于通信制式的约束,同一个phone通话时必须断开数据业务,通话结束后,再重新连接      mPhone.getCallTracker().registerForVoiceCallStarted(getHandler(),DataConnection.EVENT_DATA_CONNECTION_VOICE_CALL_STARTED, null);mPhone.getCallTracker().registerForVoiceCallEnded(getHandler(),DataConnection.EVENT_DATA_CONNECTION_VOICE_CALL_ENDED, null);//更新NetworkInfo等............if (createNetworkAgent) {//创建NetworkAgent,将注册到ConnectivityServicemNetworkAgent = new DcNetworkAgent(getHandler().getLooper(), mPhone.getContext(),"DcNetworkAgent", mNetworkInfo, makeNetworkCapabilities(), mLinkProperties,50, misc);}}.........
}

至此,与modem交互的拨号主要流程已经结束。然而,还有两件重要的事情没有做:1、通知其它APK,例如SystemUI,拨号成功;2、配置路由等,让终端可以真正的访问网络。
其中,第一件事是通过上述代码中的notifyAllOfConnected完成的,第二件事是通过创建DcNetworkAgent完成,接下来我们分别介绍完成这两件事的过程。

7 通知数据拨号成功
在DataConnection的DcActiveState中,我们已经知道拨号成功后,将调用notifyAllOfConnected函数:

private void notifyAllOfConnected(String reason) {notifyAllWithEvent(null, DctConstants.EVENT_DATA_SETUP_COMPLETE, reason);
}private void notifyAllWithEvent(ApnContext alreadySent, int event, String reason) {............for (ConnectionParams cp : mApnContexts.values()) {..........//消息发送给了DcTrackerMessage msg = mDct.obtainMessage(event, pair);AsyncResult.forMessage(msg);msg.sendToTarget();}
}

DcTracker收到EVENT_DATA_SETUP_COMPLETE消息后,将调用onDataSetupComplete进行处理。

private void onDataSetupComplete(AsyncResult ar) {.....if (ar.exception == null) {............if (dcac == null) {//正常情况下,拨号前创建过DcAsyncChannel,不会进入该分支.........} else {ApnSetting apn = apnContext.getApnSetting();//有些APN,利用mms用的,配置了Proxy等属性if (apn != null && apn.proxy != null && apn.proxy.length() != 0) {try {String port = apn.port;if (TextUtils.isEmpty(port)) port = "8080";ProxyInfo proxy = new ProxyInfo(apn.proxy,Integer.parseInt(port), null);//通过DcAsyncChannel设入DataConnection的LinkProperties属性中dcac.setLinkPropertiesHttpProxySync(proxy);} catch (NumberFormatException e) {......}.........//更新ApnContext的状态apnContext.setState(DctConstants.State.CONNECTED);//判断APN是否为网络端配置的,国内见的比较少boolean isProvApn = apnContext.isProvisioningApn();..........//如果不是网络端配置的APNif ((!isProvApn) || mIsProvisioning) {.........completeConnection(apnContext);} else {//网络配置的APN,进行通知后,关闭radio//这里为什么要这么做,自己还不是太清楚.........setRadio(false);}}}} else {//拨号失败,也会发送通知.........}......
}

通过上面的代码,我们知道DcTracker判断拨号结果符合要求后,将调用completeConnection函数:

private void completeConnection(ApnContext apnContext) {........//进行通知工作mPhone.notifyDataConnection(apnContext.getReason(), apnContext.getApnType());//周期性读取底层接口文件,判断终端是否发送和接受数据,从而更新UI界面的上下行图标,以后单独介绍startNetStatPoll();//周期性地检测终端是否出现问题:同样是读取底层文件,当连续发送10个包,但没有收到回复时,认为终端出现问题,需要进行恢复,以后单独介绍startDataStallAlarm(DATA_STALL_NOT_SUSPECTED);
}

这里我们主要关注Phone对象的notifyDataConnection函数,这个函数现在由抽象类Phone来实现:

public void notifyDataConnection(String reason, String apnType) {mNotifier.notifyDataConnection(this, reason, apnType, getDataConnectionState(apnType));
}

其中,mNotifier的类型为DefaultPhoneNotifier,是PhoneFactory调用makeDefaultPhone时创建的,传入Phone对象中。
我们看看DefaultPhoneNotifier中的notifyDataConnection函数:

@Override
public void notifyDataConnection(Phone sender, String reason, String apnType,PhoneConstants.DataState state) {doNotifyDataConnection(sender, reason, apnType, state);
}private void doNotifyDataConnection(Phone sender, String reason, String apnType,PhoneConstants.DataState state) {//获取需要通知的参数........try {//mRegistry为TelephonyRegistry的Binder代理端if (mRegistry != null) {mRegistry.notifyDataConnectionForSubscriber(....);}}catch (RemoteException ex) {// system process is dead}
}

通过上面的代码,我们知道最终DefaultPhoneNotifier将通过Binder通信,调用TelephonyRegistry的接口。

我们看看TelephonyRegistry中的notifyDataConnectionForSubscriber:

public void notifyDataConnectionForSubscriber(.....) {........//mRecords记录了注册在TelephonyRegistry中的观察者synchronized (mRecords) {.......//如果状态发生改变,例如从未连接变为连接if (modified) {//轮询所有的观察者for (Record r : mRecords) {//如果观察者关注data状态的变化,并且监听的phone对应于建立连接的phoneif (r.matchPhoneStateListenerEvent(PhoneStateListener.LISTEN_DATA_CONNECTION_STATE) &&idMatch(r.subId, subId, phoneId)) {try {//通过回调函数进行通知r.callback.onDataConnectionStateChanged(mDataConnectionState[phoneId],mDataConnectionNetworkType[phoneId]);} catch (RemoteException ex) {//如果观察者已经死亡,加入移除链表mRemoveList.add(r.binder);}}}//移除异常观察者对应的注册信息handleRemoveListLocked();}//轮询所有的观察者,对于监听PRECISE_DATA_CONNECTION_STATE的观察者进行通知//与上面的相比,监听这个消息可以获得更多的dataConnection信息,但通知更为频繁(没有状态发生改变才通知的限制),同时要求更高的权限...........}//发送广播进行通知broadcastDataConnectionStateChanged(.......);broadcastPreciseDataConnectionStateChanged(.........);
}

至此,我们已经分析框架是如何通知其它应用数据连接的状态了。

对于APK的开发者而言,既可以监听广播来获取数据连接的状态,也可以通过调用TelephonyManager的接口:
public void listen(PhoneStateListener listener, int events)
只需要自己创建PhoneStateListener,指定subId(决定监听哪个phone的数据连接),同时定义回调函数,并指定关注的事件(events指定,具体的值定义于PhoneStateListener.java中)。

8 ConnectivityService管理网络
根据前面的代码,我们知道DataConnection在DcActiveState中,创建出了DcNetworkAgent。DcNetworkAgent是DataConnection的内部类,继承NetworkAgent。
我们看看NetworkAgent的构造函数:

public NetworkAgent(.....) {.........ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);//通过ConnectivityManager将自己注册到ConnectivityService//注意到此处的Messenger中包裹了NetworkAgent自身,NetworkAgent继承自Handler//ConnectivityService将通过AsyncChannel与NetworkAgent通信netId = cm.registerNetworkAgent(new Messenger(this), new NetworkInfo(ni),new LinkProperties(lp), new NetworkCapabilities(nc), score, misc);
}

ConnectivityManager通过Binder通信调用ConnectivityService中的接口:

public int registerNetworkAgent(....) {//权限检查enforceConnectivityInternalPermission();//利用输入参数构建NetworkAgentInfo,用来存储整个网络有关的信息final NetworkAgentInfo nai = new NetworkAgentInfo(......);.......//发送消息给自己的Handler处理mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_AGENT, nai));return nai.network.netId;
}

接下来,ConnectivityService的handle收到消息后,调用handleRegisterNetworkAgent进行处理:

private void handleRegisterNetworkAgent(NetworkAgentInfo na) {//与注册NetworkFactory一样,注册的NetworkAgent信息也会被存储到ConnectivityServicemNetworkAgentInfos.put(na.messenger, na);synchronized (mNetworkForNetId) {mNetworkForNetId.put(na.network.netId, na);}//同样,mTrackerHandler与NetworkAgent的handler连接在一起了na.asyncChannel.connect(mContext, mTrackerHandler, na.messenger);//新创建的NetworkAgentInfo,为了复用更新NetworkAgentInfo的接口,才进行了下述操作NetworkInfo networkInfo = na.networkInfo;na.networkInfo = null;//更新NetworkAgentInfo中的NetworkInfoupdateNetworkInfo(na, networkInfo);
}

继续跟进updateNetworkInfo:

private void updateNetworkInfo(NetworkAgentInfo networkAgent, NetworkInfo newInfo) {..........//新创建的NetworkAgentInfo的created字段为false//DataConnection在DcActiveState将NetworkInfo的状态置为了CONNECTEDif (!networkAgent.created&& (state == NetworkInfo.State.CONNECTED|| (state == NetworkInfo.State.CONNECTING && networkAgent.isVPN()))) {try {//如果建立的是VPN网络if (networkAgent.isVPN()) {//mNetd是NetworkManagementService的binder代理端mNetd.createVirtualNetwork(.....);} else {//对于实际的网络将进入这个分支,NetworkManagementService的操作,我们在后文再讲述mNetd.createPhysicalNetwork(.....);}}catch (Exception e) {...........}}//新创建的NetworkAgentInfo进入该分支if (!networkAgent.everConnected && state == NetworkInfo.State.CONNECTED) {//更新NetworkAgentInfo中的链路信息,例如mtu,dns, 路由等updateLinkProperties(networkAgent, null);//发送消息给NetworkMonitor;NetworkMonitor也是个状态机,收到消息后,负责通进行HTTP访问,并根据返回结果,判断网络是否可用,是否需要认证networkAgent.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_CONNECTED);..............//判断新注册的NetworkAgent能否保留rematchNetworkAndRequests(networkAgent, ReapUnvalidatedNetworks.REAP);..............//断开连接时,注销NetworkAgentInfo将进入这个分支} else if (state == NetworkInfo.State.DISCONNECTED) {..............//NetworkAgent处于挂起态时,进入该分支} else if ((oldInfo != null && oldInfo.getState() == NetworkInfo.State.SUSPENDED) || state == NetworkInfo.State.SUSPENDED){.............}
}

总结一下上面的代码,目前我们主要需要关注的是:通过NetworkManagementService创建实际的物理网络,更新网络的链路信息,判断NetworkAgent能否被保留。

ConnectivityService判断NetworkAgent能否被保留的原因,之前的blog中其实提过:当两个网络同时满足一个需求时,仅保留分数较高的。
因此当一个新的NetworkAgent注册到ConnectivityService时,需要判断这个NetworkAgent是否与已经注册过的NetworkAgent产生冲突。

我们看看rematchNetworkAndRequests函数:

private void rematchNetworkAndRequests(NetworkAgentInfo newNetwork,ReapUnvalidatedNetworks reapUnvalidatedNetworks) {...........//keep最后决定新加入的NetworkAgent是否保留boolean keep = newNetwork.isVPN();//匹配ConnectivityService初始化时创建默认NetworkRequest的NetworkAgent,将成为终端的默认网络boolean isNewDefault = false;//存储受到影响的NetworkAgentInfo//新加入的NetworkAgentInfo可能同时是多个networkRequest的最优匹配对象//于是这些NetworkRequest原来的匹配对象就是受到影响的NetworkAgentInfoArrayList<NetworkAgentInfo> affectedNetworks = new ArrayList<NetworkAgentInfo>();//记录需要进行通知的对象//APK可以通过ConnectivityManager的接口,注册监听网络变化ArrayList<NetworkRequestInfo> addedRequests = new ArrayList<NetworkRequestInfo>();//每一个NetworkRequest都需要进行重新匹配for (NetworkRequestInfo nri : mNetworkRequests.values()) {//取出NetworkRequest当前的最优NetworkAgentfinal NetworkAgentInfo currentNetwork = mNetworkForRequestId.get(nri.request.requestId);//判断新注册的NetworkAgent是否匹配这个NetworkRequest,即NetworkCapabilities是否能够满足final boolean satisfies = newNetwork.satisfies(nri.request);//同样的NetworkAgent,匹配情况不变if (newNetwork == currentNetwork && satisfies) {keep = true;continue;}//新增加的NetworkAgent匹配NetworkRequestif (satisfies) {//如果这个NetworkRequest仅用于监听if (!nri.isRequest()) {//存入相应的对象中,通知时将使用if (newNetwork.addRequest(nri.request)) addedRequests.add(nri);continue;}//如果这个匹配的NetworkRequest没有对应的NetworkAgent//或者对应NetworkAgent的分数小于新增NetworkAgentif (currentNetwork == null ||currentNetwork.getCurrentScore() < newNetwork.getCurrentScore()) {//旧有的NetworkAgent被取代if (currentNetwork != null) {currentNetwork.networkRequests.remove(nri.request.requestId);currentNetwork.networkLingered.add(nri.request);//被取代后,加入到affectedNetworks中affectedNetworks.add(currentNetwork);} else {//log.......}//新增加的NetworkAgent不会被移除unlinger(newNetwork);mNetworkForRequestId.put(nri.request.requestId, newNetwork);if (!newNetwork.addRequest(nri.request)) {..........}addedRequests.add(nri);keep = true;//由于NetworkRequest匹配到了新的NetworkAgent,因此更新一下分数,以免NetworkFactory进行不必要的建立连接的操作sendUpdatedScoreToFactories(nri.request, newNetwork.getCurrentScore());//如果匹配的是ConnectivityService中默认的Request,那么新的NetworkAgent将成为默认使用的网络if (mDefaultRequest.requestId == nri.request.requestId) {isNewDefault = true;oldDefaultNetwork = currentNetwork;}}//下面这个分支的意思是:NetworkAgent中包含NetworkRequest但不匹配;说明NetworkAgent之前匹配,属性发生变化导致不匹配了} else if (newNetwork.networkRequests.get(nri.request.requestId) != null){newNetwork.networkRequests.remove(nri.request.requestId);//如果这个不再匹配的networkAgent曾经是最匹配的,那么需要更新分数,让合适的NetworkFactory建立连接if (currentNetwork == newNetwork) {mNetworkForRequestId.remove(nri.request.requestId);sendUpdatedScoreToFactories(nri.request, 0);} else {if (nri.isRequest()) {//仅打印log,不应该进入到这个分支................}}//通过回调接口通知观察者,网络断开callCallbackForRequest(nri, newNetwork, ConnectivityManager.CALLBACK_LOST);}}//处理受影响的NetworkAgentfor (NetworkAgentInfo nai : affectedNetworks) {//NetworkAgent处于等待移除的状态,不用管if (nai.lingering) {} else if (unneeded(nai)) { //unneeded函数判断该NetworkAgent是否为其它NetworkRequest的最优匹配对象,如果不是就可以移除//NetworkMonitor发送消息进入linger状态,30s后移除无用NetworkAgentlinger(nai);} else {//保留NetworkAgentunlinger(nai);}}//如果是新的默认网络if (isNewDefault) {//通过NetworkManagementService将该Network设置为默认网络makeDefault(newNetwork);...............}...............//如果输入参数为ReapUnvalidatedNetworks.REAP,则不经过linger状态,直接关闭无效NetworkAgentif (reapUnvalidatedNetworks == ReapUnvalidatedNetworks.REAP) {for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {if (unneeded(nai)) {if (DBG) log("Reaping " + nai.name());teardownUnneededNetwork(nai);}}}
}

以上就是数据长连接拨号中,ConnectivityService参与的主要流程。其中,就是rematchNetworkAndRequests函数过长,导致看起来比较繁琐。
但整个过程相对而言还是比较简单的,其实就是让ConnectivityService来管理数据拨号产生的NetworkAgent,包括判断该NetworkAgent能否保留,是否需要更新其它现有的NetworkAgent。

9 NetworkManagementService创建和配置网络
我们知道Android是运行在Linux之上的,前面的拨号实际上仅在框架层形成了网络的抽象对象,还需要在Native层中形成网络抽象,这就需要依赖于NetworkManagementService了。
在前面的代码中,我们已经提到过ConnectivityService会通过NetworkManagementService创建网络,配置路由等网络属性。现在我们看看NetworkManagementService到底是如何做到的。

9.1 创建网络

//ConnectivityService中调用以下代码创建网络
//mNetd是NetworkManagementService对应的binder代理端
mNetd.createPhysicalNetwork(networkAgent.network.netId,networkAgent.networkCapabilities.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) ?null : NetworkManagementService.PERMISSION_SYSTEM);

看看NetworkManagementService中对应的createPhysicalNetwork:

public void createPhysicalNetwork(int netId, String permission) {//权限检查mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);try {if (permission != null) {mConnector.execute("network", "create", netId, permission);} else {//默认的数据业务,是没有permission要求的mConnector.execute("network", "create", netId);}} catch (NativeDaemonConnectorException e) {throw e.rethrowAsParcelableException();}
}

代码中,mConnector是NativeDaemonConnector,是用于连接NetworkManagementService与netd的。netd是Android中管理网络的守护进程。在之前的版本中,netd的启动定义与init.rc中,由init进程启动;在android 7.0中,定义于/system/netd/server/netd.rc中:

service netd /system/bin/netdclass mainsocket netd stream 0660 root systemsocket dnsproxyd stream 0660 root inetsocket mdns stream 0660 root systemsocket fwmarkd stream 0660 root inet

目前自己没发现netd.rc是如何集成到整个Android启动过程中的,如果有朋友知道的话,请指点一下。

接下来,我们分析一下NetworkManagementService中的代码:

//创建NetworkManagementService
public static NetworkManagementService create(Context context) throws InterruptedException {//NETD_SOCKET_NAME为"netd",是netd进程启动时创建的socketreturn create(context, NETD_SOCKET_NAME);
}static NetworkManagementService create(Context context, String socket) throws InterruptedException {//创建NetworkManagementService,其中创建NativeDataConnectorfinal NetworkManagementService service = new NetworkManagementService(context, socket);//service.mConnectedSignal的值为CountDownLatch(1),只用递减1次final CountDownLatch connectedSignal = service.mConnectedSignal;//NativeDataConnector连接netdservice.mThread.start();//等待连接成功connectedSignal.await();return service;
}private NetworkManagementService(Context context, String socket) {.............//创建NativeDaemonConnector,继承runnable//NetdCallbackReceiver为回调函数mConnector = new NativeDaemonConnector(new NetdCallbackReceiver(), socket, 10, NETD_TAG, 160, wl, FgThread.get().getLooper());mThread = new Thread(mConnector, NETD_TAG);.............
}

从上面的代码可以看出,NetworkManagementService调用service.mThread.start()后,将调用NativeDaemonConnector的run方法:

public void run() {mCallbackHandler = new Handler(mLooper, this);while (true) {try {listenToSocket();} catch (Exception e) {loge("Error in NativeDaemonConnector: " + e);SystemClock.sleep(5000);}}
}private void listenToSocket() throws IOException {LocalSocket socket = null;try {socket = new LocalSocket();//返回"netd"的地址LocalSocketAddress address = determineSocketAddress();//本地socket连接netd socketsocket.connect(address);InputStream inputStream = socket.getInputStream();synchronized (mDaemonLock) {mOutputStream = socket.getOutputStream();}//连接成功后,调用NetworkManagementService中定义的回调函数mCallbacks.onDaemonConnected();//后面的部分暂时不用管,其实就是接受netd socket发过来的数据............
}

看看NetworkManagementService中定义的回调接口:

private class NetdCallbackReceiver implements INativeDaemonConnectorCallbacks {public void onDaemonConnected() {if (mConnectedSignal != null) {//将countDownLatch减1,触发NetworkManagementService的构造函数返回mConnectedSignal.countDown();mConnectedSignal = null;} else {mFgHandler.post(new Runnable() {@Overridepublic void run() {prepareNativeDaemon();}});}}........
}

根据上面的代码,我们知道了NativeDaemonConnector创建的过程,并且知道了NativeDaemonConnector通过socket与netd进程中名为"netd"的socket相连。于是,我们就可以得出结论:NativeDaemonConnector是NetworkManagementService与netd进程通信的桥梁。

现在我们回到NetworkManagementService创建physical network的流程:

.............
//调用NativeDaemonConnector的execute方法
mConnector.execute("network", "create", netId);
.............

进入NativeDaemonConnector:

public NativeDaemonEvent execute(String cmd, Object... args)throws NativeDaemonConnectorException {//DEFAULT_TIMEOUT的时间为1min;如果一个cmd执行时间超过1min,watchdog将会杀死进程return execute(DEFAULT_TIMEOUT, cmd, args);
}public NativeDaemonEvent execute(long timeoutMs, String cmd, Object... args) throws NativeDaemonConnectorException {//调用executeForList执行final NativeDaemonEvent[] events = executeForList(timeoutMs, cmd, args);.............
}public NativeDaemonEvent[] executeForList(long timeoutMs, String cmd, Object... args)throws NativeDaemonConnectorException {................//记录命令发起时间final long startTime = SystemClock.elapsedRealtime();..............//每个cmd的有唯一编号final int sequenceNumber = mSequenceNumber.incrementAndGet();//利用参数构造netd规定的cmd格式makeCommand(rawBuilder, logBuilder, sequenceNumber, cmd, args);final String rawCmd = rawBuilder.toString();synchronized (mDaemonLock) {if (mOutputStream == null) {............} else {try {//将消息发送给netdmOutputStream.write(rawCmd.getBytes(StandardCharsets.UTF_8));} catch() {...........}}}NativeDaemonEvent event = null;do {//从mResponseQueue取出返回结果;mResponseQueue的类型为BlockingQueue,此处最多等待timeoutMs//前文中,NativeDaemonConnector的run方法中,创建socket并连接netd后,接收的消息进行解析后会放入mResponseQueue中event = mResponseQueue.remove(sequenceNumber, timeoutMs, logCmd);}while (event.isClassContinue());//记录收到返回结果的时间final long endTime = SystemClock.elapsedRealtime();//根据返回结果判断命令是否执行异常...............
}

netd守护进程以后单独分析一下,这里我们只需要知道netd中定义了CommandListener,用于处理不同的命令。
CommandListener定义于文件system/netd/server/CommandListener.cpp中:

CommandListener::CommandListener() :FrameworkListener("netd", true) {registerLockingCmd(new InterfaceCmd());registerLockingCmd(new IpFwdCmd());registerLockingCmd(new TetherCmd());registerLockingCmd(new NatCmd());registerLockingCmd(new ListTtysCmd());registerLockingCmd(new PppdCmd());registerLockingCmd(new SoftapCmd());registerLockingCmd(new BandwidthControlCmd(), gCtls->bandwidthCtrl.lock);registerLockingCmd(new IdletimerControlCmd());registerLockingCmd(new ResolverCmd());registerLockingCmd(new FirewallCmd(), gCtls->firewallCtrl.lock);registerLockingCmd(new ClatdCmd());registerLockingCmd(new NetworkCommand());registerLockingCmd(new StrictCmd());...................
}

每种Cmd的名称基本上能概括它们的功能。
这里我们看一下NetowrkCommand:

//前面我们提过,NetworkManagementService创建网络时,第一个参数就是“network”
//因此在NativeDaemonConnector创建cmd时,指定的参数也是“network”
//netd进程收到消息后,就用对应的NetworkCommand表示
CommandListener::NetworkCommand::NetworkCommand() : NetdCommand("network") {
}//对应的处理函数
int CommandListener::NetworkCommand::runCommand(SocketClient* client, int argc, char** argv) {..........//根据传入参数,做相应的处理if (!strcmp(argv[1], "route")) {.............}if (!strcmp(argv[1], "interface")) {.............}if (!strcmp(argv[1], "create")) {//判断参数有效性.........//解析出framework分配的netIdunsigned netId = stringToNetId(argv[2]);if (argc == 6 && !strcmp(argv[3], "vpn")) {//创建VPN............} else if (argc > 4) {return syntaxError(client, "Unknown trailing argument(s)");} else {//默认的数据网络,是没有permission限制的Permission permission = PERMISSION_NONE;if (argc == 4) {permission = stringToPermission(argv[3]);if (permission == PERMISSION_NONE) {return syntaxError(client, "Unknown permission");}}//调用NetworkController.cpp的createPhysicalNetworkif (int ret = gCtls->netCtrl.createPhysicalNetwork(netId, permission)) {return operationError(client, "createPhysicalNetwork() failed", ret);}}}..............
}

NetworkController.cpp位于system/netd/server目录下:

int NetworkController::createPhysicalNetwork(unsigned netId, Permission permission) {//检查netId的有效性..............//创建网络对象PhysicalNetwork* physicalNetwork = new PhysicalNetwork(netId, mDelegateImpl);//如果有权限,还要设置权限if (int ret = physicalNetwork->setPermission(permission)) {ALOGE("inconceivable! setPermission cannot fail on an empty network");delete physicalNetwork;return ret;}android::RWLock::AutoWLock lock(mRWLock);//保存新建的网络mNetworks[netId] = physicalNetwork;return 0;
}

至此,Android的框架完成了在Native层创建网络对象的工作。

9.2 配置网络
ConnectivityService在创建完网络后,调用了updateLinkProperties函数:

private void updateLinkProperties(NetworkAgentInfo networkAgent, LinkProperties oldLp) {...............//这些函数均通过NetworkManagementService,利用NativeDaemonConnector发送命令给Netd进程updateInterfaces(newLp, oldLp, netId);updateMtu(newLp, oldLp);..............updateRoutes(newLp, oldLp, netId);updateDnses(newLp, oldLp, netId);...............
}

当新建的网络成为default网络后,ConnectivityService会调用makeDefault函数:

private void makeDefault(NetworkAgentInfo newNetwork) {try {//同样利用NetworkManagementService发送命令给netdmNetd.setDefaultNetId(newNetwork.network.netId);} catch (Exception e) {loge("Exception setting default network :" + e);}...................//TCP buffer大小是通过修改系统属性得到的updateTcpBufferSizes(newNetwork);
}

以上函数调用,最终均会在CommandListener中按照各种类型的Command定义的方式进行处理,用于配置网络的各种属性。其中的调用方式,与创建网络基本类似,不再做深入分析。

经过上面的分析,我们来总结一下,对于Android而言,什么叫做一个可用的网络?
其实可以认为网络就是一个可用的网卡接口(Interface),加上针对该接口的属性,例如IP地址、dns、mtu以及路由等。

前面的流程中我们知道框架拨号成功后,利用拨号返回结果中携带的信息,创建并配置了网络。这些信息利用IP地址等,是modem与网络侧协商得到的,但接口使如何分配的呢?此外,我们知道Android是运行在Linux上的,那么拨号成功后,Linux又是如何开启一个实际接口的呢?
其实这部分内容被封装在了RIL以下,由不同的厂商来实现。例如,Qualcomm中定义了NETGMRD进程,当拨号成功后,NETMGRD利用拨号得到的信息,配置Linux的数据协议栈。这部分内容是厂家的机密,就不方便写在Blog中了。

结束语
数据业务长连接拨号对应的流程比较繁琐,即使不包含RIL层以下,也很难看一遍就完全掌握。我们略去了很多细节,仅梳理了大致的脉络。
最后我们还是整理一下,整个流程涉及的类图和流程图:

类图链接

[流程图链接](https://img-blog.csdn.net/20160901162732571)

Android7.0 数据业务长连接拨号过程相关推荐

  1. Android7.0 数据业务长连接去拨号过程

    在之前的博客中,我们分析了数据业务长连接的拨号过程,现在我们来看看Android如何实现去拨号过程. 与拨号过程一样,用户也是通过点击设置界面,发起主动断开数据连接的命令. 从界面到框架,同样经过了C ...

  2. Android7.0 数据业务中的短连接

    数据业务中的短连接,是一种为了满足业务需求,临时建立起来的连接.当业务完成通信需求后,这种数据连接会被框架释放掉.与之相对,长连接一旦拨号成功就会一直存在下去,除非用户主动关闭或者终端受到网络等因素的 ...

  3. Android7.0 数据拨号前的准备工作

    背景  在介绍PhoneApp的创建过程时,我们知道为了支持双卡手机,PhoneFactory创建了两个Phone对象.  然而由于通信制式.功耗等的限制,目前底层的芯片厂商规定modem工作于DSD ...

  4. Android7.0 数据拨号前的准备工作

    背景 在介绍PhoneApp的创建过程时,我们知道为了支持双卡手机,PhoneFactory创建了两个Phone对象. 然而由于通信制式.功耗等的限制,目前底层的芯片厂商规定modem工作于DSDS模 ...

  5. android7.0及以上版本签名校验过程详解

    对于新的签名方案APK Signature Scheme v2,在这篇文章中已经有详细的介绍http://www.tuicool.com/articles/bURRVrj.从这篇文章中可以知道,新的签 ...

  6. android7.0 提示wifi已连接,但无法连接到互联网

    1.所遇问题 当连接无线网的时候,手机提示wifi已连接,但无法连接到互联网.这是因为国内在2014年5月份开始屏蔽了所有谷歌服务,而android7.0系统里引入了一个wifi监测功能,这个模块会和 ...

  7. Netty 实现长连接服务的难点和优化点

    推送服务 还记得一年半前,做的一个项目需要用到 Android 推送服务.和 iOS 不同,Android 生态中没有统一的推送服务.Google 虽然有 Google Cloud Messaging ...

  8. ios http长连接_Nginx篇05——http长连接和keeplive

    nginx中http模块使用http长连接的相关配置(主要是keepalive指令)和http长连接的原理解释. 1.http长连接 1.1 预备知识 连接管理是一个 HTTP 的关键话题:打开和保持 ...

  9. 游戏网络Socket长连接管理

    对于网络游戏来说,网络连接的开发与维护是非常重要的,这里主要说明一下最常用的socket长连接开发与管理.服务端使用的网络框架是Netty,客户端使用的是unity,本文中的源码,可以在这里查看:ht ...

最新文章

  1. cookie的路径和域
  2. Linux服务-Samba文件服务器部署
  3. 一起谈.NET技术,WPF 基础到企业应用系列5——WPF千年轮回2
  4. apache php 工作模式,PHP Apache中两种工作方式区别(CGI模式、Apache 模块DLL)
  5. 常用的5种数据分析方法有哪些?
  6. c#使用office的墨迹书写工具
  7. python 成语接龙1-爬去四字成语
  8. Win11色温如何进行调整设置
  9. IJCAI 2022 | 鲁棒的Node-Node Level自对齐图对比学习
  10. 互联网公司 概率面试题整理
  11. 解决找不到gpedit.msc文件方法
  12. java.lang.ClassCastException: java.util.Arrays$ArrayList cannot be cast to java.util.ArrayList
  13. linux Windows双系统时间不一致的解决办法
  14. Python群发短信
  15. 开始创业之路(MMORPG)
  16. Introduction To AMBA 简单理解
  17. 火车票退票费计算(函数专题)
  18. 计算机及数控编程仿真软件exsl-win7,数控编程实验..doc
  19. 利用VS(Visual Studio)自带的工具查看dll/lib文件
  20. JPA踩坑记:Spring Data Jpa 更新为null的问题(save方法保存时null值会被更新到数据库)

热门文章

  1. linux ecc校验原理,Nand ECC校验和纠错原理及ECC代码分析
  2. 华师c语言作业,16秋华师《c语言程序设计a》在线作业
  3. 计算机作文+300字,有关电脑作文300字六篇
  4. 换脸方法大汇总:生成对抗网络GAN、扩散模型等
  5. 14 医疗挂号系统_【阿里云OSS、用户认证与就诊人】
  6. VUE+Highcharts气泡图
  7. 代码版本管理工具Git
  8. 局域网攻击之DHCP Starvation(DHCP饿死)
  9. java 下载微信图片_java 微信服务器下载图片到自己服务器
  10. idea Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Progra