編輯:關於Android編程
前面我們已經分析了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())) { //將標志位置為true mMultiSimDialog = 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) { //創建PhoneInterfaceManager sInstance = 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映射得到phoneId int 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為null trySetupData(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從配置中讀出來,衡為true return mDataEnabled.get() && mDependencyMet.get(); }
根據前面的數據業務撥號准備工作的流程,我們知道ConnectivityService中default NetworkRequest對應類型的ApnContext被激活了,也就是Default Type的APN被激活了。
因此,我們至少有一個connectable的APN可以使用, 可以繼續撥號流程。
trySetupData函數中主要根據當前終端的運行狀態,判斷框架是否應該繼續撥號。
private boolean trySetupData(ApnContext apnContext, ArrayList waitingApns) { //判斷撥號條件 .......... //整個判斷能否撥號的過程比較復雜,涉及了很多細節 //本來打算詳細寫一下,但寫了一部分後,發現太亂了 //這裡就大概描述一下:其實這裡就是檢查之前的准備工作是否完成,主要是結合APN類型、數據能力是否激活及數據開關是否打開等,判斷處能否繼續撥號 if (......../*滿足撥號條件*/) { ............ if (apnContext.getState() == DctConstants.State.IDLE) { if (waitingApns == null) { //結合激活的apnContext的type,例如default類型,以及底層使用的無線技術(從servicestate獲取),從Dctracker加載卡對應的所有apn中,得到可以使用的Apn waitingApns = buildWaitingApns(apnContext.getApnType(), radioTech); } if (waitingApns.isEmpty()) { //沒獲取到可用apn ........ return false; } else { //將所有可用的apn存入apnContext apnContext.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取出可用的apn apnSetting = apnContext.getNextApnSetting(); ............. //得到profileId,這個需要傳遞給modem int 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_B if (isHigherPriorityApnContextActive(apnContext)) { return false; } //在僅支持單連接的情況下,撥號前需要清楚所有已建立的連接 //代碼走到這裡,說明當前APN的優先級是最高的,需要清除低優先級的連接 //舉例來說,在上述無線技術下,建立彩信時,會斷開已連接的default數據業務;當然彩信發送完畢後,會自動重新建立default的數據連接 if (cleanUpAllConnections(true, Phone.REASON_SINGLE_PDN_ARBITRATION)) { return false; } } //判斷能否復用dataConnection dcac = findFreeDataConnection(); if (dcac == null) { //不能復用則創建新的dataConnection和dcAsyncChannel dcac = createDataConnection(); } .......... } //下面均是更新ApnContext的狀態 final int generation = apnContext.incAndGetConnectionGeneration(); ............. apnContext.setDataConnectionAc(dcac); apnContext.setApnSetting(apnSetting); apnContext.setState(DctConstants.State.CONNECTING); Message msg = obtainMessage(); //注意此msg類型;當dataConnection撥號成功後,將會返回此消息給dcTracker msg.what = DctConstants.EVENT_DATA_SETUP_COMPLETE; msg.obj = new Pair(apnContext, generation); //調用dcAsyncChannel的bringUp dcac.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為dctracker DataConnection conn = DataConnection.makeDataConnection(mPhone, id, this, mDcTesterFailBringUpAll, mDcc); //dctracker保存dataconnection mDataConnections.put(id, conn); //創建DcAsyncChannel DcAsyncChannel 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的id mDataConnectionAcHashMap.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是個狀態機,因此需要start dc.start(); return dc; }
最後,看看DataConnection的構造函數:
//繼承狀態機 public class DataConnection extends StateMachine { ...... private DataConnection(......) { ........... //撥號成功後,將利用NetworkInfo構造NetworkAgent,注冊到ConnectivityService mNetworkInfo = 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); //初始態為InactiveState setInitialState(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,也就是dataConnection sendMessage(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 { ......... @Override public 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_DONE Message msg = obtainMessage(EVENT_SETUP_DATA_CONNECTION_DONE, cp); msg.obj = cp; ........... //通過RIL將消息發送給modem mPhone.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 { @Override public 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; //如果隊列中有斷開連接的消息待處理,則不創建NetworkAgent if (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_COMPLETE notifyAllOfConnected(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,將注冊到ConnectivityService mNetworkAgent = 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()) { .......... //消息發送給了DcTracker Message 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(); .......... //如果不是網絡端配置的APN if ((!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對應於建立連接的phone if (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信息也會被存儲到ConnectivityService mNetworkAgentInfos.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中的NetworkInfo updateNetworkInfo(na, networkInfo); }
繼續跟進updateNetworkInfo:
private void updateNetworkInfo(NetworkAgentInfo networkAgent, NetworkInfo newInfo) { .......... //新創建的NetworkAgentInfo的created字段為false //DataConnection在DcActiveState將NetworkInfo的狀態置為了CONNECTED if (!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原來的匹配對象就是受到影響的NetworkAgentInfo ArrayListaffectedNetworks = new ArrayList (); //記錄需要進行通知的對象 //APK可以通過ConnectivityManager的接口,注冊監聽網絡變化 ArrayList addedRequests = new ArrayList (); //每一個NetworkRequest都需要進行重新匹配 for (NetworkRequestInfo nri : mNetworkRequests.values()) { //取出NetworkRequest當前的最優NetworkAgent final 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匹配NetworkRequest if (satisfies) { //如果這個NetworkRequest僅用於監聽 if (!nri.isRequest()) { //存入相應的對象中,通知時將使用 if (newNetwork.addRequest(nri.request)) addedRequests.add(nri); continue; } //如果這個匹配的NetworkRequest沒有對應的NetworkAgent //或者對應NetworkAgent的分數小於新增NetworkAgent if (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); } } //處理受影響的NetworkAgent for (NetworkAgentInfo nai : affectedNetworks) { //NetworkAgent處於等待移除的狀態,不用管 if (nai.lingering) { } else if (unneeded(nai)) { //unneeded函數判斷該NetworkAgent是否為其它NetworkRequest的最優匹配對象,如果不是就可以移除 //NetworkMonitor發送消息進入linger狀態,30s後移除無用NetworkAgent linger(nai); } else { //保留NetworkAgent unlinger(nai); } } //如果是新的默認網絡 if (isNewDefault) { //通過NetworkManagementService將該Network設置為默認網絡 makeDefault(newNetwork); ............... } ............... //如果輸入參數為ReapUnvalidatedNetworks.REAP,則不經過linger狀態,直接關閉無效NetworkAgent if (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/netd class main socket netd stream 0660 root system socket dnsproxyd stream 0660 root inet socket mdns stream 0660 root system socket fwmarkd stream 0660 root inet
目前自己沒發現netd.rc是如何集成到整個Android啟動過程中的,如果有朋友知道的話,請指點一下。
接下來,我們分析一下NetworkManagementService中的代碼:
//創建NetworkManagementService public static NetworkManagementService create(Context context) throws InterruptedException { //NETD_SOCKET_NAME為"netd",是netd進程啟動時創建的socket return create(context, NETD_SOCKET_NAME); } static NetworkManagementService create(Context context, String socket) throws InterruptedException { //創建NetworkManagementService,其中創建NativeDataConnector final NetworkManagementService service = new NetworkManagementService(context, socket); //service.mConnectedSignal的值為CountDownLatch(1),只用遞減1次 final CountDownLatch connectedSignal = service.mConnectedSignal; //NativeDataConnector連接netd service.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 socket socket.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() { @Override public 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 { //將消息發送給netd mOutputStream.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分配的netId unsigned 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的createPhysicalNetwork if (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發送命令給netd mNetd.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層以下,也很難看一遍就完全掌握。我們略去了很多細節,僅梳理了大致的脈絡。
最後我們還是整理一下,整個流程涉及的類圖和流程圖:
1 背景建議閱讀本文之前先閱讀《Android Studio入門到精通》和《Groovy腳本基礎全攻略》及《Gradle腳本基礎全攻略》三篇博客作為背景知識,這樣才能更好
通過本次小Demo我學到了:1、ListView的小小的一個分頁功能2、加深了對自定義控件的理解3、對ListView的優化4、對BaseAdapter的使用5、自定義A
主要內容1. Call涉及的目錄結構及框架結構2. InCallUI層的基本架構(所涉及的Presenter、Fragment及Activity)3. Call的幾種狀態
Android 5.0 Lollipop 是迄今為止最重大的一次發布,因為 material design 的存在,android的界面風格發生了新的改變,這是一門新的設