場(chǎng)景
為了追求極致的用戶(hù)體驗(yàn),每個(gè)app都耗盡腦汁想盡辦法優(yōu)化自身,特別是網(wǎng)絡(luò)卡頓時(shí)候的體驗(yàn),期待在wifi卡頓情況下,通過(guò)白名單控制域名走用戶(hù)的蜂窩網(wǎng)絡(luò)通道。這期主要分享下wifi連接情況下,如何用蜂窩網(wǎng)發(fā)送相應(yīng)的請(qǐng)求。后續(xù)還會(huì)著重分析一個(gè)問(wèn)題,卡了自己好久的問(wèn)題,后面也是看了android官方文檔解釋才明白為啥出這個(gè)問(wèn)題。
介紹
一、系統(tǒng)Api
首先我們要了解下系統(tǒng)的api實(shí)現(xiàn),主要就是Network#bindSocket方法,這個(gè)方法是獲取對(duì)應(yīng)通道的network對(duì)象,再把客戶(hù)端網(wǎng)絡(luò)傳輸?shù)膕ocket綁定到對(duì)應(yīng)通道的network對(duì)象上,從而把對(duì)應(yīng)的網(wǎng)絡(luò)請(qǐng)求切換到不同通道上,實(shí)現(xiàn)雙網(wǎng)卡交互的邏輯。
二、Cronet底層如何實(shí)現(xiàn)雙通道邏輯
了解系統(tǒng)的Api后,我們對(duì)整體android的系統(tǒng)Api有個(gè)認(rèn)知,那接下來(lái)我根據(jù)源碼和大家簡(jiǎn)單分析下Cronet底層socket如何綁定對(duì)應(yīng)的網(wǎng)絡(luò)通道的??聪旅鍯ronet的代碼,可以看出Cronet c++底層也是加載native庫(kù),通過(guò)打開(kāi)Android底層的文件句柄,直接訪(fǎng)問(wèn)Network#bindSocket的C++方法,所以說(shuō)為什么我在第一步先介紹Android系統(tǒng)的Api,各種框架八九不離十都會(huì)和系統(tǒng)的api打交道的,只不過(guò)是分走java層還是走C++層而已。
//大于等于android 6.0
MarshmallowSetNetworkForSocket GetMarshmallowSetNetworkForSocket() {
// On Android M and newer releases use supported NDK API.
base::FilePath file(base::GetNativeLibraryName("android"));
// See declaration of android_setsocknetwork() here:
// http://androidxref.com/6.0.0_r1/xref/development/ndk/platforms/android-M/include/android/multinetwork.h#65
// Function cannot be called directly as it will cause app to fail to load on
// pre-marshmallow devices.
void* dl = dlopen(file.value().c_str(), RTLD_NOW);
return reinterpret_cast<MarshmallowSetNetworkForSocket>(
dlsym(dl, "android_setsocknetwork"));
}
//小于android 6.0
LollipopSetNetworkForSocket GetLollipopSetNetworkForSocket() {
// On Android L use setNetworkForSocket from libnetd_client.so. Android's netd
// client library should always be loaded in our address space as it shims
// socket().
base::FilePath file(base::GetNativeLibraryName("netd_client"));
// Use RTLD_NOW to match Android's prior loading of the library:
// http://androidxref.com/6.0.0_r5/xref/bionic/libc/bionic/NetdClient.cpp#37
// Use RTLD_NOLOAD to assert that the library is already loaded and avoid
// doing any disk IO.
void* dl = dlopen(file.value().c_str(), RTLD_NOW | RTLD_NOLOAD);
return reinterpret_cast<LollipopSetNetworkForSocket>(
dlsym(dl, "setNetworkForSocket"));
}
Cronet支持雙通道鏈接
Android 6.0以上的C++方法android_setsocknetwork方法
三、Cronet如何使用的
這里從tcp協(xié)議分析Cronet如何指定網(wǎng)絡(luò)通道。下面代碼可以看出函數(shù)參數(shù)需要一個(gè)叫NetworkHandle一個(gè)對(duì)象,其實(shí)這個(gè)對(duì)象就是Java的Network的id,這個(gè)Android系統(tǒng)api也有方法可以參考copy一份的,那么到這里整個(gè)鏈路都比較清晰了。要Cronet走不同的網(wǎng)絡(luò)通道,首先要獲取對(duì)應(yīng)通道的Network的id,其次通過(guò)id在Cronet決定走tcp/udp協(xié)議后,本地創(chuàng)建的socket綁定對(duì)應(yīng)的id,就可以實(shí)現(xiàn)不同網(wǎng)絡(luò)通道傳輸啦。
//tcp socket bind network
int TCPSocketPosix::BindToNetwork(handle::NetworkHandle network) {
DCHECK(IsValid());
DCHECK(!IsConnected());
VLOG(1) << "cellular BindToNetwork: " << network;
#if defined(OS_ANDROID)
return android::BindToNetwork(socket_->socket_fd(), network);
#else
NOTIMPLEMENTED();
return ERR_NOT_IMPLEMENTED;
#endif // #if defined(OS_ANDROID)
}
四、如何獲取對(duì)應(yīng)的Network的id
我這里其實(shí)不是最好的實(shí)現(xiàn),因?yàn)槲乙彩强淳W(wǎng)上和官網(wǎng)的文檔后,只能想到這種方式去獲取。但這個(gè)方式有個(gè)致命缺陷,需要用戶(hù)同意打開(kāi)某個(gè)高級(jí)權(quán)限才可以獲取到對(duì)應(yīng)的Network的id。但是微信是可以做到不需要用戶(hù)打開(kāi)權(quán)限,網(wǎng)絡(luò)差的情況下直接走蜂窩網(wǎng)通道拉取消息,這個(gè)我暫時(shí)沒(méi)想到能咋整。
下面代碼需要申請(qǐng)權(quán)限:
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
主要是WRITE_SETTINGS在高版本手機(jī)需要用戶(hù)主動(dòng)選擇打開(kāi)才行,動(dòng)態(tài)獲取Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)就可以跳轉(zhuǎn)到設(shè)置頁(yè)面,讓用戶(hù)去打開(kāi)了。
public class NetworkHelper {
private static final String TAG = "NetworkIdHelper";
/**
* Network handle representing the default network. To be used when a network has not been * explicitly set.
*/
private static final long DEFAULT_NETWORK_HANDLE = -1;
private static NetworkCallbackImpl _networkCallback = null;
private static Network _network = null;
private static long _networkId = DEFAULT_NETWORK_HANDLE;
private static ConnectivityManager _connectivityManager = null;
public static void init(Context context) {
Log.i(TAG, "init");
_connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
}
public static void openMobileNetwork(AvailableNetworkCallback callback) {
Log.i(TAG, "openMobileNetwork");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (_connectivityManager != null) {
try {
NetworkRequest.Builder builder = new NetworkRequest.Builder();
builder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR);
NetworkRequest request =
new NetworkRequest.Builder()
// 設(shè)置指定的?絡(luò)傳輸類(lèi)型(蜂窩傳輸) 等于?機(jī)?絡(luò)
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
// 設(shè)置感興趣的?絡(luò)功能
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build();
_networkCallback = new NetworkCallbackImpl(_connectivityManager, callback);
_connectivityManager.requestNetwork(request, _networkCallback);
} catch (Exception e) {
Log.i(TAG, "openMobileNetwork error: " + e.getMessage());
}
}
}
}
public static void closeMobileNetwork() {
Log.i(TAG, "closeMobileNetwork");
if (_connectivityManager != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
_connectivityManager.unregisterNetworkCallback(_networkCallback);
}
}
}
private static synchronized long getNetworkToNetId() {
if (_network != null) {
_networkId = Build.VERSION.SDK_INT >= 23 ? _network.getNetworkHandle() :
(long) Integer.parseInt(_network.toString());
}
return _networkId;
}
public static long networkToNetId() {
if (_networkId == DEFAULT_NETWORK_HANDLE) {
_networkId = getNetworkToNetId();
}
Log.i(TAG, "cellular networkToNetId _networkId: " + _networkId);
return _networkId;
}
public interface AvailableNetworkCallback {
void onAvailable(long networkId);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static class NetworkCallbackImpl extends ConnectivityManager.NetworkCallback {
final ConnectivityManager connectivityManager;
AvailableNetworkCallback availableNetworkCallback;
public NetworkCallbackImpl(ConnectivityManager connectivityManager, AvailableNetworkCallback callback) {
this.connectivityManager = connectivityManager;
this.availableNetworkCallback = callback;
}
@Override
public void onAvailable(Network network) {
super.onAvailable(network);
Log.i(TAG, "Mobile Network Available");
_network = network;
networkToNetId();
if (_networkId != DEFAULT_NETWORK_HANDLE) {
availableNetworkCallback.onAvailable(_networkId);
}
}
@Override
public void onLost(Network network) {
super.onLost(network);
Log.i(TAG, "Mobile Network onLost");
_network = null;
}
}
}
問(wèn)題
在實(shí)際開(kāi)發(fā)過(guò)程中遇到個(gè)非常棘手問(wèn)題,在某些國(guó)內(nèi)手機(jī)上,獲取到對(duì)應(yīng)的network的id,給了Cronet綁定后,Cronet使用的Android系統(tǒng)回調(diào)會(huì)拋出蜂窩網(wǎng)onLost的回調(diào),然后后續(xù)再繼續(xù)bindSocket的話(huà)就會(huì)一直失敗返回-1,這個(gè)問(wèn)題搞了我一周都沒(méi)找到解決辦法。
后面通過(guò)閱讀官網(wǎng)的文檔才發(fā)現(xiàn),雙通道的實(shí)現(xiàn)需要依賴(lài)手機(jī)打開(kāi)了 始終開(kāi)啟移動(dòng)數(shù)據(jù)設(shè)置這個(gè)開(kāi)關(guān),如果沒(méi)有打開(kāi)這個(gè)配置,一段時(shí)間后移動(dòng)網(wǎng)絡(luò)就會(huì)斷開(kāi)連接,常規(guī)網(wǎng)絡(luò)回調(diào)將收到對(duì) onLost() 的調(diào)用。這些是來(lái)自官網(wǎng)的解釋?zhuān)C明如果不開(kāi)啟這個(gè)開(kāi)關(guān)是無(wú)法實(shí)現(xiàn)雙網(wǎng)卡的邏輯的。
雙網(wǎng)卡官方解答地址
總結(jié)
- 遇到問(wèn)題要多點(diǎn)看回android的開(kāi)發(fā)文檔,說(shuō)不定就能找到你的答案了。
- 在學(xué)習(xí)中進(jìn)步,在實(shí)踐中收獲知識(shí)。