场景
为了追求极致的用户体验,每个app都耗尽脑汁想尽办法优化自身,特别是网络卡顿时候的体验,期待在wifi卡顿情况下,通过白名单控制域名走用户的蜂窝网络通道。这期主要分享下wifi连接情况下,如何用蜂窝网发送相应的请求。后续还会着重分析一个问题,卡了自己好久的问题,后面也是看了android官方文档解释才明白为啥出这个问题。
介绍
一、系统Api
首先我们要了解下系统的api实现,主要就是Network#bindSocket方法,这个方法是获取对应通道的network对象,再把客户端网络传输的socket绑定到对应通道的network对象上,从而把对应的网络请求切换到不同通道上,实现双网卡交互的逻辑。
二、Cronet底层如何实现双通道逻辑
了解系统的Api后,我们对整体android的系统Api有个认知,那接下来我根据源码和大家简单分析下Cronet底层socket如何绑定对应的网络通道的。看下面Cronet的代码,可以看出Cronet c++底层也是加载native库,通过打开Android底层的文件句柄,直接访问Network#bindSocket的C++方法,所以说为什么我在第一步先介绍Android系统的Api,各种框架八九不离十都会和系统的api打交道的,只不过是分走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协议分析Cronet如何指定网络通道。下面代码可以看出函数参数需要一个叫NetworkHandle一个对象,其实这个对象就是Java的Network的id,这个Android系统api也有方法可以参考copy一份的,那么到这里整个链路都比较清晰了。要Cronet走不同的网络通道,首先要获取对应通道的Network的id,其次通过id在Cronet决定走tcp/udp协议后,本地创建的socket绑定对应的id,就可以实现不同网络通道传输啦。
//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)
}
四、如何获取对应的Network的id
我这里其实不是最好的实现,因为我也是看网上和官网的文档后,只能想到这种方式去获取。但这个方式有个致命缺陷,需要用户同意打开某个高级权限才可以获取到对应的Network的id。但是微信是可以做到不需要用户打开权限,网络差的情况下直接走蜂窝网通道拉取消息,这个我暂时没想到能咋整。
下面代码需要申请权限:
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS"/>
主要是WRITE_SETTINGS在高版本手机需要用户主动选择打开才行,动态获取Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS)就可以跳转到设置页面,让用户去打开了。
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()
// 设置指定的⽹络传输类型(蜂窝传输) 等于⼿机⽹络
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
// 设置感兴趣的⽹络功能
.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;
}
}
}
问题
在实际开发过程中遇到个非常棘手问题,在某些国内手机上,获取到对应的network的id,给了Cronet绑定后,Cronet使用的Android系统回调会抛出蜂窝网onLost的回调,然后后续再继续bindSocket的话就会一直失败返回-1,这个问题搞了我一周都没找到解决办法。
后面通过阅读官网的文档才发现,双通道的实现需要依赖手机打开了 始终开启移动数据设置这个开关,如果没有打开这个配置,一段时间后移动网络就会断开连接,常规网络回调将收到对 onLost() 的调用。这些是来自官网的解释,证明如果不开启这个开关是无法实现双网卡的逻辑的。
双网卡官方解答地址
总结
- 遇到问题要多点看回android的开发文档,说不定就能找到你的答案了。
- 在学习中进步,在实践中收获知识。
网友评论