2015.11.18 - 12.09
个人英文阅读练习笔记。原文地址:http://developer.android.com/training/building-connectivity.html。
2015.11.18
此部分内容展示如何编写连接到用户设备之外的世界中去。即怎么在局域中“连接其它设备”、“连接互联网”、“备份及同步应用程序的数据”等内容。
如何用网络服务搜索来找到并连接到本地的设备,在Wi-Fi下如何创建对等连接。
除了能和云通信之外,安卓的无线APIs也能够和在相同局域网内的其它设备通信,设置能够和不在网络中单在附近的设备通信。添加的网络服务搜索(NSD)允许应用程序搜索在附近的运行着它(应用程序)能够通信的服务程序的设备。将这些功能集成到应用程序中将会给应用程序提供很多特点,如跟用户在同一个房间内玩游戏,从有NSD的网络摄像头处获取图片或登录到同网络中的其它机器之上。
此节描述在应用程序中用于寻找并连接到其它设备的主要的APIs。尤其地,此节将会描述用于搜索可用服务的NSD API以及用于P2P无线连接的Wi-Fi P2P API。此节同样会介绍如何联合应用NSD和Wi-Fi P2P来检测又某个设备提供的服务,并在其中一个设备连接到网络时连接到此设备上。
2015.11.19
学习如何“广播您的应用程序所提供的服务”、“发现局域网提供的服务”以及“用NSD来判断欲连接服务的细节”。
将网络服务搜索(NSD)添加到应用程序能够让用户识别在局域网络中支持该应用程序所请求的服务的设备。这对很多P2P诸如文件分享或多媒体播放游戏的应用程序很有用。安卓的NSD APIs简化了这些特性的实现。
此节展示如何构建一个能够广播其名字和连接信息到网络并能够扫描其它应用程序相同信息的应用程序。最后,此节介绍如何连接到运行相同程序的另一台设备上。
注:此步是可选的。如果并不关心应用程序在局域网上的广播服务,可直接跳到下一节内容。
欲在网络中注册服务,首先需要创建NsdServiceInfo对象。此对象提供在网络中的其它设备需要连接此设备服务的信息。
public void registerService(int port) {
// Create the NsdServiceInfo object, and populate it.
NsdServiceInfo serviceInfo = new NsdServiceInfo();
// The name is subject to change based on conflicts
// with other services advertised on the same network.
serviceInfo.setServiceName("NsdChat");
serviceInfo.setServiceType("_http._tcp.");
serviceInfo.setPort(port);
....
}
此代码片段将此服务命名为“NsdChat”。此名对在此网络中用NSD来搜素当地服务的所有设备都可用。需要保持此名在网络中的唯一性,安卓也会自动处理命名冲突。如果在网络上同时安装了两个名为NsdChat的应用程序,其中一个会自动更改其名为类似“NsdChat(1)”的名字。
第二个参数设置服务类型,用来指明应用程序所使用的协议和传输层。参数语法为“<协议>.<传输层>”。在代码片段中,在TCP层上使用HTTP协议。若应用程序提供了打印服务(如网络打印),则应该将服务类型设置为“_ipp._tcp”。
注:国际数字分配局(IANA)管理集中的、当局的被诸如NSD和Bonjour使用的服务搜索协议的服务类型列表。可以从the IANA list of service names and port numbers下载此列表。如果想使用新的服务类型,需要通过填写IANA Ports and Service registration form来预订。
在为服务设置端口时,避免硬编码,因为这可能会跟其它的应用程序冲突。例如,假设您的应用程序总是使用1337端口,那么就有可能和已经安装到网络的使用相同端口的其它应用程序冲突。取代硬编码的方式是使用设备的下一个可用端口。因为此信息将会通过服务广播的方式提供给其它的应用程序,在编译时期就没有必要让其它的应用程序知道你的应用程序所使用的端口。其它应用程序在连接到您的设备之前可以通过你的应用程序的服务广播而获得端口信息。
如果使用套接字(sockets),以下代码展示了通过将socket设置为0获取了可用的端口:
public void initializeServerSocket() {
// Initialize a server socket on the next available port.
mServerSocket = new ServerSocket(0);
// Store the chosen port.
mLocalPort = mServerSocket.getLocalPort();
...
}
至此,已经定义了NsdServiceInfo对象,接下来需要实现RegistrationListener接口。此接口中包含了安卓用来通知应用程序服务注册/注销是否成功的回调方法。
public void initializeRegistrationListener() {
mRegistrationListener = new NsdManager.RegistrationListener() {
@Override
public void onServiceRegistered(NsdServiceInfo NsdServiceInfo) {
// Save the service name. Android may have changed it in order to
// resolve a conflict, so update the name you initially requested
// with the name Android actually used.
mServiceName = NsdServiceInfo.getServiceName();
}
@Override
public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
// Registration failed! Put debugging code here to determine why.
}
@Override
public void onServiceUnregistered(NsdServiceInfo arg0) {
// Service has been unregistered. This only happens when you call
// NsdManager.unregisterService() and pass in this listener.
}
@Override
public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode) {
// Unregistration failed. Put debugging code here to determine why.
}
};
}
现在可以调用registerService()方法来注册服务了。
注意祖册服务的方法是异步的,所以所有在注册服务后方执行的操作都要放在onServiceRegistered()中:
public void registerService(int port) {
NsdServiceInfo serviceInfo = new NsdServiceInfo();
serviceInfo.setServiceName("NsdChat");
serviceInfo.setServiceType("_http._tcp.");
serviceInfo.setPort(port);
mNsdManager = Context.getSystemService(Context.NSD_SERVICE);
mNsdManager.registerService(
serviceInfo, NsdManager.PROTOCOL_DNS_SD, mRegistrationListener);
}
网络四处充满生机,从简单的网络打印机到网络摄像头,到各种联网的游戏。让应用程序加入到网络中的关键是使用服务搜索功能。应用程序需要监听网络中的广播信息以确定哪些服务是可用的并过滤掉应用程序不支持的部分。
类似服务注册,实现服务搜索有两个步骤:在应用程序中用给定的回调方法监听搜索,并调用异步的discoverServices()方法。
首先,初始化无名类来实现NsdManager.DiscoveryListener。示例如下:
public void initializeDiscoveryListener() {
// Instantiate a new DiscoveryListener
mDiscoveryListener = new NsdManager.DiscoveryListener() {
// Called as soon as service discovery begins.
@Override
public void onDiscoveryStarted(String regType) {
Log.d(TAG, "Service discovery started");
}
@Override
public void onServiceFound(NsdServiceInfo service) {
// A service was found! Do something with it.
Log.d(TAG, "Service discovery success" + service);
if (!service.getServiceType().equals(SERVICE_TYPE)) {
// Service type is the string containing the protocol and
// transport layer for this service.
Log.d(TAG, "Unknown Service Type: " + service.getServiceType());
} else if (service.getServiceName().equals(mServiceName)) {
// The name of the service tells the user what they'd be
// connecting to. It could be "Bob's Chat App".
Log.d(TAG, "Same machine: " + mServiceName);
} else if (service.getServiceName().contains("NsdChat")){
mNsdManager.resolveService(service, mResolveListener);
}
}
@Override
public void onServiceLost(NsdServiceInfo service) {
// When the network service is no longer available.
// Internal bookkeeping code goes here.
Log.e(TAG, "service lost" + service);
}
@Override
public void onDiscoveryStopped(String serviceType) {
Log.i(TAG, "Discovery stopped: " + serviceType);
}
@Override
public void onStartDiscoveryFailed(String serviceType, int errorCode) {
Log.e(TAG, "Discovery failed: Error code:" + errorCode);
mNsdManager.stopServiceDiscovery(this);
}
@Override
public void onStopDiscoveryFailed(String serviceType, int errorCode) {
Log.e(TAG, "Discovery failed: Error code:" + errorCode);
mNsdManager.stopServiceDiscovery(this);
}
};
}
当失败或服务在找到后又丢失时,NSD API调用此接口中的方法来告知应用程序搜索(发现,discovery)在何时开启。注意当服务找到时此代码会做多次核查。
[1]. 将搜寻到的服务名和局域服务中的服务名比较,判断是否是自己本身的广播(有效)。
[2]. 检查服务类型,确定是否为本应用程序能够连接的类型。
[3]. 检查服务名以去顶是否连接到了正确的应用程序。
对服务名的检查不总是必要的,它只具有表明是否确认连接到一个特定的应用程序。例如,应用程序只想连接正在其它设备上运行的本应用程序。然而,如果应用想连接网络打印机,检查服务类型是否为”_ipp._tcp”即可。
在设置监听器后,将应用程序将寻找的服务类型、搜索的协议以及所设置的监听器传递给discoverServices()方法。
mNsdManager.discoverServices(
SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener);
当在网络上寻找到一个可以连接的服务时,首先必须用resolveService()方法来判断该服务的连接信息。实现NsdManager.ResolveListener将连接信息传递到该方法中,并用此实现获得包含连接信息的NsdServiceInfo。
public void initializeResolveListener() {
mResolveListener = new NsdManager.ResolveListener() {
@Override
public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) {
// Called when the resolve fails. Use the error code to debug.
Log.e(TAG, "Resolve failed" + errorCode);
}
@Override
public void onServiceResolved(NsdServiceInfo serviceInfo) {
Log.e(TAG, "Resolve Succeeded. " + serviceInfo);
if (serviceInfo.getServiceName().equals(mServiceName)) {
Log.d(TAG, "Same IP.");
return;
}
mService = serviceInfo;
int port = mService.getPort();
InetAddress host = mService.getHost();
}
};
}
一旦确认了服务,应用程序将会收到服务包含IP地址和端口号的详细信息。这是应用程序连接到服务的所需要的。
在应用程序生命周期合适的时间开启/关闭NSD功能都非常的重要。在应用程序关闭时注销其网络服务,以避免其它的应用程序还认为已经被关闭的应用程序可连。同时,服务搜索也是一个耗费很大的操作,当父活动暂停时服务搜索也应该被停止,当父活动恢复时再将服务搜索恢复。在主活动中重写生命周期方法来合适地开始或停止广播和搜索:
//In your application's Activity
@Override
protected void onPause() {
if (mNsdHelper != null) {
mNsdHelper.tearDown();
}
super.onPause();
}
@Override
protected void onResume() {
super.onResume();
if (mNsdHelper != null) {
mNsdHelper.registerService(mConnection.getLocalPort());
mNsdHelper.discoverServices();
}
}
@Override
protected void onDestroy() {
mNsdHelper.tearDown();
mConnection.tearDown();
super.onDestroy();
}
// NsdHelper's tearDown method
public void tearDown() {
mNsdManager.unregisterService(mRegistrationListener);
mNsdManager.stopServiceDiscovery(mDiscoveryListener);
}
学习如何“提取附近同样设备的列表”、“为遗失的设备创建访问点”以及“连接拥有Wi-Fi P2P连接能力的设备”。
Wi-Fi的对等(P2P)连接APIs允许应用程序在不连接到网络或热点(安卓的Wi-Fi P2P框架结合Wi-Fi Direct认证程序)的情况下连接邻近的设备。Wi-FI P2P允许应用程序快速找到并和邻近(超过蓝牙连接的距离)的设备互动。
此节展示如何用Wi-Fi P2P来寻找和连接邻近的设备。
2015.11.20
欲使用Wi-Fi P2P,将CHANGE_WIFI_STATE、ACCESS_WIFI_STATE和INTERNET权限添加到清单文件中。Wi-Fi P2P不需要意图(intent)连接,但会使用需要INTERNET权限的标准的Java套接字。所以,欲使用Wi-Fi P2P则需要以下权限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.nsdchat"
...
<uses-permission
android:required="true"
android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission
android:required="true"
android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission
android:required="true"
android:name="android.permission.INTERNET"/>
...
欲使用Wi-Fi P2P,需要监听通知应用程序何时有确定事件发生的广播意图。在应用程序中,初始化IntentFilter并设置它去监听以下事件:
WIFI_P2P_STATE_CHANGED_ACTION
表明Wi-Fi P2P是否开启
WIFI_P2P_PEERS_CHANGED_ACTION
表明可用的同行(同等)列表已经改变
WIFI_P2P_CONNECTION_CHANGED_ACTION
表明Wi-Fi P2P的连接状态已经改变
WIFI_P2P_THIS_DEVICE_CHANGED_ACTION
表明设备的配置细节已经改变
private final IntentFilter intentFilter = new IntentFilter();
...
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Indicates a change in the Wi-Fi P2P status.
intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
// Indicates a change in the list of available peers.
intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
// Indicates the state of Wi-Fi P2P connectivity has changed.
intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
// Indicates this device's details have changed.
intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
...
}
在onCreate()方法的末尾,获取wifiP2pManager的实例并调用其initialize()方法。此方法返回wifiP2pManager.Channel对象,稍后应用程序会用此对象连接到Wi-Fi P2P框架。
@Override
Channel mChannel;
public void onCreate(Bundle savedInstanceState) {
....
mManager = (WifiP2pManager) getSystemService(Context.WIFI_P2P_SERVICE);
mChannel = mManager.initialize(this, getMainLooper(), null);
}
至此,可以创建一个新的BroadcastReceiver类来监听系统Wi-Fi P2P状态的改变。在onReceive()方法中,增加一个条件来处理以上所列举的P2P状态的改变。
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION.equals(action)) {
// Determine if Wifi P2P mode is enabled or not, alert
// the Activity.
int state = intent.getIntExtra(WifiP2pManager.EXTRA_WIFI_STATE, -1);
if (state == WifiP2pManager.WIFI_P2P_STATE_ENABLED) {
activity.setIsWifiP2pEnabled(true);
} else {
activity.setIsWifiP2pEnabled(false);
}
} else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
// The peer list has changed! We should probably do something about
// that.
} else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {
// Connection state changed! We should probably do something about
// that.
} else if (WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION.equals(action)) {
DeviceListFragment fragment = (DeviceListFragment) activity.getFragmentManager()
.findFragmentById(R.id.frag_list);
fragment.updateThisDevice((WifiP2pDevice) intent.getParcelableExtra(
WifiP2pManager.EXTRA_WIFI_P2P_DEVICE));
}
}
最后,添加代码来实现当主活动活跃时注册意图过滤器和广播接收器并在活动暂停时注销它们。做这两个过程的最佳地方是在onResume()和onPause()方法中。
/** register the BroadcastReceiver with the intent values to be matched */
@Override
public void onResume() {
super.onResume();
receiver = new WiFiDirectBroadcastReceiver(mManager, mChannel, this);
registerReceiver(receiver, intentFilter);
}
@Override
public void onPause() {
super.onPause();
unregisterReceiver(receiver);
}
欲开始用Wi-Fi P2P来搜索邻近的设备,调用discoverPeers()。此方法带以下参数:
- 当初始化P2P管理器时回收到的wifiP2pManager.Channel。
- 系统为搜索(成功或失败)调用方法实现的wifiP2pManager.ActionListener。
mManager.discoverPeers(mChannel, new WifiP2pManager.ActionListener() {
@Override
public void onSuccess() {
// Code for when the discovery initiation is successful goes here.
// No services have actually been discovered yet, so this method
// can often be left blank. Code for peer discovery goes in the
// onReceive method, detailed below.
}
@Override
public void onFailure(int reasonCode) {
// Code for when the discovery initiation fails goes here.
// Alert the user that something went wrong.
}
});
记住这仅初始化对等的搜索(发现)。discoverPeers()方法开启发现(搜索)过程然后理解返回。若通过调用在提供的动作监听器中的方法成功处世话了发现过程,则系统将会通知应用程序。同样,直到连接被初始化或P2P组形成后发现将保持活跃。
现在可以编写提取并处理同等者列表的代码了。首先实现wifiP2pManager.PeerListListener接口,此接口将提供关于检测到的对等Wi-Fi P2P的信息。以下代码示例此过程。
private List peers = new ArrayList();
...
private PeerListListener peerListListener = new PeerListListener() {
@Override
public void onPeersAvailable(WifiP2pDeviceList peerList) {
// Out with the old, in with the new.
peers.clear();
peers.addAll(peerList.getDeviceList());
// If an AdapterView is backed by this data, notify it
// of the change. For instance, if you have a ListView of available
// peers, trigger an update.
((WiFiPeerListAdapter) getListAdapter()).notifyDataSetChanged();
if (peers.size() == 0) {
Log.d(WiFiDirectActivity.TAG, "No devices found");
return;
}
}
}
当收到包含WIFI_P2P_PEERS_CHANGED_ACITON动作的意图是修改广播接收器的onReceive()方法来调用requestPeerI()。需要将监听器传递给接收器。其中一种方式是将监听器作为参数传递给广播接收器的构造函数。
public void onReceive(Context context, Intent intent) {
...
else if (WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION.equals(action)) {
// Request available peers from the wifi p2p manager. This is an
// asynchronous call and the calling activity is notified with a
// callback on PeerListListener.onPeersAvailable()
if (mManager != null) {
mManager.requestPeers(mChannel, peerListListener);
}
Log.d(WiFiDirectActivity.TAG, "P2P peers changed");
}...
}
现在,一个包含WIFI_P2P_PEERS_CHANGED_ACITON动作的意图能够触发更新对等这列表的请求。
欲连接对等者,创建一个新的wifiP2pConfig对象,并将欲连接的设备的数据拷贝到此对象中。然后调用connect()方法。
@Override
public void connect() {
// Picking the first device found on the network.
WifiP2pDevice device = peers.get(0);
WifiP2pConfig config = new WifiP2pConfig();
config.deviceAddress = device.deviceAddress;
config.wps.setup = WpsInfo.PBC;
mManager.connect(mChannel, config, new ActionListener() {
@Override
public void onSuccess() {
// WiFiDirectBroadcastReceiver will notify us. Ignore for now.
}
@Override
public void onFailure(int reason) {
Toast.makeText(WiFiDirectActivity.this, "Connect failed. Retry.",
Toast.LENGTH_SHORT).show();
}
});
}
此代码片段中实现的WifiP2pManager.ActionListener在初始化成功或失败时会发出通知。欲监听连接状态的改变,实现WifiP2pManager.ConnecionInfoListener接口。当连接状态改变时,其onConnectionInfoAvailable()回调方法将会给出通知。在多个设备准备理解到单个设备时(如一个游戏有3个或多个玩家或聊天程序),此设备将会被设计为“组拥有者”。
@Override
public void onConnectionInfoAvailable(final WifiP2pInfo info) {
// InetAddress from WifiP2pInfo struct.
InetAddress groupOwnerAddress = info.groupOwnerAddress.getHostAddress());
// After the group negotiation, we can determine the group owner.
if (info.groupFormed && info.isGroupOwner) {
// Do whatever tasks are specific to the group owner.
// One common case is creating a server thread and accepting
// incoming connections.
} else if (info.groupFormed) {
// The other device acts as the client. In this case,
// you'll want to create a client thread that connects to the group
// owner.
}
}
现在回到广播接收器的onReceive()方法中,修改监听WIFI_P2P_CONNECTION_CHANGED_ACTION意图的模块。当收到此意图时,调用requestConnectionInfo()。这是一个异步的调用,所以结果会被连接信息监听器作为一个参数给获取。
...
} else if (WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION.equals(action)) {
if (mManager == null) {
return;
}
NetworkInfo networkInfo = (NetworkInfo) intent
.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);
if (networkInfo.isConnected()) {
// We are connected with the other device, request connection
// info to find group owner IP
mManager.requestConnectionInfo(mChannel, connectionListener);
}
...
学习如何用Wi-Fi P2P发现附近没有在同一网络下但发行服务的设备。
上一节介绍了如何发现局域网中的服务。使用Wi-Fi P2P服务搜索能够直接连接到临近没有连接到网络中的设备。同时也可为正运行在设备上的服务广播消息。此能力能够帮助应用程序之间的通信,即使没有可用的局域网或热点。
此节介绍的APIs跟前一节中NSD的APIs的框架、目的相似,在代码中调用它们的方式却跟前一节大不相同。此节介绍如何用Wi-Fi P2P发现其他设备上的可用服务。此节假设读者对Wi-Fi P2P API熟悉了。
欲使用Wi-Fi P2P,将CHANGE_WIFI_STATE,ACCESS_WIFI_STATE和INTERNET 权限添加到应用程序的清单文件中。尽管Wi-Fi P2P不需要意图连接,但是它使用标准的java套接字,使用这些权限安卓需要请求以下权限:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.nsdchat"
...
<uses-permission
android:required="true"
android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission
android:required="true"
android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission
android:required="true"
android:name="android.permission.INTERNET"/>
...
如果打算提供本地服务,则需要为服务发现注册服务。一旦注册了本地服务,框架会自动响应来自同等设备的服务搜索。
欲创建本地服务:
[1]. 创建WifiP2pServiceInfo对象。
[2]. 用服务的信息填充该对象。
[3]. 调用 addLocalService()为服务搜索注册本地服务。
private void startRegistration() {
// Create a string map containing information about your service.
Map record = new HashMap();
record.put("listenport", String.valueOf(SERVER_PORT));
record.put("buddyname", "John Doe" + (int) (Math.random() * 1000));
record.put("available", "visible");
// Service information. Pass it an instance name, service type
// _protocol._transportlayer , and the map containing
// information other devices will want once they connect to this one.
WifiP2pDnsSdServiceInfo serviceInfo =
WifiP2pDnsSdServiceInfo.newInstance("_test", "_presence._tcp", record);
// Add the local service, sending the service info, network channel,
// and listener that will be used to indicate success or failure of
// the request.
mManager.addLocalService(channel, serviceInfo, new ActionListener() {
@Override
public void onSuccess() {
// Command successful! Code isn't necessarily needed here,
// Unless you want to update the UI or add logging statements.
}
@Override
public void onFailure(int arg0) {
// Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY
}
});
}
安卓用回调方法来通知应用程序可用的服务,所以首先要设置这些回调方法。创建一个来监听 WifiP2pManager.DnsSdTxtRecordListener输入的记录。此记录能被其他设备广播。当输入一个记录时,拷贝设备的地址以及其它想要的信息到方法设定的数据结构中,这样便于后续访问。以下代码假设记录包含了“好友名称”域,此记录被用户的确认操作填充。
final HashMap<String, String> buddies = new HashMap<String, String>();
...
private void discoverService() {
DnsSdTxtRecordListener txtListener = new DnsSdTxtRecordListener() {
@Override
/* Callback includes:
* fullDomain: full domain name: e.g "printer._ipp._tcp.local."
* record: TXT record dta as a map of key/value pairs.
* device: The device running the advertised service.
*/
public void onDnsSdTxtRecordAvailable(
String fullDomain, Map record, WifiP2pDevice device) {
Log.d(TAG, "DnsSdTxtRecord available -" + record.toString());
buddies.put(device.deviceAddress, record.get("buddyname"));
}
};
...
}
欲获取服务信息,创建一个WifiP2pManager.DnsSdServiceResponseListener。此能够接收实际的描述和连接信息。之前的代码片段实现Map对象用好友名称来配对设备地址。此服务响应监听器用此连接到用服务信息记录的DNS。所有的监听器一旦都实现,用setDnsSdResponseListeners()方法将它们增加到WifiP2pManager。
private void discoverService() {
...
DnsSdServiceResponseListener servListener = new DnsSdServiceResponseListener() {
@Override
public void onDnsSdServiceAvailable(String instanceName, String registrationType,
WifiP2pDevice resourceType) {
// Update the device name with the human-friendly version from
// the DnsTxtRecord, assuming one arrived.
resourceType.deviceName = buddies
.containsKey(resourceType.deviceAddress) ? buddies
.get(resourceType.deviceAddress) : resourceType.deviceName;
// Add to the custom adapter defined specifically for showing
// wifi devices.
WiFiDirectServicesList fragment = (WiFiDirectServicesList) getFragmentManager()
.findFragmentById(R.id.frag_peerlist);
WiFiDevicesAdapter adapter = ((WiFiDevicesAdapter) fragment
.getListAdapter());
adapter.add(resourceType);
adapter.notifyDataSetChanged();
Log.d(TAG, "onBonjourServiceAvailable " + instanceName);
}
};
mManager.setDnsSdResponseListeners(channel, servListener, txtListener);
...
}
现在创建一个服务请求并调用addServiceRequest()。此方法同样用监听器来表明成功或失败。
serviceRequest = WifiP2pDnsSdServiceRequest.newInstance();
mManager.addServiceRequest(channel,
serviceRequest,
new ActionListener() {
@Override
public void onSuccess() {
// Success!
}
@Override
public void onFailure(int code) {
// Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY
}
});
最后,调用discoverServices()。
mManager.discoverServices(channel, new ActionListener() {
@Override
public void onSuccess() {
// Success!
}
@Override
public void onFailure(int code) {
// Command failed. Check for P2P_UNSUPPORTED, ERROR, or BUSY
if (code == WifiP2pManager.P2P_UNSUPPORTED) {
Log.d(TAG, "P2P isn't supported on this device.");
else if(...)
...
}
});
如果以上所有步骤进行得都很顺利,阿弥陀佛,完成本节内容。如果其中还有些问题,检查是否异步调用中是否都包含了WifiP2pManager.ActionListener参数,此参数同时还能够表明操作是否成功。欲检测问题,增onFailure()中添加调试的代码。由此方法提供的错误代码将暗示其中的问题。以下是可能出现的错误的值:
P2P_UNSUPPORTED
运行该应用程序的设备不支持Wi-Fi P2P。
BUSY
系统过忙而不能处理请求
ERROR
由于内部错误导致操作失败
如何“创建网络连接”、“检测连接变化”以及“用XML数据执行交易”。
此节解释关于“连接网络”、“监控网络(包括连接变化)”以及“给用户控制应用程序的网络使用量”的基本任务。并描述如何解析和使用XML数据。
此节包含了一个用来演示如何执行常见的网络操作的程序样例。可以下载(在右侧)并重复使用其中的代码。
通过学习此节内容,您将拥有创建能够最大化减小网络赌赛、高效下载和解析数据的应用程序的基本模块。
注:见Transmitting Network Data Using Volley类中的迸发信息,HTTP库使得安卓应用程序能更快更简单的访问网络。通过打开AOSP知识库可以访问迸发。迸发可能可以帮助提升应用程序的网络操作的流水线和性能。
2015.11.21
学习如何“连接网络”、“选择HTTP客户端”以及“在用户界面线程之外的线程执行网络操作”。
此节展示如何实现一个能连接到网络的简单的应用程序。解释了一个最基本的联网应用程序应该需要包含的一些最好(必要)的步骤。
注意在此节中执行的网络操作需要在清单文件中包含以下权限:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
大多数的网络连接的安卓应用程序使用HTTP来发送和接收数据。安卓平台包括HttpURLConnection客户端,它支持HTTPS、上传和下载、超时配置、IPv6以及连接池。
在应用程序连接到网络之前,应该用getActiveNetworkInfo()和isConnected()方法检查是否有可用的网络连接。设备有可能在网络所在的范围之外,或者用户关闭了Wi-Fi和移动数据访问功能。关于此话题的更多讨论见 Managing Network Usage。
public void myClickHandler(View view) {
...
ConnectivityManager connMgr = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isConnected()) {
// fetch data
} else {
// display error
}
...
}
网络操作可能会涉及到不可预测的延迟。欲阻止此事件发生以带来差劲的用户体验,常在一个独立于主用户界面线程的线程中处理网络操作。AsyncTask类提供了一种创建新任务最简单的方式从而脱离用户界面所在的线程。关于此话题的更多讨论见Multithreading For Performance。
在以下的代码片中,myClickHandler()方法调用new DownloadWebpageTask().execute(stringUrl)。DownloadWebpageTask 类是AsyncTask的一个子类。DownloadWebpageTask 实现了AsyncTask中的以下方法:
- doInBackground()执行downloadUrl()方法。它将网页URL作为参数传递。downloadUrl()方法提取并处理网页内容。当此函数执行完毕后,传回一个字符串的结果。
- onPostExecute()方法获取以上方法返回的字符串并在用户界面中展示。
public class HttpExampleActivity extends Activity {
private static final String DEBUG_TAG = "HttpExample";
private EditText urlText;
private TextView textView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
urlText = (EditText) findViewById(R.id.myUrl);
textView = (TextView) findViewById(R.id.myText);
}
// When user clicks button, calls AsyncTask.
// Before attempting to fetch the URL, makes sure that there is a network connection.
public void myClickHandler(View view) {
// Gets the URL from the UI's text field.
String stringUrl = urlText.getText().toString();
ConnectivityManager connMgr = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
if (networkInfo != null && networkInfo.isConnected()) {
new DownloadWebpageTask().execute(stringUrl);
} else {
textView.setText("No network connection available.");
}
}
// Uses AsyncTask to create a task away from the main UI thread. This task takes a
// URL string and uses it to create an HttpUrlConnection. Once the connection
// has been established, the AsyncTask downloads the contents of the webpage as
// an InputStream. Finally, the InputStream is converted into a string, which is
// displayed in the UI by the AsyncTask's onPostExecute method.
private class DownloadWebpageTask extends AsyncTask<String, Void, String> {
@Override
protected String doInBackground(String... urls) {
// params comes from the execute() call: params[0] is the url.
try {
return downloadUrl(urls[0]);
} catch (IOException e) {
return "Unable to retrieve web page. URL may be invalid.";
}
}
// onPostExecute displays the results of the AsyncTask.
@Override
protected void onPostExecute(String result) {
textView.setText(result);
}
}
...
}
此代码片段中处理的事件如下:
[1]. 当用户点击按钮时触发调用myClickHandler(),应用程序传递制定的URL给AsyncTask的子类DownloadWebpageTask。
[2]. AsyncTask中的doInBackground()方法调用downloadUrl()方法。
[3]. downloadUrl()方法携带URL字符串参数并用它创建URL对象。
[4]. 用URL对象建立HttpURLConnection。
[5]. 一段建立了链接,HttpURLConnection对象将网页内容作为InputStream提取。
[6]. InputStream被传递给readIt()方法,此方法将输入流转换为字符串。
[7]. 最后,AsyncTask的onPostExecute()方法将字符串展示在主用户界面中。
如果在新建的线程中执行网络交易,可以用HttpURLConnection来执行GET和下载数据的操作。在调用connect()之后,通过调用getInputStream()可以获得InputStream。
在下面的代码片段中,doInBackground()方法调用downloadUrl()方法。downloadUrl()方法用给定的RUL去通过HttpURLConnection连接到网络。一旦建立了网络连接,应用程序将会使用getInputStream()以InputStream的方式检索数据。
// Given a URL, establishes an HttpUrlConnection and retrieves
// the web page content as a InputStream, which it returns as
// a string.
private String downloadUrl(String myurl) throws IOException {
InputStream is = null;
// Only display the first 500 characters of the retrieved
// web page content.
int len = 500;
try {
URL url = new URL(myurl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(10000 /* milliseconds */);
conn.setConnectTimeout(15000 /* milliseconds */);
conn.setRequestMethod("GET");
conn.setDoInput(true);
// Starts the query
conn.connect();
int response = conn.getResponseCode();
Log.d(DEBUG_TAG, "The response is: " + response);
is = conn.getInputStream();
// Convert the InputStream into a string
String contentAsString = readIt(is, len);
return contentAsString;
// Makes sure that the InputStream is closed after the app is
// finished using it.
} finally {
if (is != null) {
is.close();
}
}
}
注意getResponseCode() 方法将返回连接的状态码。这对于获取关于连接的额外信息很有用。若状态码为200则表示连接成功。
InputStream是可读的字节资源。一旦获得了InputStream,一般都需要对其编码或将其转换为目标数据类型。例如,如果当前正在下载图片数据,那么就可能需要像以下代码这样编码:
InputStream is = null;
...
Bitmap bitmap = BitmapFactory.decodeStream(is);
ImageView imageView = (ImageView) findViewById(R.id.image_view);
imageView.setImageBitmap(bitmap);
在以上代码中,InputStream代表网页的文本内容。以下代码展示如何将这InputStream转换为字符串以在用户界面中显示:
// Reads an InputStream and converts it to a String.
public String readIt(InputStream stream, int len) throws IOException, UnsupportedEncodingException {
Reader reader = null;
reader = new InputStreamReader(stream, "UTF-8");
char[] buffer = new char[len];
reader.read(buffer);
return new String(buffer);
}
学习如何“检查设备的网络连接”、“创建一个令人喜欢的用户界面来控制网络使用量”以及“响应连接改变”。
此节描述如何编写一个能够细粒度地控制网络资源使用量的应用程序。如果应用程序将处理大量的网络操作,应该在应用程序中提供用户设置来允许用户控制应用程序的数据习惯,如多久同步数据、是否只在Wi-Fi连接下才上传/下载数据、当漫游时是否使用数据等。应用程序拥有这些可用的设置,当到达设备网络限制时运行在后台处理数据的此应用程序就更小可能失去网络处理能力,因为此应用程序可以被设置成精细的网络流量使用模式。
对于如何编写最小化下载和网络连接对电池声明的影响,见Optimizing Battery Life 和 Transferring Data Without Draining the Battery。
一个设备可拥有多种类型的网络连接。此节集中于Wi-Fi和移动网络连接。对于可能的网络连接类型,见ConnectivityManager。
Wi-Fi是一种典型的较快的类型。另外,移动数据是计量的,此种方式比较昂贵。一种常见的策略是只在Wi-Fi网络可用的情况下应用程序才提取大数据。
在执行网络操作以前,检查网络连接状态是不错的实践。这可以防止应用程序不经意的用了错误的无线电。如果网络连接不可用,应用程序应该有对应的优雅的响应。欲检查网络是否连接,一般遵循以下步骤:
- ConnectivityManager: 回答关于网络连接状态的查询。同时在网络连接改变时它也会通知应用程序。
- NetworkInfo:检查给定网络类型接口的状态(当前是移动数据还是Wi-Fi).
以下代码测试Wi-Fi和移动流量的连接。判断这些网络接口是否可用(即网络是否可能可连)并且/或是否已经连接(即是否存在网络连接且是否可以建立套接字并传递数据):
private static final String DEBUG_TAG = "NetworkStatusExample";
...
ConnectivityManager connMgr = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
boolean isWifiConn = networkInfo.isConnected();
networkInfo = connMgr.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
boolean isMobileConn = networkInfo.isConnected();
Log.d(DEBUG_TAG, "Wifi connected: " + isWifiConn);
Log.d(DEBUG_TAG, "Mobile connected: " + isMobileConn);
注意不能基于网络是否“可用”来做决定。在执行网络操作以前应该调用isConnected()方法,因为此方法可以处理移动流量网络、飞行模式以及限制后台数据的情况。
一种更为简洁的检查是否有可用网络接口的方式如下。getActiveNetworkInfo()方法返回一个代表所找到的第一次连接的网络接口的NetworkInfo实例,若无任何连接过的接口则返回null(意味着互联网连接不可用):
public boolean isOnline() {
ConnectivityManager connMgr = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
return (networkInfo != null && networkInfo.isConnected());
}
可用NetworkInfo.DetailedState查询更多细粒度的状态,但很少有必要如此做。
可以实现一个优先的活动来让用户精确的控制应用程序的网络资源使用量。例如:
- 当设备连接到Wi-Fi网络时方允许用户上传视频。
- 可以基于诸如网络是否可用、时间戳等标准来决定是否同步数据。
欲编写支持网络访问及网络资源使用量管理的应用程序,在应用程序的清单文件汇总必须要拥有以下权限和意图过滤器。
- 清单文件中需要包含以下权限:
- android.permission.INTERNET - 允许应用程序打开网络套接字。
- android.permission.ACCESS_NETWORK_STATE - 允许应用程序访问网络信息。
- 可以为意图过滤器声明ACTION_MANAGE_NETWORK_USAGE动作(在Android 4.0中发布)来表明应用程序定义了提供选择来控制数据用量的活动。在应用程序中,ACTION_MANAGE_NETWORK_USAGE展示了管理网络数据用量的设置。当应用程序中包含允许用户设置网络使用量的活动时,应该在意图过滤器中声明此活动。在样例程序中,此动作被SettingsActivity类处理,此类展示了优先的用户界面以让用户决定何时下载种子。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.networkusage"
...>
<uses-sdk android:minSdkVersion="4"
android:targetSdkVersion="14" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
...>
...
<activity android:label="SettingsActivity" android:name=".SettingsActivity">
<intent-filter>
<action android:name="android.intent.action.MANAGE_NETWORK_USAGE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>
正如以上清单文件中的摘录,样例应用程序的SettingsActivity活动有一个ACTION_MANAGE_NETWORK_USAGE动作的意图过滤器。SettingsActivity是PreferenceActivity的子类。它提供了优先的显示(如图1所示)来供用户有以下选择:
- 是否为每个XML种子入口显示下载,还是只为每个种子连接。
- 是否在任何网络连接下都下载XML中的种子,还是只在Wi-Fi下下载。
图1. 优先活动
以下有SettingsActivity的实现。注意其实现了OnSharedPreferenceChangeListener。当用户改变优先时它将调用onSharedPreferenceChanged(),此方法将refreshDisplay 设置为真。的那个用户返回到主活动中时此将刷新显示:
public class SettingsActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Loads the XML preferences file
addPreferencesFromResource(R.xml.preferences);
}
@Override
protected void onResume() {
super.onResume();
// Registers a listener whenever a key changes
getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
}
@Override
protected void onPause() {
super.onPause();
// Unregisters the listener set in onResume().
// It's best practice to unregister listeners when your app isn't using them to cut down on
// unnecessary system overhead. You do this in onPause().
getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
}
// When the user changes the preferences selection,
// onSharedPreferenceChanged() restarts the main activity as a new
// task. Sets the refreshDisplay flag to "true" to indicate that
// the main activity should update its display.
// The main activity queries the PreferenceManager to get the latest settings.
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
// Sets refreshDisplay to true so that when the user returns to the main
// activity, the display refreshes to reflect the new settings.
NetworkActivity.refreshDisplay = true;
}
}
当用户在屏幕设置中改变优先时,应用程序都会给予相应的行为。在此代码片段中,应用程序在onStart()中检查优先的设置。如果设置和设备的网络连接匹配(例如,设置为Wi-Fi设备也连接的Wi-Fi网络),那么应用程序将开始下载种子并用下载过程刷新显示:
public class NetworkActivity extends Activity {
public static final String WIFI = "Wi-Fi";
public static final String ANY = "Any";
private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest";
// Whether there is a Wi-Fi connection.
private static boolean wifiConnected = false;
// Whether there is a mobile connection.
private static boolean mobileConnected = false;
// Whether the display should be refreshed.
public static boolean refreshDisplay = true;
// The user's current network preference setting.
public static String sPref = null;
// The BroadcastReceiver that tracks network connectivity changes.
private NetworkReceiver receiver = new NetworkReceiver();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Registers BroadcastReceiver to track network connection changes.
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
receiver = new NetworkReceiver();
this.registerReceiver(receiver, filter);
}
@Override
public void onDestroy() {
super.onDestroy();
// Unregisters BroadcastReceiver when app is destroyed.
if (receiver != null) {
this.unregisterReceiver(receiver);
}
}
// Refreshes the display if the network connection and the
// pref settings allow it.
@Override
public void onStart () {
super.onStart();
// Gets the user's network preference settings
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
// Retrieves a string value for the preferences. The second parameter
// is the default value to use if a preference value is not found.
sPref = sharedPrefs.getString("listPref", "Wi-Fi");
updateConnectedFlags();
if(refreshDisplay){
loadPage();
}
}
// Checks the network connection and sets the wifiConnected and mobileConnected
// variables accordingly.
public void updateConnectedFlags() {
ConnectivityManager connMgr = (ConnectivityManager)
getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeInfo = connMgr.getActiveNetworkInfo();
if (activeInfo != null && activeInfo.isConnected()) {
wifiConnected = activeInfo.getType() == ConnectivityManager.TYPE_WIFI;
mobileConnected = activeInfo.getType() == ConnectivityManager.TYPE_MOBILE;
} else {
wifiConnected = false;
mobileConnected = false;
}
}
// Uses AsyncTask subclass to download the XML feed from stackoverflow.com.
public void loadPage() {
if (((sPref.equals(ANY)) && (wifiConnected || mobileConnected))
|| ((sPref.equals(WIFI)) && (wifiConnected))) {
// AsyncTask subclass
new DownloadXmlTask().execute(URL);
} else {
showErrorPage();
}
}
...
}
最后只剩下 BroadcastReceiver的子类NetworkReceiver这一个代码片段了。当设备网络连接改变时,NetworkReceiver拦截CONNECTIVITY_ACTION动作,判断网络连接状态并设置wifiConnected 和 mobileConnected标志为真或假。这样设置的结果是,当用户下一次回到应用程序中时,应用程序只会下载最新的种子并更新显示(如果NetworkActivity.refreshDisplay被设置为真)。
设置不必要被调用的BroadcastReceiver 将会消耗系统资源。此样例在onCreate()中注册了BroadcastReceiver NetworkReceiver,并在onDestroy()中注销。此种方式比在清单文件中声明要轻量级许多,此能够在任何时候欢迎应用程序,即使几周都没有运行该应用程序。通过在主活动中注册和注销NetworkReceiver ,要确保用户离开应用程序后应用程序不会自动唤醒。如果在清单文件中声明,可以调用setComponentEnabledSetting()来开启和关闭此功能。
以下为一个NetworkReceiver的实现:
public class NetworkReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
ConnectivityManager conn = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = conn.getActiveNetworkInfo();
// Checks the user prefs and the network connection. Based on the result, decides whether
// to refresh the display or keep the current display.
// If the userpref is Wi-Fi only, checks to see if the device has a Wi-Fi connection.
if (WIFI.equals(sPref) && networkInfo != null && networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
// If device has its Wi-Fi connection, sets refreshDisplay
// to true. This causes the display to be refreshed when the user
// returns to the app.
refreshDisplay = true;
Toast.makeText(context, R.string.wifi_connected, Toast.LENGTH_SHORT).show();
// If the setting is ANY network and there is a network connection
// (which by process of elimination would be mobile), sets refreshDisplay to true.
} else if (ANY.equals(sPref) && networkInfo != null) {
refreshDisplay = true;
// Otherwise, the app can't download content--either because there is no network
// connection (mobile or Wi-Fi), or because the pref setting is WIFI, and there
// is no Wi-Fi connection.
// Sets refreshDisplay to false.
} else {
refreshDisplay = false;
Toast.makeText(context, R.string.lost_connection, Toast.LENGTH_SHORT).show();
}
}
学习如何解析和使用XML数据。
扩展标记语言(XML)是一套以计算机可处理格式编码文档的规则。XML格式在互联网共享数据文件中很流行。诸如新闻或博客这种会频繁跟新其内容的网址,常提供XML支持以支持其内容的改变。上传和解析XML数据时网络连接应用程序常见的任务。此节解释如何解析XML文档并使用需要的数据。
在Android上,我们推荐高效和具维护性的XmlPullParser来解析XML。从以往来看,在安卓上需要实现此接口的两个方法中的其中一个:
- 通过XmlPullParserFactory.newPullParser()实现KXmlParser。
- 通过Xml.newPullParser()实现ExpatPullParser。
任何选择都可以。此节的样例通过通过Xml.newPullParser()实现ExpatPullParser。
解析进料的头异步是决定感兴趣的域。解析器会提取此部分域的数据并忽略其余部分。
此处有一段在应用程序中会被解析所摘录的域。每个在域中出现的 StackOverflow.com后缀作为嵌套几层标签的入口标签。
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:creativeCommons="http://backend.userland.com/creativeCommonsRssModule" ...">
<title type="text">newest questions tagged android - Stack Overflow</title>
...
<entry>
...
</entry>
<entry>
<id>http://stackoverflow.com/q/9439999</id>
<re:rank scheme="http://stackoverflow.com">0</re:rank>
<title type="text">Where is my data file?</title>
<category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="android"/>
<category scheme="http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest/tags" term="file"/>
<author>
<name>cliff2310</name>
<uri>http://stackoverflow.com/users/1128925</uri>
</author>
<link rel="alternate" href="http://stackoverflow.com/questions/9439999/where-is-my-data-file" />
<published>2012-02-25T00:30:54Z</published>
<updated>2012-02-25T00:30:54Z</updated>
<summary type="html">
<p>I have an Application that requires a data file...</p>
</summary>
</entry>
<entry>
...
</entry>
...
</feed>
此样例代码为入口(entry)标签及其嵌套其内的title、link以及summary标签提取数据。
下一步就该初始化解析器并揭开解析的序幕了。在以下代码片段中,被初始化的解析器不处理命名空间,并用提供的InputStream作为其输入。通过调用nextTag()和readFeed()方法开始解析处理,提取和处理应用程序感兴趣部分的数据:
public class StackOverflowXmlParser {
// We don't use namespaces
private static final String ns = null;
public List parse(InputStream in) throws XmlPullParserException, IOException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(in, null);
parser.nextTag();
return readFeed(parser);
} finally {
in.close();
}
}
...
}
readFeed()是实际处理进料的方法。它将寻找被标记为“入口”的标签作为开始点并递归的处理进料。对于不是入口的标签,它将忽略它。一旦整个进料都被递归的处理之后,readFeed()返回它从进料提取的包含条目(包含嵌套数据成员)的列表。此列表将被解析器返回。
private List readFeed(XmlPullParser parser) throws XmlPullParserException, IOException {
List entries = new ArrayList();
parser.require(XmlPullParser.START_TAG, ns, "feed");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
// Starts by looking for the entry tag
if (name.equals("entry")) {
entries.add(readEntry(parser));
} else {
skip(parser);
}
}
return entries;
}
解析XML进料的步骤如下:
[1]. 如 Analyze the Feed中的描述,确认欲在应用程序中包含的标签。此样例程序中提取入口和其下嵌套的title、link和summary标签的数据。
[2]. 创建以下方法:
- 为欲读的每个标签都创建一个“读”方法。例如,readEntry(),readTitle()等。解析器从输入流中读取标签。当遇到名为entry, title, link或summary的标签时,解析器将会为此标签调用相应的方法。否则,解析器将忽略此标签。
- 编写方法来实现为每个不同类型标签提取数据并提前让解析器到达下一个标签。例如:
- 对于title和summary标签,解析器调用readText()。此方法调用parser.getText()为标签提取数据。
- 对于link标签,解析器首先为此标签提取数据(若此标签下的数据需要被读的话)。然后再调用parser.getAttributeValue()来提取link的值。
- 对于entry标签,解析器调用readEntery()。此方法解析入口所嵌套的标签并返回包含title、link和summary数据的入口对象。
- sikp()帮助方法也是递归的。更多关于此话题见 “跳过不关心的标签”。
以下代码片段展示如何解析entries、titles、links以及summaries:
public static class Entry {
public final String title;
public final String link;
public final String summary;
private Entry(String title, String summary, String link) {
this.title = title;
this.summary = summary;
this.link = link;
}
}
// Parses the contents of an entry. If it encounters a title, summary, or link tag, hands them off
// to their respective "read" methods for processing. Otherwise, skips the tag.
private Entry readEntry(XmlPullParser parser) throws XmlPullParserException, IOException {
parser.require(XmlPullParser.START_TAG, ns, "entry");
String title = null;
String summary = null;
String link = null;
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
String name = parser.getName();
if (name.equals("title")) {
title = readTitle(parser);
} else if (name.equals("summary")) {
summary = readSummary(parser);
} else if (name.equals("link")) {
link = readLink(parser);
} else {
skip(parser);
}
}
return new Entry(title, summary, link);
}
// Processes title tags in the feed.
private String readTitle(XmlPullParser parser) throws IOException, XmlPullParserException {
parser.require(XmlPullParser.START_TAG, ns, "title");
String title = readText(parser);
parser.require(XmlPullParser.END_TAG, ns, "title");
return title;
}
// Processes link tags in the feed.
private String readLink(XmlPullParser parser) throws IOException, XmlPullParserException {
String link = "";
parser.require(XmlPullParser.START_TAG, ns, "link");
String tag = parser.getName();
String relType = parser.getAttributeValue(null, "rel");
if (tag.equals("link")) {
if (relType.equals("alternate")){
link = parser.getAttributeValue(null, "href");
parser.nextTag();
}
}
parser.require(XmlPullParser.END_TAG, ns, "link");
return link;
}
// Processes summary tags in the feed.
private String readSummary(XmlPullParser parser) throws IOException, XmlPullParserException {
parser.require(XmlPullParser.START_TAG, ns, "summary");
String summary = readText(parser);
parser.require(XmlPullParser.END_TAG, ns, "summary");
return summary;
}
// For the tags title and summary, extracts their text values.
private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
String result = "";
if (parser.next() == XmlPullParser.TEXT) {
result = parser.getText();
parser.nextTag();
}
return result;
}
...
}
以上描述的解析XML步骤中的其中一步是跳过不感兴趣的标签。以下是解析器的skip()方法:
private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {
if (parser.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
以下是此方法工作机制的过程:
- 如果当前事件不是START_TAG将抛出异常。
- 假设事件为START_TAG且所有的事件都爬出来了,其中包括END_TAT。
- 确保在正确的END_TAG处停下来且不是在遇到START_TAG后所遇到的第一个标签,并保持跟踪嵌套的深度。
因此,如果当前的元素为嵌套元素,depth的值不会为0指导解析器解析完了在START_TAG和END_TAG之间的所有事件。例如,考虑解析器如何跳过元素,此元素嵌套了和两个元素:
- 首次通过while循环时,在元素之后所遇到的下一个标签为START_TAG的标签,depth的值被增加到2。
- 第二次运行while循环时,解析器遇到的下一个标签为END_TAG的。depth的值被减到1.
- 第三次运行while循环时,解析器遇到的下一个标签为START_TAG的 ,depth的值被加到2.
- 第四次运行while循环时,解析器遇到的下一个标签为END_TAG的,depth的值被减到1.
- 第五次也是最后一次运行while()循环时,解析器遇到的下一个标签是EDN_TAG的。depth的值被减至0,表示元素已被成功跳过。
样例应用程序在AsyncTask中提取和解析XML进料,即不在主用户界面中处理这个过程。当处理完成时,应用程序更新主活动(NetworkActivity)用户界面。
摘录一段代码如下,loadPage()方法做以下工作:
- 为XML feed用URL初始化字符串变量。
- 如果用户设置和网络连接允许,调用new DownloadXmlTask().execute(url)。此语句初始化一个DownloadXmlTask对象(AsyncTask子类)并运行其execute()方法,此方法下载并解析feed并返回需要在用户界面中展示的字符串。
public class NetworkActivity extends Activity {
public static final String WIFI = "Wi-Fi";
public static final String ANY = "Any";
private static final String URL = "http://stackoverflow.com/feeds/tag?tagnames=android&sort=newest";
// Whether there is a Wi-Fi connection.
private static boolean wifiConnected = false;
// Whether there is a mobile connection.
private static boolean mobileConnected = false;
// Whether the display should be refreshed.
public static boolean refreshDisplay = true;
public static String sPref = null;
...
// Uses AsyncTask to download the XML feed from stackoverflow.com.
public void loadPage() {
if((sPref.equals(ANY)) && (wifiConnected || mobileConnected)) {
new DownloadXmlTask().execute(URL);
}
else if ((sPref.equals(WIFI)) && (wifiConnected)) {
new DownloadXmlTask().execute(URL);
} else {
// show error
}
}
AsyncTask的子类DownloadXmlTask内容如下,它实现了AsyncTask以下方法:
- doInBackground()执行loadXmlFromNetwork()方法。将feed URL作为参数。loadXmlFromNetwork()方法提取并处理feed。当此方法运行结束时,它传回一个字符串结果。
- 携带返回字符串并将其展示在用户界面的onPostExecute()。
// Implementation of AsyncTask used to download XML feed from stackoverflow.com.
private class DownloadXmlTask extends AsyncTask<String, Void, String> {
@Override
protected String doInBackground(String... urls) {
try {
return loadXmlFromNetwork(urls[0]);
} catch (IOException e) {
return getResources().getString(R.string.connection_error);
} catch (XmlPullParserException e) {
return getResources().getString(R.string.xml_error);
}
}
@Override
protected void onPostExecute(String result) {
setContentView(R.layout.main);
// Displays the HTML string in the UI via a WebView
WebView myWebView = (WebView) findViewById(R.id.webview);
myWebView.loadData(result, "text/html", null);
}
}
以下是在DownloadXmlTask中被调用的loadXmlFromNetwork()方法。此方法做以下事情:
[1]. 初始化StackOverflowXmlParser。同时也为入口(entries)对象、title、url以及summary创建变量。
[2]. 调用downloadUrl(),此方法提取feed并将它作为InputStream返回。
[3]. 用StackOverflowXmlParser 解析InputStream。StackOverflowXmlParser 填充从feed的数据填充入口列表。
[4]. 结合HML标记的feed数据, 处理入口列表。
[5]. 通过AsyncTask的onPostExecute()方法返回HTML字符串并将其显示在主活动用户界面中。
// Uploads XML from stackoverflow.com, parses it, and combines it with
// HTML markup. Returns HTML string.
private String loadXmlFromNetwork(String urlString) throws XmlPullParserException, IOException {
InputStream stream = null;
// Instantiate the parser
StackOverflowXmlParser stackOverflowXmlParser = new StackOverflowXmlParser();
List<Entry> entries = null;
String title = null;
String url = null;
String summary = null;
Calendar rightNow = Calendar.getInstance();
DateFormat formatter = new SimpleDateFormat("MMM dd h:mmaa");
// Checks whether the user set the preference to include summary text
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
boolean pref = sharedPrefs.getBoolean("summaryPref", false);
StringBuilder htmlString = new StringBuilder();
htmlString.append("<h3>" + getResources().getString(R.string.page_title) + "</h3>");
htmlString.append("<em>" + getResources().getString(R.string.updated) + " " +
formatter.format(rightNow.getTime()) + "</em>");
try {
stream = downloadUrl(urlString);
entries = stackOverflowXmlParser.parse(stream);
// Makes sure that the InputStream is closed after the app is
// finished using it.
} finally {
if (stream != null) {
stream.close();
}
}
// StackOverflowXmlParser returns a List (called "entries") of Entry objects.
// Each Entry object represents a single post in the XML feed.
// This section processes the entries list to combine each entry with HTML markup.
// Each entry is displayed in the UI as a link that optionally includes
// a text summary.
for (Entry entry : entries) {
htmlString.append("<p><a href='");
htmlString.append(entry.link);
htmlString.append("'>" + entry.title + "</a></p>");
// If the user set the preference to include summary text,
// adds it to the display.
if (pref) {
htmlString.append(entry.summary);
}
}
return htmlString.toString();
}
// Given a string representation of a URL, sets up a connection and gets
// an input stream.
private InputStream downloadUrl(String urlString) throws IOException {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(10000 /* milliseconds */);
conn.setConnectTimeout(15000 /* milliseconds */);
conn.setRequestMethod("GET");
conn.setDoInput(true);
// Starts the query
conn.connect();
return conn.getInputStream();
}
在执行下载或其它网络交易时如何最小化对电池的消耗。
在此节中,您将学习在“下载”、“网络连接”尤其是“跟无线电相关的操作”过程中如何最小化对电池的影响。
此节演示用诸如缓存、轮询以及预取计数来调度和执行下载操作时的最佳步骤。您还将会学习到在无线电下如何使用电池的简介,来影响对何时、什么以及怎么传输数据以最小化影响电池的寿命。
此节介绍无线电状态机,解释应用程序的连接模型是如何跟其互动的,以及在传输数据时如何结合最小化耗电来如何进行数据连接、预取。
使用无线电来传输数据是应用程序耗电最大的隐藏祸患。欲最小化网络活动对电池耗尽的影响,理解连接模型如何影响无线电硬件的异常关键。
一个完全活跃的无线电台会消耗很多的电,欲在不使用电的时候保留电量需要在不同状态下转变,当无线电上电时要最小化电量到来的延迟。
典型的3G网络无线电的状态机制由以下3种能量状态组成:
[1]. 全功率:懂连接活跃时使用全功率,允许设备传输数据以最高速的传输速率。
[2]. 低功率:一种中间状态,当电量用到50%左右后。
[3]. 备用:出于无活跃或需要的网络连接期间的最低能量状态。
低和闲置状态会耗更少的电池,同时还会引出网络请求的延迟。从低状态返回到全功率状态约耗1.5s左右,从闲置到全功率状态需要耗2s。
欲最小化延迟,状态机制使用延迟来推迟到低能量状态的转变。图1使用AT&T时间来支持典型的3G无线电。
图1.典型的3G无线电台状态机
在每个设备上的无线电状态机,尤其是跟延时转变和启动延时关联的,将基于无线电台技术雇主(2G,3G,LTE等)和其定义和其网络配置变化。
2015.11.22
此节描述的是基于由AT&T提供数据的代表典型3G无线电台的内容。然而,其中体现的通用原则和最佳的实践步骤适合所有的无线电台实现。
此方法尤其对典型的网页浏览有效,因为当用户浏览网页时它会阻止不受欢迎的延迟。相对的低尾时间也确保一旦浏览块结束时,无线能够移至低能量状态。
不幸的是,此方法可能会导致在像在Android操作系统的现代智能手机上的应用程序不那么高效,不管引用程序是运行在前台(无延迟比较重要)还是后台(应该针对电池优先做相应的处理)。
每创建新的网络连接时,无线电将转变到全攻率状态。在以上描述的3G无线电状态机的所有情况中,在数据转移期间都会是全功率状态 - 加上额外的5秒尾时间 - 紧跟12秒的低能量状态。目前对于一个典型的3G设备来说,每次数据传输都将引起无线电高达20s的耗电时间。
实际上,这意味着应用程序每18s有1s未未绑定数据传输将保持无线电台的持续活跃,仅当它要变为闲置状态时能够将其返回到高功率。因此,每分钟有18s在以高功率消耗电池,有42s在以低功率消耗电池。
通过对比,相同的应用程序每分钟有3s来绑定数据时,无线电台只有8s在高功率状态,有12s保持在第功率状态。
第二个例子允许无线电在每分钟内闲置40s,这样就大量缩减了电池耗量。
图2.无线电台绑定数据与未绑定数据功率使用对比
预取数据是减少独立数据传输时间数量的有效方法。预取允许在单突发(burst)规定的时间内、多连接以及全负载情况下载您所需要的数据。
通过提前加载传输数据,减少了需要下载数据的活跃无线电的数量。这样不仅能保存电池寿命,还能够提升延迟、降低需要的带宽以及减少下载时间。
预取同样在执行某项操作或视图数据前通过最小化由在应用程序中等待下载引起的延迟提升了用户体验。
然而,若预取使用过多则可能会引起增加“电池耗量”、“带宽使用”、下载量 - “下载了不使用的数据”的危险。当应用程序在等待预取完成时确保预取操作不会延迟应用程序启动也是很重要的。实际上,预取适合于渐进处理数据或初始化预先的连续的传输数据以需要应用程序提前为其准备好的情况。
预取使用过多与否取决于预下载数据的大小以及可能会被使用的量。作为一个粗略的指南,基于以上所描述的状态机,假设在用户当前期间(session)有50%的数据会被使用,以下载不需要使用的数据为代价,可以预取约6s(约1-2Mb)数据。
一般来讲,预取数据是一个不错的步骤,这样就只需要每2到5分钟时间才去初始化另外一个下载,每次预取1到5M字节。
遵循此项原则,下载大数据时 - 如视频文件 - 应该以块为单位间接(每2到5分钟)的下载,在视频数据可能在接下来的几分钟会被用到时使用高效的预取方法。
进一步的下载应该被绑定,见下一节“批量传输和连接”中的描述,此基于连接类型和速度,在Modify your Download Patterns Based on the Connectivity Type一节中有讨论。
让咱来看看一些实际的例子:
音乐播放器
若预取了整个专辑,用户在听完第一首歌曲后就停止了继续听歌,这就浪费了带宽和电池耗量。
一种更好的方式是在当前有一首歌正在播放时再缓存一首歌。对于流水线音乐,不用维持持续的流来保持音频一直活跃,考虑使用HTTP来直播流水线以在单突发中传输流,模拟以上描述的预取方法。
新闻阅读器
许多的应用程序都尝试在某个类别被选中通过下载标题的方式来减少带宽,只当用户要读正文时方下载正文,且通过提供滑动条的缩略图提供给用户。
使用这种方式,在用户滚动标题、改变类别以及阅读正文等这些主要的用户操作时,无线电都应该一直保持活跃。不仅如此,当用户在改变类别或正阅读时常见的低能量装填转变将会导致很大的延迟。
一种更好的方式是在应用程序启动阶段从新闻开始处的标题和缩略图处预取适当数量的数据 - 确定在启动期间拥有低延迟 - 以及持续的显示后续标题和缩略图,当然也至少要显示从每篇文章的正文内容开始部分的内容。
另外一种可选的方法是预取每个标题、缩略图、文章内容甚至文章中的所有图片 - 通常在预订时间表背景下。此种方法有消耗更大带宽、电池寿命以及下载不会被使用内容的危险,所以要使用此方法需要谨慎。
一种解决方案是仅当连接到Wi-Fi且设备正在充电时执行全全下载。关于此内容的更多研究见 Modify your Download Patterns Based on the Connectivity Type。
每次初始化连接 - 不管相关的数据传输的大小 - 在使用典型的3G无线电台网络时都可能会引起无线电消耗近20s的功率。
应用程序会ping20s的时间来告知应用程序正在运行且对用户可见,此将不定期的让无线电耗电,尽管无实际的数据传输但会消耗很大的电量。
绑定数据传输以及创建待定数据阐述队列很重要。正确的完成了这个工作,可以通过高效的位移传输来确保所有得操作都持续发生 - 保证无线电尽可能的短。
此方法的潜在思想是尽可能在一个传输期间中尽可能的多传数据以限制所需要的数据传输周期。
这就意味着需要通过排列可忍受延迟的传输来批量传输数据,并抢占计划的更新和预取,这样对于时间敏感的传输都会得到持续执行。类似的,计划的更新和有规律的预取需要初始化待传的队列。
欲见一个时机的例子,见 Prefetch Data。
使用以上所描述的预取技术路线实现一个新闻应用程序。新闻阅读器通过收集分析信息来理解用户的阅读模式并对最流行的故事排名。欲保持新闻的新鲜性,应每小时都检查更新。欲节约带宽,通过只预取缩略图并在用户选择后下载缩略图对应的所有照片。
在此例中,在应用程序中所搜集的所有信息需要绑定在一起并以队列的形式提供下载,而不是当应用程序收集后就上传。当其中一个全尺寸的图片正被下载或当每小时有内容更新时,其所对应的绑定数据也应该开始传输。
任何时间敏感或基于需求的传输 - 如下载全尺寸图片 - 就应该抢占定期更新。基于需求的传输所计划的更新应该和下一次计划的更新在同时得到执行。此种方式通过基于必要的时间敏感图片下载的回贪(piggy-backing)方法减小了执行定期更新的代价。
重新使用一个已存在的网络连接通常要比重新初始化一个网络连接要高效得多。
一般不会创建多个同时连接的网络来下载数据,也不会连接多个连续的GET请求,而常可能是绑定这些请求到单个GET。
例如,为每个会返回到请求/响应的新闻文章创建单请求会比为多个新闻类别创建多个查询高效得多。无线电台在服务器和客户端超时时还需要保持活跃来传递跟服务器和客户端相关的终止服务包没所以在连接不需要使用时可以关闭连接,而不是等待超时。
也就是说,太早地关闭连接能够阻止其被重用,这样会使得需要重复的步骤来建立新的连接。一种妥协的方式是不立即关闭连接,超时时再关闭。
2015.11.23
包含在安卓DDMS(dalvik Debug Monitor Server)中的详细网络使用量(Detailed Network Usage)能够跟踪应用程序访问网络。使用此工具,可以监控应用程序如何、何时传输数据并能够适当地优化应用程序中的代码。
图三显示了小量数据的大约15s的传输模式,通过预取每个请求或绑定上传数据能够动态的提升效率。
图3.用DDMS跟踪网络使用量
通过监控数据传输的频率和在每次连接期间所传输的数据量,可以确定应用程序能提升电池效率的部分。通常是寻找可能的延迟或需要被抢先发送的部分。
一种确认引起传输传输尖峰(spikes)更好的方法,交通条例API(Traffic Stats API)允许标记传输数据,在某线程中使用TrafficStats.setThreadStatsTag() 方法可以标记传输数据,然后通过使用tagSocket()和untagSocket()方法手动的标记(解标记)个别的套接字。举例如下:
TrafficStats.setThreadStatsTag(0xF00D);
TrafficStats.tagSocket(outputSocket);
// Transfer data using socket
TrafficStats.untagSocket(outputSocket);
Apache的HttpClient 和URLConnection 库根据当前的getThreadStatsTag()值自动标记套接字。当循环时这些库通过保持生命的池来标记和解标记套接字。
TrafficStats.setThreadStatsTag(0xF00D);
try {
// Make network request using HttpClient.execute()
} finally {
TrafficStats.clearThreadStatsTag();
}
安卓4.0支持套接字标记,但真实的统计是只有在安卓4.0.3或更高版本才能够实现套接字标记。
2015.11.24
此节介绍在无线电状态机下多大的刷新频率范围能够减轻对后台更新的影响。
定期更新的最优频率基于不同设备的“网络连接”、“用户行为”以及“用户优先项”等不同状态而变化。
“优化电池寿命”讨论如何根据主机状态来修改刷新频率而构建电池高效的应用程序。包括当断开连接时关闭后台服务的更新并在电量较低时减少更新的比率。
此节检查出刷新频率范围以最大减小后台更新对于无线状态机的影响。
对于一个典型的3G连接,应用程序应每隔20s轮询服务以检查是否有更新请求、是否激活了无线电台、是否在不必要的耗电。
支持安卓的谷歌云消息(GCM)是用来从服务端传输数据到特定的应用程序实例的轻量型机制。通过使用GCM,服务端可以通知运行在某设备上的应用程序有新的数据可用。
和轮询方式相比,应用程序必须定期的ping服务端以查询是否有新数据,此事件驱动只在当知道有数据下载时才让应用程序创建连接。
使用谷歌云消息的结果是减少了不必要的连接,并缩小了应用程序中数据更新的延迟。
GCM通过使用持久的TCP/IP连接实现。虽然可以自定义实现(推送)服务,但最好还是使用GCM。GCM能够最小化持久连接的次数、允许平台优化带宽并能最小化对电池寿命的影响。
当需要轮询时,在不减损用户体验的情况下应该尽量降低数据的刷新频率。
一个简单的方法是提供一个优先的界面来让用户精确的设置所需要的刷新频率,以允许用户自己来平衡数据更新率和电池的寿命。
调度更新时,使用模糊的重复警报来让系统在每次警报出发时“相移”到精确的时刻。
int alarmType = AlarmManager.ELAPSED_REALTIME;
long interval = AlarmManager.INTERVAL_HOUR;
long start = System.currentTimeMillis() + interval;
alarmManager.setInexactRepeating(alarmType, start, interval, pi);
如果多个警报在相近的时间里被调度触发,相移会引发它们几乎同时触发,允许在单个活跃的无线电状态改变的顶部有多个更新。
只要有可能,将警报(alarm)类型设置为ELAPSED_REALTIME 或RTC ,不要设置为_WAKEUP 。通过等待直到在警报出发前没有备用备用的模式可以大大的减少对电池的影响。
基于应用程序最近被使用的情况来减少它们的频率可以大大减小警报们的影响。
若应用程序自从上次更新后都没有被使用过,一种方式是实现指数反取舍的模式来实现更新频率(以及/或执行预取的程度)。在用户重设频率前默认使用最小的更新频率很有用,如下例:
SharedPreferences sp =
context.getSharedPreferences(PREFS, Context.MODE_WORLD_READABLE);
boolean appUsed = sp.getBoolean(PREFS_APPUSED, false);
long updateInterval = sp.getLong(PREFS_INTERVAL, DEFAULT_REFRESH_INTERVAL);
if (!appUsed)
if ((updateInterval *= 2) > MAX_REFRESH_INTERVAL)
updateInterval = MAX_REFRESH_INTERVAL;
Editor spEdit = sp.edit();
spEdit.putBoolean(PREFS_APPUSED, false);
spEdit.putLong(PREFS_INTERVAL, updateInterval);
spEdit.apply();
rescheduleUpdates(updateInterval);
executeUpdateOrPrefetch();
可以使用类似的指数反取舍模式来减少连接失败和下载错误的影响。
无论能否连接到服务器并下载数据与否,初始化网络的代价都是相同的。对于实时传输,数据成功传输的时刻很重要,指数反取舍算数能够用来减低重试的频率,这样就可以减少对电池的影响,如下例:
private void retryIn(long interval) {
boolean success = attemptTransfer();
if (!success) {
retryIn(interval*2 < MAX_RETRY_INTERVAL ?
interval*2 : MAX_RETRY_INTERVAL);
}
}
或者,对于那些可忍受失败的传输(如定期更新),可以简单的忽略失败的连接和尝试重传。
2015.11.25
减少下载最基础的方式是只下载所需要的资源。此节介绍一些避免下载多余下载的基本步骤。
减少下载最基本的方式就是只下载所需要的数据。对于数据来说,这就意味着需要实现REST APIs来允许指定查询标准来限制返回数据(通过使用诸如最近一次更新的时间一类的参数)。
同理,下载图片时,应该在服务器端缩小图片的尺寸,而不是从服务器端下载全尺寸的图片到客户端后再缩小图片尺寸。
另一种重要的技术是避免重复下载相同的数据。可以通过缓存来实现这个目的。尽可能地缓存静态数据,包括诸如全尺寸图片这种按需求下载的资源。基于需求的资源应该分开存储以使之能够被按照用户具体需求而刷新缓存来管理其大小。
确保缓存机制不会导致应用程序展示陈旧的数据,即确定从缓存中提取的数据是最近一次所更新的数据,当缓存中的数据过期时,立即从HTTP响应头内更新数据。这将允许您决定何时刷新缓存中的内容。
long currentTime = System.currentTimeMillis());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
long expires = conn.getHeaderFieldDate("Expires", currentTime);
long lastModified = conn.getHeaderFieldDate("Last-Modified", currentTime);
setDataExpirationDate(expires);
if (lastModified < lastUpdateTime) {
// Skip update
} else {
// Parse update
}
通过这种方法,在确保应用程序不展示陈旧信息的情况下也可以有效的缓存动态内容。
可在非管理的外部缓存目录下缓存非敏感的数据:
Context.getExternalCacheDir();
或者,使用管理的/安全的应用程序缓存。注意当系统内存比较紧张时内部缓存内容可能会被冲刷。
Context.getCacheDir();
无论文件被缓存在哪里,在应用程序被卸载时它们都会被移除。
安卓4.0在HttpURLConnection中增加了响应缓存。可以在支持的安卓4.0的设备上像如下代码那样开启HTTP的响应缓存:
private void enableHttpResponseCache() {
try {
long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
File httpCacheDir = new File(getCacheDir(), "http");
Class.forName("android.net.http.HttpResponseCache")
.getMethod("install", File.class, long.class)
.invoke(null, httpCacheDir, httpCacheSize);
} catch (Exception httpResponseCacheNotAvailable) {
Log.d(TAG, "HTTP response cache is unavailable.");
}
}
此段代码会开启安卓4.0+设备上的响应缓存,且此段代码也不会影响早期版本的安卓设备。
安装缓存后,全缓存的HTTP请求可直接服务于本地存储器,如此就消除了打开网络连接的步骤。另外,缓存响应能够有效的从服务器端更新,消除由下载带来的带宽问题。
高速缓存的响应存储在响应缓存中以供长远的请求。
2015.11.26
并不是所有连接类型对电池的寿命影响都是相同的。Wi-Fi无线电不仅比无线电耗电池更少,而且应用在不同无线电中的无线电计数拥有对电池寿命不同的影响。
并不是所有被创建的连接类型对电池寿命的影响都是相同的。Wi-Fi无线电不禁比无线电使用更少的电量,而且当无线电用于不同的无线电台技术时对电池有不同的影响。
在大多数情况下,Wi-Fi能在消耗低电量的情况下提供较大的带宽。因此,只要有可能就应该在连接Wi-Fi时执行数据的传输操作。
可以使用广播接收器来监听连接的改变,当连接切换为Wi-Fi连接时就执行较大的下载操作、抢占定期更新并极可能哪怕暂时增加定期更新的频率,如 Optimizing Battery Life中 Determining and Monitoring the Connectivity Status一节中描述的那样。
当连接到无线设备时,高的带宽常会以高电量消耗为代价。这就意味着LTE比3G消耗更多的能源(3G当然比2G消耗更多能源)。
这就意味着当基于无线技术的底层的无线状态变化时,一般来讲,尾时状态改变对电池的影响会超过高带宽的优越。
同时,更高的带宽意味着可以使用更多的预取操作、下载更多数据。也行并不那么直观,因为尾时电池耗量相对更高,在传输数据期间减少更新频率是一种重要的保持无线电活跃更长时间的策略。
举例来说,假设LTE无线的带宽和耗电量都是3G的两倍,那么LTE每个周期所下载的数据都应该是3G的4倍 —- 要下载10mb数据。当下载如此多的数据时,需要考虑在本地存储中的预取操作的影响以及定期的刷新缓存中的内容。
可以通过连接管理器来判断当前活跃的无线信号,并根据当前活跃的无线信号修改预取代码:
ConnectivityManager cm =
(ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
TelephonyManager tm =
(TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
int PrefetchCacheSize = DEFAULT_PREFETCH_CACHE;
switch (activeNetwork.getType()) {
case (ConnectivityManager.TYPE_WIFI):
PrefetchCacheSize = MAX_PREFETCH_CACHE; break;
case (ConnectivityManager.TYPE_MOBILE): {
switch (tm.getNetworkType()) {
case (TelephonyManager.NETWORK_TYPE_LTE |
TelephonyManager.NETWORK_TYPE_HSPAP):
PrefetchCacheSize *= 4;
break;
case (TelephonyManager.NETWORK_TYPE_EDGE |
TelephonyManager.NETWORK_TYPE_GPRS):
PrefetchCacheSize /= 2;
break;
default: break;
}
break;
}
default: break;
}
如何同步和备份应用程序及用户数据到远端的Web云服务中,如何从远端的Web云服务取回数据到多个设备中。重使用网络连接同时也能更智能的响应拥塞和相关的网络数据问题。
用户常会投入较多的时间和努力在应用程序中创建数据和设置他们喜欢的设置。对于更换换掉的设备或买新手机的用户来说,保存他们的数据和设置将是一个好的用户体验。
此节包含备份数据到云的技术,用户可以将它们的数据存储,当它们从丢失数据(如恢复出厂值)或安装应用程序到新设备后再恢复使用应用程序时,原来的数据都可以随之恢复过来。
注意,备份数据到云的技术在Android 6.0(API level 23)发布版本中有改变。对于想同时运行在Android 6.0和Android 5.1(API level 22)以及以下版本的系统中的应用程序来说,必须要实现此节所介绍的两种版本对应的技术。
此部分内容适用于Android 6.0(API level 23)及更高版本的系统。学习如何编写无漏洞数据备份以及无多余操作的应用程序。
2015.11.28
用户会经常花时间和精力按照他们喜欢的方式配置应用程序。当在一个新的设备上重新安装应用程序时这些经过仔细配置的设置和数据都将会消失。对于target SDK version为Android 6.0(API level 23)以及更高版本时,若设备运行Android 6.0及更高版本时,系统默认将应用程序最近使用过的数据或配置自动备份到云端,这个过程不需要开发者编写额外的代码。
注:为保护用户隐私,设备用户必须要拥有是否备份数据到谷歌云端的选择权。当用户在设备上根据设置导向或第一次配置谷歌账户时谷歌服务的选择会出现在对话框中。
当用户在新设备上安装应用程序或者在同一个设备上重新安装此应用程序时(例如,在恢复出厂设置之后)。系统会自动从云端获取数据。此节提供如何配置应用程序自动备份的特点、解释自动备份的默认行为以及如何怎么实现不想让系统自动备份某些数据的信息。
自动备份的特性通过上传数据到用户谷歌驱动账户并对数据加密的方式保存了用户在设备中的应用程序中创建的数据。对用户和开发者来说,存储数据无需任何费用,且保存的数据不算用户个人谷歌驱动的配额。每个应用程序能够存储25MB数据。一旦备份数据到达25MB,应用程序将不再备份数据到云端。如果系统再执行在云端存储数据的操作,云端将保存应用程序最近发送的数据。
当以下条件满足时方能发生自动备份:
- 设备空闲。
- 设备电量充足。
- 设备连接了Wi-Fi。
- 自上次备份已过24小时。
当设备运行Android 6.0(API level 23)或更高版本系统时,系统默认的行为是备份几乎所有的用户在应用程序中创建的数据。不备份的例外是系统会自动排出数据文件。此部分内容解释如何在应用程序的清单文件中进一步限制和配置系统备份的数据。
基于应用程序需要备份什么数据及如何保存这些数据,开发者可能需要为需要备份或不需要备份的文件或目录指定规则。应用程序的自动备份功能能够让开发者在应用程序的清单文件中指定备份规则,即在在XML文件中书写这些规则。如下例:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.my.appexample">
<uses-sdk android:minSdkVersion="23"/>
<uses-sdk android:targetSdkVersion="23"/>
<application ...
android:fullBackupContent="@xml/mybackupscheme">
</app>
...
</manifest>
在此例中,android:fulBackupContent属性指定了一个名为mybackupscheme.xml的XML文件,此文件保存在应用程序工程的res/xml目录下。此配置文件中包含了控制哪些文件会被备份的规则。下例代码展示不备份指定文件device_info.db的内容:
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<exclude domain="database" path="device_info.db"/>
</full-backup-content>
大多数应用程序不需要(实际上也不应该)备份应用程序所有的数据。例如,系统不应该备份临时文件和缓存数据。因此,自动备份服务会默认排除一些数据文件的备份:
- getCacheDir()和getCodeCacheDir()方法能够访问的目录下的文件。
- 在外部存储器中的文件(除getExternalFilesDir()方法能够访问的文件)。
- getNoBackupFilesDir()方法能够访问的目录下的文件。
备份服务配置允许应用程序指定数据文件备份与否。在XML文件中指定数据备份配置的语法如下:
<full-backup-content>
<include domain=["file" | "database" | "sharedpref" | "external" | "root"]
path="string" />
<exclude domain=["file" | "database" | "sharedpref" | "external" | "root"]
path="string" />
</full-backup-content>
以下元素和属性允许应用程序指定备份的文件以及不用备份的文件:
- :指定一套需要备份的数据,代替系统备份所有数据的默认方式。如果应用程序指定了元素,系统只会备份在此元素中指定的资源。可以用多个元素来指定多套需要备份的数据。
- :当系统会备份所有数据时,可以通过此元素来指定不被备份的数据。如果有些数据文件既包含在中也包含在中,具有优先处理权。
- domain:指定(不)想要备份的资源类型。其下包含以下属性:
- root:指定在应用程序中资源的根目录。
- 指定由getFilesDir()方法返回的目录下的文件。
- database:指定由getDatabasePath()方法返回的数据库,或和应用程序中的SQLiteOpenHelper类交互的数据库。
- sharedpref:指定由getSharedPreferences()方法返回的SharedPreferences对象。
- external:指定在外部存储中的资源,并对应由getExternalFilesDir()方法返回目录中的文件。
通过设置清单文件中app元素下的android:allowBackup属性为false 可以选择组织应用程序对任何数据的自动备份。如下例:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.my.appexample">
<uses-sdk android:minSdkVersion="23"/>
<uses-sdk android:targetSdkVersion="23"/>
<application ...
android:allowBackup="false">
</application>
...
</manifest>
欲想在低于Android 6.0(API level 23)的系统版本上也支持自动备份数据到云端功能时,可能是以下两种情况。在应用程序兼容低于Android 6.0系统的前提下更新应用程序,当应用程序运行在Android 6.0及更高版本系统上时应用程序拥有自动备份数据的功能;或者是开发一个新的应用程序,既能够运行在低于Android 6.0的系统版本上又能在Android 6.0及以上的系统上具有自动备份数据的功能。
更早版本的安卓系统支持基于键/值对的备份机制,这需要在应用程序中定义BackupAgent的子类并在应用程序清单文件中设置 android:backupAgent属性。如果您在应用程序中使用了这种遗留的方法,可以通过在清单文件中的元素中增加android:fullBackupOnly=”true”属性将备份的方式转换为全数据备份的方式。在Android 5.1(API level 22)以及更低的系统版本上运行此应用程序时,引用程序将忽略清单文件中的此属性,并会继续执行之前的数据备份方式。
即使没有使用键/值对的备份方式,仍可以使用以上描述的方式在onCreate()或onFullBackup()方法中自定义备份方式。可以在onRestreFinished()方法中接收通知。如果想利用系统默认的实现,则调用super.onFullBackup()方法。
若创建一个以Android 6.0位目标的新的应用程序,但也想其运行Android 5.1及更低的系统上,那么久必须实现备份API( implement the Backup API)。
一旦创建了备份配置,那么就应该测试以确保应用程序的确能够保存数据并能够合适的存储它们。
欲帮助判断备份是如何解析XML文件的,在执行测试备份之前开启日志:
$ adb shell setprop log.tag.BackupXmlParserLogging VERBOSE
欲手动运行备份,首先需要通过以下命令来初始化备份管理器:
$ adb shell bmgr run
然后,通过以下命令手动备份应用程序。使用参数来制定应用程序包名:
$ adb shell bmgr fullbackup <PACKAGE>
欲在系统已经备份应用程序数据之后手动初始化存储,执行以下命令,用参数制定应用程序包:
$ adb shell bmgr restore <PACKAGE>
警告:在执行存储操作之前此动作会停止应用程序并清除其数据。
可以通过卸载并重新安装应用程序的方式测试应用程序的自动安装功能。当应用程序重新安装后,若应用程序的备份配置正确,应用程序之前的数据会从云端更新到应用程序中。
如果备份失败,可以通过在设置 >> 备份中设置开启或关闭备份来清除备份数据并关联元数据,将设备恢复出厂值或执行以下命令:
$ adb shell bmgr wipe <TRANSPORT> <PACKAGE>
必须优先考虑将com.google.android.gms设置为的值。欲得到的列表,执行以下命令:
$ adb shell bmgr list transports
对于使用谷歌云信息(GCM)来推送通知的应用程序,备份在谷歌云消息中注册返回的注册符号会在存储此备份的应用程序中引起不定的行为。因为当用户在一个新的设备上安装应用程序时,应用程序必须为新的注符号查询GCM API。如果旧的注册存在,由于系统已经将其备份并存储了它,应用程序将不会搜寻新的符号。欲放置此问题发生,从备份文件清单中一出对注册符号的备份。
此部分内容适用于Android 5.1(API level 22)及更低版本系统。学习如何整合备份API到Android的应用程序中,让应用程序的用户数据(诸如个人偏好设置、笔记以及高的分数)能通过设备所连接到的谷歌账户无缝隙的得到备份。
2015.11.29
当用户重新设置或新购买一个新的设备时,他们可能希望能够从Google Play存储那里恢复他们的应用程序的最初的设置,也希望跟应用程序关联数据也被存储着。对于运行在Android 6.0(API level 23)之前的系统上的应用程序,应用程序的数据没有被默认存储,所有关于应用程序的数据和设置都将会全部丢失。
欲解决少量数据(少于兆字节)的备份,诸如用户个人偏好设置、笔记游戏的最高分或其它的统计,备份API提供了备份这些少量数据的方法。此节描述如何将备份API写入应用程序中,为应用程序在新的设备上安装时能够找回某些数据。
注:对于运行Android 6.0及更高系统的设备会自动备份应用程序近期使用的数据。
此节需要使用安卓备份服务(Android Backup Service),此服务需要注册。一旦完成注册,此服务会预先填充到清单文件中的XML标签中,示例代码如下:
<meta-data android:name="com.google.android.backup.api_key"
android:value="ABcDe1FGHij2KlmN3oPQRs4TUvW5xYZ" />
注意每个备份关键字都包含一个特定的包名。如果还有其它的应用程序,需要为此再单独注册。
使用Android备份服务还需要在应用程序的清单文件中添加两个额外的条件。第一,为备份代理声明一个对应的类名,然后将以上的一小段代码作为应用程序标签的子元素。假设备份代理名为theBackupAgent,下例展示清单文件中需要包含的元素:
<application android:label="MyApp"
android:backupAgent="TheBackupAgent">
...
<meta-data android:name="com.google.android.backup.api_key"
android:value="ABcDe1FGHij2KlmN3oPQRs4TUvW5xYZ" />
...
</application>
创建备份代理最简单的方式是扩展包装类BackupAgentHelper。创建此帮助类是一个非常简单的过程。在之前的步骤中将此类的名称声明在清单文件中(如TheBackupAgent),然后用此名扩展BackupAgentHelper类。然后重写onCreate()方法。
在重写onCreate()方法时,创建BackupHelper。这些帮助类是存储指定类型数据的特殊的类。Android框架目前包含两种这样类型的类:FileBackupHelper和SharedPreferencesBackupHelper。在创建此类并指定所需备份的数据之后,只需使用addHelper()方法将它们添加到BackupAgentHelper 中,还需将稍后会用来检索数据的key添加进去。在大多数情况下,实现这个过程大概需要10行代码。
以下是备份最高得分文件的例子:
import android.app.backup.BackupAgentHelper;
import android.app.backup.FileBackupHelper;
public class TheBackupAgent extends BackupAgentHelper {
// The name of the SharedPreferences file
static final String HIGH_SCORES_FILENAME = "scores";
// A key to uniquely identify the set of backup data
static final String FILES_BACKUP_KEY = "myfiles";
// Allocate a helper and add it to the backup agent
@Override
void onCreate() {
FileBackupHelper helper = new FileBackupHelper(this, HIGH_SCORES_FILENAME);
addHelper(FILES_BACKUP_KEY, helper);
}
}
欲达增加的灵活性,FileBackupHelper的构造函数能够携带几个文件的名字的参数。可以通过将最高数据作为某文件关联的参数传递给构造函数,如下例:
@Override
void onCreate() {
FileBackupHelper helper = new FileBackupHelper(this, HIGH_SCORES_FILENAME, PROGRESS_FILENAME);
addHelper(FILES_BACKUP_KEY, helper);
}
备份用户个人偏好设置也类似的简单。以相同的方式扩展类FileBackupHelper得到一个SharedPreferencesBackupHelper。在这种情况,不用增加文件名给构造函数,增加应用程序所用的偏好设置组即可。以下例子展示实现用备份代理帮助来备份个人偏好设置的代码:
import android.app.backup.BackupAgentHelper;
import android.app.backup.SharedPreferencesBackupHelper;
public class TheBackupAgent extends BackupAgentHelper {
// The names of the SharedPreferences groups that the application maintains. These
// are the same strings that are passed to getSharedPreferences(String, int).
static final String PREFS_DISPLAY = "displayprefs";
static final String PREFS_SCORES = "highscores";
// An arbitrary string used within the BackupAgentHelper implementation to
// identify the SharedPreferencesBackupHelper's data.
static final String MY_PREFS_BACKUP_KEY = "myprefs";
// Simply allocate a helper and install it
void onCreate() {
SharedPreferencesBackupHelper helper =
new SharedPreferencesBackupHelper(this, PREFS_DISPLAY, PREFS_SCORES);
addHelper(MY_PREFS_BACKUP_KEY, helper);
}
}
可以根据需要添加多个备份帮助类实例到应用程序中,但需要记住每个实例都需要一个备份数据的类型。FileBackupHelper操控所有需要备份的文件,SharedPreferencesBackupHelper操控所有需要备份的用户偏好设置数据。
欲请求备份,创建BackupManager的实例,并调用其内的dataChanged()方法。
import android.app.backup.BackupManager;
...
public void requestBackup() {
BackupManager bm = new BackupManager(this);
bm.dataChanged();
}
此方法通知备份管理此时有数据需要备份到云。备份管理将在稍后某时调用备份代理的onBackup()方法。在数据改变后可以在任何时候调用此方法,不用担忧引起会引起过度的网络活动。若在备份发生前请求了两次备份操作,备份只会发生一次。
一般不会手动请求存储,在应用程序安装到设备上时它会自动发生备份操作。然而,若手动触发存储必要,只需调用requestRestore()方法。
如何设计稳健的策略来解决应用程序存储数据到云端时的冲突问题。
2015.11.30
此节描述如何设计一个解决使用云保存服务的应用程序保存数据到云时的冲突问题的稳健方案。谷歌服务能为使用谷歌服务上应用程序的每个用户备份用户的应用程序数据。应用程序能够从Android、IOS设备或使用云保存APIs的网页应用程序上检索和更新用户数据。
保存和载入进度到云保存中是简单的:只需要序列化用户数据为字节数组并将这些序列数组存储到云中。然而,当用户有多台设备并有两台或多台设备都要保存数据到云时,保存就可能会发生冲突,此时就必须决定如何解决这个冲突问题。云保存数据的结构从很大程度上决定了解决这种冲突方式的稳健性,所以为能够正确处理好每一种情况必须仔细设计云存储数据的结构。
此节首先描述几个有缺点的方法并解释它们的缺点在哪里。然后呈现避免冲突的解决方法。讨论主要针对游戏,但其中包含的原则可以应用到任何其它将会保存数据到云的应用程序。
OnStateLoadedListener方法能够从谷歌服务中载入应用程序状态数据。 OnStateLoadedListener.onStateConflict回调方法提供了解决在用户设备上的本地状态与云中存储状态冲突的方法。
@Override
public void onStateConflict(int stateKey, String resolvedVersion,
byte[] localData, byte[] serverData) {
// resolve conflict, then call mAppStateClient.resolveConflict()
...
}
此时,应用程序必须选择当前要保持哪一个数据集,或合并数据提交一份新的数据集。这取决于开发者实现冲突解决的逻辑。
要意识到云保存服务在后台同步数据。因此,必须确保应用程序已被好接受在上下文之外的产生数据的回调函数。尤其地,如果谷歌Play服务在后台检测到冲突,回调函数被在应用程序下一次载入数据时被调用,这就造成只有用户下一次启动程序时才会从云上取数据。
因此,设计的云保存数据和冲突解决方法的代码必须是独立于上下文的:给定两个冲突保存状态,必须使用数据集类的数据来解决冲突,而不用任何外部的上下文。
以下有一些冲突解决的简单情形。对于大多数应用程序来说,采用以下策略中的一种就足够了:
- 旧不胜新(New is better than old)。在某些情况下,新数据总会替换旧数据。例如代替用户设置应用皮肤颜色的数据,最近的选择数据会覆盖以往的选择数据。在这种情况下,需要合适的存储一个时间戳到云上。然后解决冲突,将最近的数据和其时间戳保存起来(记住要使用可靠的时钟,还需注意时区的差异)。
- 保存比其它都佳的数据(One set of data is clearly better than the other)。在其它情形下,总是保存被视为最佳的数据。如用户竞车游戏中的最佳时间数据,对比用户每次玩此游戏所花的时间,只保存最佳(最小)时间。
- 合并(Merge by union)。通过计算两盒两个冲突的数据集可能可以解决冲突问题。例如,用户没有锁定的游戏的级数,保存游戏的级数就是简单的对两个数据集的联合。通过这种方式,用户不会丢失任何他们玩过的关数。CollectAllTheStars游戏使用的策略就是基于此合并策略的。
2015.12.01
当游戏允许玩家收集诸如金币或经验点这种可互换的商品或装备时,更复杂的情况就来了。假设有一个叫Coin Run的游戏,游戏中的主角可以无限的跑,跑的过程中可以收集金币以让游戏中的主角变得很富有。游戏主角每获得一个金币时就加到玩家的小金库中去。
以下3节内容描述3种解决在多台设备上同步冲突的问题:前两个策略看起来不错,但最终都不能成功解决所有场景的问题,最后一个解决方案能够解决多台设备间的冲突问题。
咋一想,在云端存储数据应该只是简单的存储金币的总数量即可。但如果所有的数据都可用,解决冲突的方法就会受到限制。在具有冲突的情况下最好是比较两个最大的数字。
考虑在表1中列举的场景。假设玩家开先有20个金币,然后在A设备上获得了10金币,在B设备上获得了15金币。设备B将状态保存到了云。当设备A也尝试保存时,就会检测到冲突。那么“存储总量”冲突解决方法通过往云里存储35(选取两数中最大的值)来解决此处的冲突。
表1. 只存储金币的总量(失败的策略)
事件 | 设备A上的数据 | 设备B上的数据 | 云上的数据 | 实际总量 |
初始条件 | 20 | 20 | 20 | 20 |
玩家在设备A上收集了10个金币 | 30 | 20 | 20 | 30 |
玩家在设备B上收集了15个金币 | 30 | 35 | 20 | 45 |
设备B存储状态到云上 | 30 | 35 | 35 | 45 |
设备A尝试保存状态到云。检测到冲突 | 30 | 35 | 35 | 45 |
设备A通过保留两个数中最大的数来解决冲突 | 35 | 35 | 35 | 45 |
此策略不能解决冲突问题 - 玩家的小金库金币数量从20增加到35,单用户实际手机了25个金币(A设备上收集了10个,B设备上收集了15个)。所以,此策略将用户收集的金币弄丢了10个。看来,只存储金币的总量到云端对于实现解决冲突的稳健方法还不够。
另一个不同的方法是再多存储一个数据量:根据上一次的情况金币增加的数量(delta)。保存数据的此方法能被元组(T,d)所描述,T表示金币的总量,d表示金币的增量。
通过元组这个结构,解决冲突的算法将变得更加稳健。但这个方法仍然不能给应用程序的用户所有的状态提供一个可用的画面。
以下是包含增量的冲突解决算法:
- 本地的数据:(T,d)
- 云上的数据:(T’, d’)
- 更新数据:(T’ + d, d)
如当从本地状态(T,d)和云状态(T’, d’)有冲突时,可以通过(T’ + d, d)来解决冲突。这表示将本地增加的数据增加到云数据中,并希望此方法能够对在任何设备上有增加金币时都能够有效的解决冲突问题。
此方法开起来可信,但在动态的移动设备环境下此方法仍旧会发生错误:
- 用户可能在设备没联网的状态下保存数据。当设备联网后设备的改变将要排队往云上跟新数据。
- 同步数据的方式是用最新的数据覆盖之前的数据。换句话说,第二次的数据会被写到云上(当设备重新连网后),而第一次数据增量将会被忽略。
表2演示了以上描述的场景。在表中所述系列步骤后,云的状态将是(130, +5)。这意味着更新状态将是(140, +10)。总量是不正确的,用户在设备A上收集了110个金币,在设备B上收集了120个金币。总量应该是250个金币。
表2. 总量+增量策略失败的情形
事件 | 设备A上的数据 | 设备B上的数据 | 云上的数据 | 实际总量 |
初始条件 | (20, x) | (20, x) | (20, x) | 20 |
玩家在设备A上收集了100个金币 | (120, +100) | (20, x) | (20, x) | 120 |
玩家在设备A上收集又收集了10个金币 | (130, +10) | (20, x) | (20, x) | 130 |
玩家在设备B上收集了115个金币 | (130, +10) | (125, +115) | (20, x) | 245 |
玩家在设备B上又收集了5个金币 | (130, +10) | (130, +5) | (20, x) | 250 |
设备B存储状态到云上 | (130, +10) | (130, +5) | (130, +5) | 250 |
设备A尝试保存状态到云。检测到冲突 | (130, +10) | (130, +5) | (130, +5) | 250 |
设备A通过用本地增量加到云端总量的方式解决冲突 | (140, +10) | (130, +5) | (140, +10) | 250 |
(*):x代表场景未透露的数据值。
您可能想通过在每次保存后不重新设置增量的方式来修复以上存在的问题,这样第二次为每台设备所存储的金币就是目前的总量了。如此,第二次由设备A存储的将是(130, +110),而不是(130, +10)。这种方法还是会遇到表3所列的问题。
表3.修改算法的失败的情形
事件 | 设备A上的数据 | 设备B上的数据 | 云上的数据 | 实际总量 |
初始条件 | (20, x) | (20, x) | (20, x) | 20 |
玩家在设备A上收集100金币 | (120, +100) | (20, x) | (20, x) | 120 |
设备A保存状态到云 | (120, +100) | (20, x) | (120, +100) | 120 |
玩家在设备A上又收集10金币 | (130, +110) | (20, x) | (120, +100) | 130 |
玩家在设备B上收集1金币 | (130, +110) | (21, +1) | (120, +100) | 131 |
设备B尝试保存数据到云。检测到冲突 | (130, +110) | (21, +1) | (120, +100) | 131 |
设备B通过增加本地增量到云数据总量方式解决冲突 | (130, +110) | (121, +1) | (121, +1) | 131 |
设备A尝试上传它的数据到云。检测到冲突 | (130, +110) | (121, +1) | (121, +1) | 131 |
设备A通过将本地增量增加到云总量的方式解决冲突 | (231, +110) | (121, +1) | (231, +110) | 131 |
(*):x代表场景中未透露的书值。
现在这个问题就体现出来了:给了玩家过多的金币。玩家在云上获得了211个金币,但是实际她只手机了111个金币。
分析之前所尝试的策略,它们似乎没有知晓哪些金币已经被计数哪些金币还未被计数的能力,尤其是在多台不同设备连续提交各自数据到云端的时候。
解决此问题的方法是改变云保存的结果,将字符串映射为整数的字典。字典中的每个键-值对代表一个包含金币和保存在云中的金币数量(所有条目的值)的“抽屉”。设计此策略的基本原则是每个设备都有属于他自己的抽屉,且只有每各设备本身可以往抽屉里面存放金币。
字典的结构为(A:a, B:b, C:c, …),抽屉A金币的数量为a,抽屉B的金币数量为b,依次类推。
新的冲突解决算法“抽屉法”流程如下:
- 本地数据:(A:a, B:b, C:c,…)
- 云端数据:(A:a’, B:b’, C:c,’…)
- 更新数据:(A:max(a, a’), B:max(b, b’), C:max(c, c’), …)
举例,本地数据为(A:20, B:4, C:7),云端数据为(B:10, C:2, D:4),那么跟新数据将为(A:20, B:10, C:7, D:14)。注意,应用程序实现冲突解决方案(字典数据)的逻辑取决于具体的应用程序。例如,对于有的应用程序可能会取数据中更小的值。
为测试新算法,将此算法应用到之前提到的每一个场景中。您将会发现,此策略都能够正确处理每个冲突。
表4演示了这个过程,其中的情形基于表3。注意以下几点:
- 在初始状态时,玩家有20个金币。在设备和云端都能够正确的反映这一点。此值代表字典的(X:20)状态,X值的意义并不重要 - 我们不关心初始值来自哪个设备。
- 当玩家在设备A上收集了100金币时,此改变被打包入字典并被保存到云端。字典的值为100,因为此值是玩家在设备A上收集的金币。此时没有对数据进行计算的过程 - 设备A只是简单的报告玩家所搜集到的金币的数量。
- 每次提交的跟设备相关的被打包成字典的数据被发送到云端保存。当瓦加在设备A上收集10个金币时,设备A字典值应该被更新到110。
- 应用程序知道每个在每个设备上的玩家分别收集了多少金币。如此就能够简单的计算金币的总量了。
表4. 键-值对策略的成功
事件 | 设备A上的数据 | 设备B上的数据 | 云上的数据 | 实际总量 |
初始条件 | (X:20, x) | (X:20, x) | (X:20, x) | 20 |
玩家在设备A上收集100金币 | (X:20, A:100) | (X:20) | (X:20) | 120 |
保存设备A的状态到云端 | (X:20, A:100) | (X:20) | (X:20, A:100) | 120 |
玩家在设备A上再获10金币 | (X:20, A:110) | (X:20) | (X:20, A:100) | 130 |
玩家在设备B上收集1金币 | (X:20, A:110) | (X:20, B:1) | (X:20, A:100) | 131 |
设备B尝试保存状态到云。检测到冲突 | (X:20, A:110) | (X:20, B:1) | (X:20, A:100) | 131 |
设备B解决冲突 | (X:20, A:110) | (X:20, A:100, B:1) | (X:20, A:100, B:1) | 131 |
设备A尝试上传数据到云。检测到冲突 | (X:20, A:110) | (X:20, A:100, B:1) | (X:20, A:100, B:1) | 131 |
设备A解决冲突 | (X:20, A:110, B:1) | (X:20, A:100, B:1) | (X:20, A:110, B:1)一共131 | 131 |
云保存数据的大小有限,所以在使用此节中的策略时,注意不要创建任意打的字典。初看每台设备只拥有一个条目,再热情的用户也不太希望它们有成千上万的条目。然而,获取设备ID是有困难的且是个不好的用法,所以您应该使用安装ID,此ID更易获取并且可用。这就意味着安装在每台设备上的应用程序共同拥有一个条目入口。假设每个键-值对占用32字节,因为个人的云缓冲有128k,所以安全条目数最多为4096个。
在现实生活中,数据会比金币数据复杂。在这种情况下,字典中的条目数量更可能会受限制。基于具体的实现,还有可能在当字典条目更改时同时存储时间戳。当检测到自上次几周或几月以来条目被更改了,这就应该删掉旧的条目而将金币数量转存到另外一个条目中。
如何使用安卓同步适配器在云和设备之间转移数据。
2015.12.02
利用安卓设备和网络服务之间的数据同步特性,从用户角度说,能够让应用程序更加有用和吸引力。如传递数据到网络服务能够备份设备数据,同时即使在离线的情况下用户也能够从网络服务转移数据到设备上。在某些情况下,用户会觉得使用网络服务提供的接口来保存和编辑数据供设备后续使用的方式也更简单,用户或许将网络服务当成一个上传数据的中心存储域。
尽管您可以在应用程序中自己设计系统来转移数据,但您还是应该考虑一下使用安卓的同步适配器框架。此框架能够管理和自动地转移数据,且能够在不同应用程序间协调同步操作。当使用此框架时,您可以使用您自己所设计的数据转移的系统中没有的特性:
插件结构(Plug-in architecture)
允许往可调用的组件的系统中添加数据转移代码。
自动化执行
根据几个标准自动进行数据转移,包括数据改变、逝去的时间或某日期。另外,系统转移不会以队列的形式,只要有可能就转移它们。
自动检查网络
系统只在设备联网的情况下进行数据转移。
提升电耗性能
允许将应用程序的数据转移任务集中到一个地方,以让它们同时得到转移。同时也可以联合其它应用程序的数据一起转移。这些因素减少了系统在网络上的操作时间,也就减少的电池用量。
账户管理和认证
若应用程序需要用户凭证或登录,可以选择集成账户管理和认证到数据转移中。
此节描述如何创建同步适配器和包装的绑定服务,如何提供其它的组件来将同步适配器插入到网络,如何运行同步适配器。
注:同步适配器以异步的方式运行,所以可以用它们来定期和高效的传输数据,但不能理解完成。如果需要做实时的数据转移,那应该在AsyncTask或IntentService中完成数据转移任务。
此节内容描述如何增加同步适配器希望能成为应用程序一部分的账户-操控组件,如何简单地创建存根(Stub)验证器组件。
同步适配器框架假设同步适配器在设备存储之间的数据传输和需要登录访问的账户和服务存储关联。因此,框架希望应用程序提供一个叫验证器的组件作为同步适配器的一部分。此组件嵌在谷歌账户和验证框架中并提供标准的接口来处理用户的诸如登录信息的凭证。
2015.12.03
即使应用程序不适用账户,仍旧需要在应用程序中提供一个验证器组件。如果应用程序不适用账户或服务登录,验证器所操控的信息会被忽略,所以可以提供一个包含存根方法实现的验证器组件。同时也需要为应用程序提供绑定服务以让同步适配器框架调用验证器的方法。
此节描述如何定义满足应用程序同步适配器框架的验证器。如果应用程序需要定义验证器来处理用户账户,那就读AbstractAccountAuthenticator的参考文档。
欲在应用程序中添加存根验证器组件,创建一个扩展自AbstractAccountAuthenticator的类,然后提取出需要的方法,让它们都返回null或让它们抛出一个异常。
以下代码片段时实现一个存储验证器类的例子:
/* * Implement AbstractAccountAuthenticator and stub out all * of its methods */
public class Authenticator extends AbstractAccountAuthenticator {
// Simple constructor
public Authenticator(Context context) {
super(context);
}
// Editing properties is not supported
@Override
public Bundle editProperties(
AccountAuthenticatorResponse r, String s) {
throw new UnsupportedOperationException();
}
// Don't add additional accounts
@Override
public Bundle addAccount(
AccountAuthenticatorResponse r,
String s,
String s2,
String[] strings,
Bundle bundle) throws NetworkErrorException {
return null;
}
// Ignore attempts to confirm credentials
@Override
public Bundle confirmCredentials(
AccountAuthenticatorResponse r,
Account account,
Bundle bundle) throws NetworkErrorException {
return null;
}
// Getting an authentication token is not supported
@Override
public Bundle getAuthToken(
AccountAuthenticatorResponse r,
Account account,
String s,
Bundle bundle) throws NetworkErrorException {
throw new UnsupportedOperationException();
}
// Getting a label for the auth token is not supported
@Override
public String getAuthTokenLabel(String s) {
throw new UnsupportedOperationException();
}
// Updating user credentials is not supported
@Override
public Bundle updateCredentials(
AccountAuthenticatorResponse r,
Account account,
String s, Bundle bundle) throws NetworkErrorException {
throw new UnsupportedOperationException();
}
// Checking features for the account is not supported
@Override
public Bundle hasFeatures(
AccountAuthenticatorResponse r,
Account account, String[] strings) throws NetworkErrorException {
throw new UnsupportedOperationException();
}
}
欲让同步适配器框架访问验证器,必须为其创建一个绑定服务。此服务提供能让框架调用验证器并能够在验证器和框架间传递数据的安卓包扎对象。
由于框架第一次开启此服务时需要访问验证器,所以可以在此服务的Service.onCreate()方法中调用验证器的构造函数来实例化验证器。
以下代码片段演示如何定义绑定服务:
/** * A bound Service that instantiates the authenticator * when started. */
public class AuthenticatorService extends Service {
...
// Instance field that stores the authenticator object
private Authenticator mAuthenticator;
@Override
public void onCreate() {
// Create a new authenticator object
mAuthenticator = new Authenticator(this);
}
/* * When the system binds to this Service to make the RPC call * return the authenticator's IBinder. */
@Override
public IBinder onBind(Intent intent) {
return mAuthenticator.getIBinder();
}
}
欲将验证器组件插入到同步适配器和账户框架中,需要为这些框架提供描述组件的元数据。元素据声明为同步适配器创建的账户类型以及声明系统欲展示的用户接口元素(如果想让账户类型对用户可见的话)。在应用程序工程的/res/xml目录中的XML文件中声明元数据。可以给给声明元数据的文件任何名字,尽管通常都被命名为authenticator.xml。
XML文件中包含一个account-authenticator元素,其有以下属性:
android:accountType
同步适配器要求每个同步适配器有自己的账户类型(以域名格式)。框架使用账户类型作为同步适配器内部的辨识符。服务需要登录,账户类型下的用户账户作为登录凭证的一部分。
如果服务不需要登录,仍旧需要提供账户类型。用能控制的域名作为其值。框架将会使用它去管理同步适配器,此值不会被发送给服务。
android:icon
指向包含图标的Drawable资源。如果在res/xml/syncadapter.xml中有android:userVisible=”true”让同步适配器可见,那么就必须提供图标资源。它将在系统设置应用程序的账户板块出现。
android:smallIcon
指向包含更小版本图标的Drawble资源。此资源用于代替android:icon。基于屏幕尺寸,在系统的设置应用程序模块会出现。
android:lable
用于定义用户账户类型的本地字符串。如果在res/xml/syncadapter.xml中指定了android:userVisible=”true”让同步适配器可见,那么就应该提供此字符串。它将出现在系统设置应用程序的账户模块,临近为验证器定义的图标。
以下代码片段展示如何为之前创建的验证器创建XML文件:
<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" android:accountType="example.com" android:icon="@drawable/ic_launcher" android:smallIcon="@drawable/ic_launcher" android:label="@string/app_name"/>
在前一步中,创建了讲验证器连接到同步适配器框架上的绑定服务。欲确定此服务能让系统知道,在应用程序的清单文件中添加service元素,让此元素作为application元素的子元素:
<service android:name="com.example.android.syncadapter.AuthenticatorService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/authenticator" />
</service>
元素intent-filter设置了能够被意图动作android.accounts.AccountAuthenticator触发的过滤器。此动作由系统发送来运行验证器。当过滤器被触发,系统会开启AuthenticatorService,绑定服务将会包装验证器。
meta-data元素为验证器声明了元数据。android:name将元数据连接到了验证器框架。android:resource元素指定了之前所创建的验证器的元数据文件的名字。
除了验证器之外,同步适配器还需要内容提供器。如果应用程序不使用内容提供器,那么去“创建同步适配器”一节;如果需要内容提供器,则看下一节“创建存根内容提供器”。
此节内容描述如何添加同步适配器框架所希望能成为应用程序一部分的内容提供器组件,此节假设应用程序没有使用内容提供器,因此演示如何增加存根组件,如果应用程序中已经提供了内容提供器,那么可跳过本节。
2015.12.04
同步适配器框架被设计来和能够灵活、高安全地管理设备数据的内容提供器一起工作。因此,同步适配器希望应用程序中有一个为本地数据设计的内容提供器。如果同步适配器框架尝试运行同步适配器,而应用程序中无内容提供器,那么同步适配器将会崩溃。
如果您正在开发一个从服务器传数据到设备的新的应用程序,最好将本地数据存储到内容提供器中。除了同步适配器的重要性,内容提供器不仅提供多样的安全效益,它还被设计来处理在安卓系统中的数据存储。欲学习更多关于创建内容提供器的知识,见“创建内容提供器(Creating a Content Provider)”。
如果已经使用其他的方式存储了本地数据,仍旧可以使用同步适配器来传递数据。欲满足同步适配器对内容提供器的要求,可以增加一个存根内容提供器到应用程序中。存根提供内容提供器的类实现,但其所有的方法都返回null或0。如果增加了存储提供器,就可以使用同步适配器来一任何存储机制传输数据了。
若在应用程序中已经有了内容提供器,那就不需要在增加存储内容提供器了。在这种情况下,就可以跳过本节进入“创建同步适配器”一节。若应用程序还未有内容提供器,此节将描述如何增加存根内容提供器来让同步适配器插入到框架中。
欲在应用程序中创建存根内容提供器,扩展ContentProvider类并改写其需要的方法。以下代码片段演示如何创建存根提供器:
/* * Define an implementation of ContentProvider that stubs out * all methods */
public class StubProvider extends ContentProvider {
/* * Always return true, indicating that the * provider loaded correctly. */
@Override
public boolean onCreate() {
return true;
}
/* * Return no type for MIME type */
@Override
public String getType(Uri uri) {
return null;
}
/* * query() always returns no results * */
@Override
public Cursor query(
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder) {
return null;
}
/* * insert() always returns null (no URI) */
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
/* * delete() always returns "no rows affected" (0) */
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
/* * update() always returns "no rows affected" (0) */
public int update(
Uri uri,
ContentValues values,
String selection,
String[] selectionArgs) {
return 0;
}
}
同步适配器框架通过检查应用程序是否在清单文件中声明内容提供器来确认应用程序是否有内容提供器。欲在清单文件中声明存根提供器,增加provider元素到清单文件中,并设置设置以下属性:
android:name=”com.example.android:datasync.provider.StuProvider”
指定实现存根内容提供器的类的全名。
android:authorities=”com.example.android.datasync.provider”
标识存根内容提供器的URI权威。此值由应用程序包名和后缀字符串”.provider”组成。即使声明了存根提供器,但没有任何东西回来访问此提供器。
android:exported=”false”
判断是否有另外的应用程序能访问此内容提供器。将此值设置为false,因为不需要其它的应用程序来访问此提供器。此值不会影响同步适配器和内容提供器之间的交互。
android:syncable=”true”
设置此值用来表示此提供器是可同步的。若将此属性设置为true,就不再需要在代码中调用setIsSyncable()。此值允许同步适配器框架和内容提供器之间传递数据,在正确设置它们后传输就会发生。
以下代码片段展示如何往应用程序清单文件中添加provider元素:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.network.sync.BasicSyncAdapter"
android:versionCode="1"
android:versionName="1.0" >
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
...
<provider
android:name="com.example.android.datasync.provider.StubProvider"
android:authorities="com.example.android.datasync.provider"
android:exported="false"
android:syncable="true"/>
...
</application>
</manifest>
到此就已经为同步适配器创建了依赖,可以通过封装传输数据的代码来实现该组件(同步适配器)。下一节将会描述在应用程序中如何增加此组件。
此节描述如何保证需要传输的数据的代码到组件中,然后让同步适配器框架能够自动将它们传输。
应用程序中的同步适配器组件封装了在设备和服务器之间传输数据任务的代码。根据应用程序提供的调度和触发事件,同步适配器将运行同步适配器组件中的代码。欲添加同步适配器组件到应用程序中,需要完成以下步骤:
同步适配器类
此类包装兼容欲同步适配器框架用来传数据的代码。
绑定服务
允许同步适配器框架运行同步适配器类中代码的组件。
同步适配器XML元数据文件
包含同步适配器信息的文件。框架通过读此文件来决定如何载入和调度数据传输。
在应用程序清单文件中声明
用来声明绑定服务并指向指定的同步适配器元数据的XML文件。
此节描述如何定义以上这些元素。
此部分内容将描述如何创建包装了传输数据代码的同步适配器类。创建此类包括扩展同步适配器基础类。定义此类的构造函数以及实现数据传输任务的方法。
欲创建同步适配器组件,首先需要扩展AbstractThreadedSyncAdapter且重写器构造函数。构造函数用来运行同步适配器被创建时的初始化代码,就如活动的Activity.onCreate()方法用来初始化活动一样。例如,假设应用程序使用内容提供器来存储数据,那就使用构造函数获取一个ContentResolver实例。因为在Android平台的3.0版本中增加了一个支持parallelSyncs 参数的构造函数,所以需要实现两个构造函数来维持兼容性。
注:同步适配器只能跟一个同步适配器组件实例工作。实例化同步适配器组件的更多席间见 Bind the Sync Adapter to the Framework。
以下代码片段展示如何扩展AbstractThreadedSyncAdapter并实现其构造函数:
/** * Handle the transfer of data between a server and an * app, using the Android sync adapter framework. */
public class SyncAdapter extends AbstractThreadedSyncAdapter {
...
// Global variables
// Define a variable to contain a content resolver instance
ContentResolver mContentResolver;
/** * Set up the sync adapter */
public SyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
/* * If your app uses a content resolver, get an instance of it * from the incoming Context */
mContentResolver = context.getContentResolver();
}
...
/** * Set up the sync adapter. This form of the * constructor maintains compatibility with Android 3.0 * and later platform versions */
public SyncAdapter(
Context context,
boolean autoInitialize,
boolean allowParallelSyncs) {
super(context, autoInitialize, allowParallelSyncs);
/* * If your app uses a content resolver, get an instance of it * from the incoming Context */
mContentResolver = context.getContentResolver();
...
}
同步适配器组件不会自动传输数据。它包装应用程序传输数据的代码,因此同步适配器框架可以在后台运行数据传输,不会参与到应用程序的前台中。当框架已能同步应用程序数据,它将调用onPerformSync()方法。
欲更加方便的将主应用程序代码中的数据传递给同步适配器组件,同步适配器框架调用onPerformSync()时使用以下参数:
Account
关联触发同步适配器组件事件的Account对象。如果服务不适用用户账户,就不必在此对象中包含任何信息。
Extras
一个Bundle,包含发送标志(由触发同步适配器的事件发送)。
Authority
系统内容提供器的权威。应用程序应该要能够访问此提供器。通常来说,要在应用程序中将权威和内容提供器关联起来。
Content provider client
由Authority参数指向的内容提供器(ContentProviderClient)。ContentProviderClient是一个轻量的公共的内容提供器接口。它拥有同ContentResolver一样的基本功能。若您在应用程序中使用该内容提供器存储数据,就可以将提供器连接到到此对象。否则,忽略这个参数。
Sync result
用来发送信息到同步适配器框架的SyncResult对象。
以下代码片段展示了onPerformSync()的整个结构:
/*
* Specify the code you want to run in the sync adapter. The entire
* sync adapter runs in a background thread, so you don't have to set
* up your own background processing.
*/
@Override
public void onPerformSync(
Account account,
Bundle extras,
String authority,
ContentProviderClient provider,
SyncResult syncResult) {
/*
* Put the data transfer code here.
*/
...
}
onPerformSync()的具体实现要根据数据的同步需求以及服务连接的协议,以下是需要实现的几个基本任务:
连接到服务
尽管在数据传输开始时可假设网络可用,但同步适配器框架不会自动连接到服务。
下载和上传数据
同步适配器不会自动执行任何数据传输的任务。若欲从服务下载数据并将它存储到内容提供器中,就必须提供请求、下载数据和将数据插入提供器中的代码。同理,如果欲发送数据给服务器,就需要从文件、数据库或提供器中读数据,再发送。同时还必须处理数据传输过程中所发生的网络错误。
处理数据冲突(判断当前数据)
同步适配器不会自动处理服务端和设备端的数据冲突。同时,它也不会自动检测服务端的数据是否新于设备上的数据,反之亦然。其实,此部分需要开发者自己提供算法来处理此情况。
清理
数据传输完毕后关闭跟服务的连接并清理掉临时文件和缓存。
注:同步适配器在后台线程中运行onPerformSync(),所以不必设置后台进程。
除跟同步相关的任务外,还应该将跟网络相关的任务添加到onPerformSync()中。通过在该方法中集中所有的网络任务,要根据需要来开启和停止网络接口以保存电量。欲学更多如何高效的访问网络,见 Transferring Data Without Draining the Battery,此类描述了几种可包含到数据传输代码中的网络访问任务。
到此,已在同步适配器组件中包装了数据传输的代码,还得提供框架来访问这些代码。欲实现此,需要创建一个绑定服务来传递同步适配器组件的安卓绑定到框架。通过此绑定对象,框架就能够调用onPerformSync()方法。
在服务的onCreate()方法中实例化一个同步适配器组件。通过在onCreate()中实例化组件,当服务启动时方创建该组件,此过程发生在框架第一次尝试运行数据传输时刻。需要在线程安全中实例化组件,以防同步适配器框架中排队了许多的执行操作,这样同步适配器的响应的触发和调度就得等待。
例如,以下代码片段展示如何创建类来实现绑定服务、实例化同步适配器组件以及过去安卓绑定对象:
package com.example.android.syncadapter;
/** * Define a Service that returns an IBinder for the * sync adapter class, allowing the sync adapter framework to call * onPerformSync(). */
public class SyncService extends Service {
// Storage for an instance of the sync adapter
private static SyncAdapter sSyncAdapter = null;
// Object to use as a thread-safe lock
private static final Object sSyncAdapterLock = new Object();
/* * Instantiate the sync adapter object. */
@Override
public void onCreate() {
/* * Create the sync adapter as a singleton. * Set the sync adapter as syncable * Disallow parallel syncs */
synchronized (sSyncAdapterLock) {
if (sSyncAdapter == null) {
sSyncAdapter = new SyncAdapter(getApplicationContext(), true);
}
}
}
/** * Return an object that allows the system to invoke * the sync adapter. * */
@Override
public IBinder onBind(Intent intent) {
/* * Get the object that allows external processes * to call onPerformSync(). The object is created * in the base class code when the SyncAdapter * constructors call super() */
return sSyncAdapter.getSyncAdapterBinder();
}
}
注:欲见更多关于同步适配器绑定服务的例子,见本节样例应用程序。
同步适配器框架要求每个同步适配器拥有一个账户类型。在“添加验证器元数据文件”一节声明账户类型值。现在已经在Android系统中设置了账户类型。欲设置账户类型,通过调用addAccountExplicitly()来添加一个不用的用户账户类型。
调用此方法最佳的地方是在应用程序的活动的onCreate()方法中。以下代码片段展示如何实现这个过程:
public class MainActivity extends FragmentActivity {
...
...
// Constants
// The authority for the sync adapter's content provider
public static final String AUTHORITY = "com.example.android.datasync.provider"
// An account type, in the form of a domain name
public static final String ACCOUNT_TYPE = "example.com";
// The account name
public static final String ACCOUNT = "dummyaccount";
// Instance fields
Account mAccount;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
// Create the dummy account
mAccount = CreateSyncAccount(this);
...
}
...
/** * Create a new dummy account for the sync adapter * * @param context The application context */
public static Account CreateSyncAccount(Context context) {
// Create the account type and default account
Account newAccount = new Account(
ACCOUNT, ACCOUNT_TYPE);
// Get an instance of the Android account manager
AccountManager accountManager =
(AccountManager) context.getSystemService(
ACCOUNT_SERVICE);
/* * Add the account and account type, no password or user data * If successful, return the Account object, otherwise report an error. */
if (accountManager.addAccountExplicitly(newAccount, null, null))) {
/* * If you don't set android:syncable="true" in * in your <provider> element in the manifest, * then call context.setIsSyncable(account, AUTHORITY, 1) * here. */
} else {
/* * The account exists or some other error occurred. Log this, report it, * or handle it internally. */
}
}
...
}
欲将同步适配器插入框架中,需要为框架提供描述组件的元数据和额外的标志。元数据指定为同步适配器所创建的账户类型、声明应用程序的内容提供器的权威、控制跟同步适配器相关的系统用户接口以及声明其它的同步相关的标志。在应用程序工程的/res/xml目录下指定声明元数据的XML。可以给这个文件取任意的名字,尽管它通常都被命名为syncadapter.xml。
XML文件包含一个sync-adapter元素,此元素包含以下属性:
android:contentAuthority
内容提供器的URI权威。如果在前面一节“ Creating a Stub Content Provider”中为应用程序创建了存根内容提供器,为清单文件中的provider元素下的android:authorities属性指定值。此属性在 Declare the Provider in the Manifest.一节中被描述得更多。
如果用同步适配器从内容提供器传输数据到服务端,此值应该是同数据的内容URI权威值一样。此值也是声明在应用程序清单文件中用来描述提供器的provider元素下的android:authorities属性的其中一个值。
android:accountType
同步适配器框架需要的账户类型。此值必须跟创建权威元数据文件时提供的账户类型值一样,正如 Add the Authenticator Metadata File一节中所描述。它也是为常量ACCOUNT_TYPE(在 Add the Account Required by the Framework一节中描述的代码片段中)所指定的值。
设置属性
android:userVisible
用来设置同步适配器的账户类型的可见性。默认时,账户图标和标签都可见,所以欲让标签和图标不可见就需要用此值来控制。若让账户类型不可见,仍旧可以通过应用程序的活动的用户界面让用户控制同步适配器。
android:supportsUploading
运行上传数据到云端。若应用程序只下载数据就将此值设置为false。
android:allowParallelSyncs
运行多个同步适配器组件实例同时运行。如果应用程序支持多个用户账户且欲让多个用户并行的传输数据则设置此值。若应用程序不允许多个数据传输,设置此值也不会有任何影响。
android:isAlwaysSyncable
用来表明同步适配器框架能够在指定的任何时间运行同步适配器。如果想通过程序控制同步适配器何时运行,将此值设置为false,然后调用requestSync()来运行同步适配器。欲学习更多关于运行同步适配器,见 Running a Sync Adapter。
以下样例展示为同步适配器创建的XML(使用单个不适用的账户且只下载数据)。
<?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android" android:contentAuthority="com.example.android.datasync.provider" android:accountType="com.android.example.datasync" android:userVisible="false" android:supportsUploading="false" android:allowParallelSyncs="false" android:isAlwaysSyncable="true"/>
2015.12.05
一旦往应用程序中添加了同步适配器组件,就必须请求跟使用此组件相关的权限,还必须声明添加的绑定服务。
因为同步适配器会运行在设备和网络间传输数据的代码,所以需要请求访问网络的权限。另外,应用程序还必须请求请求读和写同步适配器设置的权限,这样就可以在应用程序中的其它组件中编程来控制同步适配器。同时也必须请求允许应用程序使用在“ Creating a Stub Authenticator”创建的权威组件的权限。
欲请求这些权限,将以下元素增加到清单文件中并作为manifest的子元素:
android.permission.INTERNET
允许同步适配器代码访问网络,这样就能够从服务端下载和上传数据。若之前曾添加了此权限就不必再增加了。
android:perission.READ_SYNC_SETTINGS
允许应用程序读当前同步适配器的设置。例如,若需调用getIsSyncable()则需要此权限。
android:permission.WRITE_SYNC_SETTINGS
允许应用程序控制同步适配器设置。需要此权限来让同步适配器调用addPeriodicSync()周期的运行。对于调用requestSync()则不需此权限。欲学习更多关于运行同步适配器,见 Running A Sync Adapter。
以下代码片段展示如何增加此权限:
<manifest>
...
<uses-permission
android:name="android.permission.INTERNET"/>
<uses-permission
android:name="android.permission.READ_SYNC_SETTINGS"/>
<uses-permission
android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<uses-permission
android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
...
</manifest>
最后,欲声明框架用来和同步适配器交互的绑定服务,将以下代码增加到清单文件中的application元素下:
<service android:name="com.example.android.datasync.SyncService" android:exported="true" android:process=":sync">
<intent-filter>
<action android:name="android.content.SyncAdapter"/>
</intent-filter>
<meta-data android:name="android.content.SyncAdapter" android:resource="@xml/syncadapter" />
</service>
intent-filter元素用来设置由android.content.SyncAdapter意图动作触发的过滤器,此意图动作由系统运行同步适配器时发送。当过滤器被触发,系统将会开启创建的绑定服务,即在此例中的SyncService。android:exported=”true”属性允许进程而非应用程序(包括系统)访问服务。android:process=”:sync”属性告知系统由名为sync的全局共享进程运行服务。若应用程序中有多个同步适配器且能够分享此进程,这就能够减少系统消耗。
meta-data元素提供之前为同步适配器所创建的元数据文件名。android:name属性表示此元数据是为同步适配器框架而生。android:resource元素指定元数据文件名。
至此已经拥有了同步适配器相关的所有组件。下一节将描述如何告知同步适配器框架来运行同步适配器,同时如何响应在调度方案中的某事件的发生。
此节描述使用同步适配器框架如何触发和规划数据传输。
在本章的前几节中,已经介绍了如何创建包装数据传输的代码的同步适配器组件以及如何添加其他的组件来将同步适配器插入到系统中。这几节内容包含了安装应用程序所有的需要,但还未有运行同步适配器的任何代码。
应该基于调度或一些间接的事件结果来运行同步适配器。例如,可能需要同步适配器运行一个定期的调度,在一个确定的时间之后或在一天中某个特殊的时刻。您可能还想在设备数据有改变时运行同步适配器。应该避免直接根据用户动作来运行同步适配器,因为如此做就不能获得同步适配器框架的调度的全部能力。例如,应该避免在用户界面中提供刷新按钮。
您拥有以下的选择来运行同步适配器:
当服务端数据改变时
运行同步适配器来响应从服务端来的信息,表明基于服务端的数据已经改变。此选择允许您从服务端刷新数据到设备而不用通过轮询服务端来降低程序性能或消耗更多的电量。
当设备数据改变时
当设备数据发生改变时运行同步适配器。此选择允许发送更改的设备数据到服务端,当需要确保服务端永远都保持最新数据时此选择尤其重要。若数据存储在内容提供器中,此选择的实现就很简单。如果使用了存根内容提供器,检测数据的改变可能会更加复杂一些。
当系统发出网络信息时
当android系统发出保持TCP/IP连接畅通时的消息时运行同步适配器。此信息时网络框架的一个基础的一部分。此选择是唯一能够使同步适配器自动运行的方式。考虑结合基于间隔的同步适配器来使用。
定期间隔
在超过设置的间隔时间时或在每天的某个确切时刻运行同步适配器。
按照要求
根据用户动作来运行同步适配器。然而,为提升用户体验,最好还是按照其中一个自动选择来设置运行同步适配器。通过使用自动选择,能够保存电量和网络资源。
此节的后续内容将分别详细描述这些选择。
如果应用程序从服务端获取数据来传输且服务端数据时常改变,就可使用同步适配器在数据改变时更新下载。欲运行同步适配器,在应用程序中让服务端发一个特殊的信息给BroadcastReceiver。在响应此信息时,调用ContentResolver.requestSync()来通知同步适配器框架运行同步适配器。
谷歌云信息(GCM)同时提供了服务和设备组件来让此信息工作。私用GCM触发比通过轮询服务端状态更加可靠和高效。轮询需要服务总是活跃的,GCM使用BroadcastReceiver以达到当信息到达时服务方活跃。即使没有更新发生时,定期的轮询也会消耗电量,而GCM只有在需要发送数据时才会耗电。
注:若使用GCM通过广播来触发安装在所有设备上的应用程序中的同步适配器,记住它们差不多同时收到此信息。这种情况会引起同步适配器的多个实例在同时间里运行,这可能会引起服务和网络超载负荷。欲避免广播所有设备的情形,应该考虑为每台设备上的同步适配器排队来接收网络信息。
以下代码片段展示如何运行requestSync()来响应收到的GCM信息:
public class GcmBroadcastReceiver extends BroadcastReceiver {
...
// Constants
// Content provider authority
public static final String AUTHORITY = "com.example.android.datasync.provider"
// Account type
public static final String ACCOUNT_TYPE = "com.example.android.datasync";
// Account
public static final String ACCOUNT = "default_account";
// Incoming Intent key for extended data
public static final String KEY_SYNC_REQUEST =
"com.example.android.datasync.KEY_SYNC_REQUEST";
...
@Override
public void onReceive(Context context, Intent intent) {
// Get a GCM object instance
GoogleCloudMessaging gcm =
GoogleCloudMessaging.getInstance(context);
// Get the type of GCM message
String messageType = gcm.getMessageType(intent);
/* * Test the message type and examine the message contents. * Since GCM is a general-purpose messaging system, you * may receive normal messages that don't require a sync * adapter run. * The following code tests for a a boolean flag indicating * that the message is requesting a transfer from the device. */
if (GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType)
&&
intent.getBooleanExtra(KEY_SYNC_REQUEST)) {
/* * Signal the framework to run your sync adapter. Assume that * app initialization has already created the account. */
ContentResolver.requestSync(ACCOUNT, AUTHORITY, null);
...
}
...
}
...
}
如果应用程序将数据存储在内容提供器中,且当更新内容提供器时也欲更新服务端,可设置应用程序自动的运行同步适配器。欲实现此过程,为内容提供器注册一个观察器(observer)。当内容提供器中的数据发生改变时,内容提供器框架将会调用观察器。在观察器中,调用requestSync()来告知框架运行同步适配器。
注:如果应用程序使用了存根内容提供器,在内容提供器中就无任何数据且onChange()永不会被调用。在这种情况下,需要应用程序自己提供检测设备数据改变的机制。当数据改变时,此机制也应该负责调用requestSync()。
欲为内容提供器提供观察器,扩展ContentObserver类并实现其内onChange()方法的格式。在onChange()中,调用requestSync()来开启同步适配器。
欲注册观察器,将它作为registerContentObserver()方法的参数。在此方法中,也需要传递欲检测数据的内容URI。内容提供器框架会比较URI和由作为更改提供器ContentResolver方法的参数的内容URIs,如ContentResolver.insert()。如果两者匹配,调用之前所实现的ContentObserver.onChange()。
以下代码片段展示当表格改变时如何定义调用requestSync()的ContentObserver:
public class MainActivity extends FragmentActivity {
...
// Constants
// Content provider scheme
public static final String SCHEME = "content://";
// Content provider authority
public static final String AUTHORITY = "com.example.android.datasync.provider";
// Path for the content provider table
public static final String TABLE_PATH = "data_table";
// Account
public static final String ACCOUNT = "default_account";
// Global variables
// A content URI for the content provider's data table
Uri mUri;
// A content resolver for accessing the provider
ContentResolver mResolver;
...
public class TableObserver extends ContentObserver {
/* * Define a method that's called when data in the * observed content provider changes. * This method signature is provided for compatibility with * older platforms. */
@Override
public void onChange(boolean selfChange) {
/* * Invoke the method signature available as of * Android platform version 4.1, with a null URI. */
onChange(selfChange, null);
}
/* * Define a method that's called when data in the * observed content provider changes. */
@Override
public void onChange(boolean selfChange, Uri changeUri) {
/* * Ask the framework to run your sync adapter. * To maintain backward compatibility, assume that * changeUri is null. ContentResolver.requestSync(ACCOUNT, AUTHORITY, null); } ... } ... @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ... // Get the content resolver object for your app mResolver = getContentResolver(); // Construct a URI that points to the content provider data table mUri = new Uri.Builder() .scheme(SCHEME) .authority(AUTHORITY) .path(TABLE_PATH) .build(); /* * Create a content observer object. * Its code does not mutate the provider, so set * selfChange to "false" */
TableObserver observer = new TableObserver(false);
/* * Register the observer for the data table. The table's path * and any of its subpaths trigger the observer. */
mResolver.registerContentObserver(mUri, true, observer);
...
}
...
}
当网络连接可用时,安卓系统会每隔几秒就发送出一个信息以保持设备TCP/IP连接畅通。此信息也会到达每个应用程序中的ContentResolver。通过调用setSyncAutomatically(),当ContentResolver接收到此信息时运行同步适配器。
当网络信息被发出时调度同步适配器运行,要确保在网络可用时同步适配器总是能够被调度运行。如果不必墙纸数据传输来响应数据改变时使用此选择,但需要确保数据的定期更新。同理,如果不想修复调度但想按照某频率运行同步适配器也可以使用此选择。
由于setSyncAutomatically()方法不会关闭addPeriodicSync(),同步适配器在一段短的时间内有可能重复触发同步适配器。如果不想按照定期的调度来运行同步适配器,那就应该关闭setSyncAutomatically()的调用。
以下代码片段展示如何配置ContentResolver来运行同步适配器响应网络信息:
public class MainActivity extends FragmentActivity {
...
// Constants
// Content provider authority
public static final String AUTHORITY = "com.example.android.datasync.provider";
// Account
public static final String ACCOUNT = "default_account";
// Global variables
// A content resolver for accessing the provider
ContentResolver mResolver;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
// Get the content resolver for your app
mResolver = getContentResolver();
// Turn on automatic syncing for the default account and authority
mResolver.setSyncAutomatically(ACCOUNT, AUTHORITY, true);
...
}
...
}
通过在运行间设置一段时间可以定期的运行同步适配器,或设置在每天的某个确切时刻运行同步适配器,两者也可同时被实现。定期的运行同步适配器运行粗糙的匹配服务端的更新间隔。
同理,当服务端闲时通过调度同步适配器在晚间运行也可以上传设备数据。大多数的用户在晚间给设备充电,所以这段时间通常可用来完成此项操作。此外,运行同步适配器时设备不会运行其它的任务。然而,如果采用了此方法,需要确保每个设备触发数据传输都在不同的时间。如果所有的设备同时运行同步适配器,很可能使网络超负荷。
通常来讲,如果用户不需要立即更新,定期运行就变得比较敏感。在平衡数据更新和更小的不怎么使用设备资源的同步适配时定期运行也会显得敏感。
欲定期的运行同步适配器,调用addPeriodicSync()。此方法调度同步适配器在一段确切的时间消失后运行。因为同步适配器框架需要负责多个同步适配器的执行且需要最大优化电池的使用效率,此定期时间可能会在几秒间变化。同时,在网络不可用时,框架不会运行同步适配器。
注意addPeriodicSync()不会让同步适配器额在每天的指定时间运行。欲让同步适配器在每天的某一时刻运行,使用一个重复的闹铃作为触发。重复的闹铃的更多细节在AlarmManager被描述得更多。如果使用有少许变化的setInexactRepeating()来设置每天时刻的触发,也需要设置一个随机的开始值来确保每台设备上的同步适配器不会同时运行。
addPeriodicSync()方法不会关闭setSyncAutomatically()方法,所以在某段短的时间内可能会有多个运行的同步适配器。同时,只有几个同步适配器控制标志被允许调用addPeriodicSync();不允许被调用的标志在addPeriodicSync()相关的文档中被描述。
以下代码片段展示如何调度同步适配器的运行:
public class MainActivity extends FragmentActivity {
...
// Constants
// Content provider authority
public static final String AUTHORITY = "com.example.android.datasync.provider";
// Account
public static final String ACCOUNT = "default_account";
// Sync interval constants
public static final long SECONDS_PER_MINUTE = 60L;
public static final long SYNC_INTERVAL_IN_MINUTES = 60L;
public static final long SYNC_INTERVAL =
SYNC_INTERVAL_IN_MINUTES *
SECONDS_PER_MINUTE;
// Global variables
// A content resolver for accessing the provider
ContentResolver mResolver;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
// Get the content resolver for your app
mResolver = getContentResolver();
/* * Turn on periodic syncing */
ContentResolver.addPeriodicSync(
ACCOUNT,
AUTHORITY,
Bundle.EMPTY,
SYNC_INTERVAL);
...
}
...
}
根据用户的请求来运行同步适配器是下策。框架在运行同步适配器时根据调度算法会保存保存电池用量。按照选择来运行同步适配器以响应高效实用电池用量。
比较下,允许用户来运行同步适配器就意味着同步适配器自己运行,即实用网络和电量资源都可能不那么高效。同时,提供按照用户需求来运行同步适配器可能会导致无数据更新时也运行了同步适配器,在无数据刷新的情况下同步适配器以低效的方式实用了电量。通常来讲,应用程序都是通过其他的信号或调度算法来触发同步适配器的运行,而不是用户输入。
然而,如果实在是需要按照用户需求来运行同步适配器,设置同步适配器可手动运行的标志,然后调用ContentResolver.requestSync()。
按照用户需要传输需要以下标志:
SYNC_EXTRAS_MANUAL
强制手动运行同步适配器。同步适配器框架忽略存在的设置,诸如被setSyncAutomatically()设置的标志。
SYNC_EXTRAS_EXPEDITED
强制立即启动同步。如果不适用此值,系统可能会等待几秒在运行同步请求,因为它会尝试通过在短时间内调度许多的请求来优化电量的使用。
以下代码片段展示如何调用requestSync()来响应按钮点击事件:
public class MainActivity extends FragmentActivity {
...
// Constants
// Content provider authority
public static final String AUTHORITY =
"com.example.android.datasync.provider"
// Account type
public static final String ACCOUNT_TYPE = "com.example.android.datasync";
// Account
public static final String ACCOUNT = "default_account";
// Instance fields
Account mAccount;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
/* * Create the dummy account. The code for CreateSyncAccount * is listed in the lesson Creating a Sync Adapter */
mAccount = CreateSyncAccount(this);
...
}
/** * Respond to a button click by calling requestSync(). This is an * asynchronous operation. * * This method is attached to the refresh button in the layout * XML file * * @param v The View associated with the method call, * in this case a Button */
public void onRefreshButtonClick(View v) {
...
// Pass the settings flags by inserting them in a bundle
Bundle settingsBundle = new Bundle();
settingsBundle.putBoolean(
ContentResolver.SYNC_EXTRAS_MANUAL, true);
settingsBundle.putBoolean(
ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
/* * Request the sync for the default account, authority, and * manual sync settings */
ContentResolver.requestSync(mAccount, AUTHORITY, settingsBundle);
}
2015.12.06
如何在网络上使用迸发来执行快速、可扩展的用户界面操作。
Vollery是能够让安卓应用程序网络更简单(更重要的是)更快的HTTP库。通过打开AOSP库可使Vollery可用。
Vollery能提供以下便利:
- 自动调度网络请求。
- 多个并行网络连接。
- 使用标准的HTTP缓存相关的硬盘和内存来响应缓存。
- 支持请求优先级。
- 有提供取消请求的API。可取消单个请求,可设置取消请求的块或范围。
- 易于定制,如重试或退后。
- 使得用从网络提取到的数据填充用户界面变得更加的有秩序。
- 调试和跟踪的工具。
Vollery优于用来填充用户界面的RPC-类型操作,如提取搜索结果的某一页数据作为结构化数据。它简单的集成了任何一种协议并提供一个支持原始字符串、图片以及JSON的盒子。通过提供内建的特性来支持您所需要的特性,通过使用Vollery,您可以自由的创建写模板代码,它还允许在应用程序中集中指定的逻辑。
Vollery不适合含大量下载或流水线的操作,因为Vollery在解析器件都是在内存中操控响应。对于大量下载这样的操作,考虑使用像DownloadManager这样的选择。
Vollery库核在frameworks/vollery中的开放AOSP库中开发且包含主请求调度、一套在Vollery的“工具箱”中通用的工具。添加Vollery到工程中最简单的方式是复制Vollery库并将其设置工程的一个库:
[1]. 通过以下命令得到Vollery库的复制品
git clone https://android.googlesource.com/platform/frameworks/volley
[2]. 将下载的资源作为安卓库导入到应用程序中[在用ADT来管理Eclipse工程中有描述]或将其制作成.jar文件。
2015.12.07
学习如何使用Volley默认行为来发送简单的请求,以及如何取消请求。
在高版本中,通过创建RequestQueue并给其传递Request对象而使用Volley。RequestQueue管理工作线程运行“网络操作”、“读/写缓存”以及“解析响应”。请求解析原始响应以及Volley调度解析响应会回到主线程中传送。
此节描述如何用Volley.newRequestQueue这个方便的方法来发送请求,此方法能够设置RequestQueue。在下一节“设置请求队列”中,将描述如何设置RequestQueue。
此节同样描述如何在RequestQueue中增加或取消请求。
欲使用Volley,必须在应用程序清单文件中添加android.permission.INTERNET权限。否则,应用程序不能连接到网络。
Vollery提供方便的方法Volley.newRequestQueue来设置RequestQueue,通过使用其中的默认值然后启动队列。如下例:
final TextView mTextView = (TextView) findViewById(R.id.text);
...
// Instantiate the RequestQueue.
RequestQueue queue = Volley.newRequestQueue(this);
String url ="http://www.google.com";
// Request a string response from the provided URL.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
// Display the first 500 characters of the response string.
mTextView.setText("Response is: "+ response.substring(0,500));
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
mTextView.setText("That didn't work!");
}
});
// Add the request to the RequestQueue.
queue.add(stringRequest);
Volley常在主线程中提供解析响应。结合控制接收到的数据运行在主线程中能够方便的填充用户界面,因为根据响应控制器可自由的修改用户界面控制,这对许多由库提供的许多重要的语义非常关键,尤其是跟取消请求相关。
见“设置请求队列”来描述如何设置RequestQueue,用此方法取代方便的Volley.newRequestQueue方法。
欲发送请求,需要简单的构造一个请求并通过add()将其添加到RequestQueue,正如以上所示。一旦增加请求它就会通过管道、获取服务并且获取原始响应的解析和传递。
当调用add()时,Volley运行一个缓存处理线程和网络调度线程池。当将请求增加到队列中时,请求被缓存线程获取并激起一个触发:如果请求能从缓存被服务,缓存响应在缓存线程中且解析响应被发送到主线程。如果请求不能从缓存服务,他就被列入网络队列中。第一个可用的线程将从队列中提取此请求,执行HTTP变换,在网络线程中解析响应,将响应写入缓存,最后将细节响应传递到主线程中。
注意要将费时的I/O阻塞和解析/解码操作放到其它的线程中运行。可以在其它任何线程中增加请求,但响应常在主线程中。
欲取消一个请求,对Request对象调用cancel()方法。一旦取消了请求,Volley保证响应处理程序将不会再被调用。这实际意味着可以在活动的onStop()方法中取消所有挂起的请求以不必用getActivity() == null检查来回收响应城里程序,或者检查onSaveInstanceState()或其它程序样板是否已经被取消。
欲利用此行为,应跟踪所有正在运行的请求以在合适的时间中就能够取消它们。有一个简单的方法:为每个请求关联一个标记对象。可以用此标记提供取消请求的判断。例如,可以用活动标记所有的请求,然后在onStop()中调用requestQueue.cancelAll(this)。
以下是使用标记字符串的例子。
[1] 定义标记并将其添加到请求中
public static final String TAG = "MyTag";
StringRequest stringRequest; // Assume this exists.
RequestQueue mRequestQueue; // Assume this exists.
// Set the tag on the request.
stringRequest.setTag(TAG);
// Add the request to the RequestQueue.
mRequestQueue.add(stringRequest);
[2] 在活动的onStop()方法中,取消所有拥有此标记的请求
@Override
protected void onStop () {
super.onStop();
if (mRequestQueue != null) {
mRequestQueue.cancelAll(TAG);
}
}
要注意何时取消请求。如果是基于利用响应处理程序来提前一个状态或开启另一个进程,那就需要为此设置监听。如此,响应处理程序不会再被调用。
学习如何设置RequestQueue,以及如何实现单模式来创造能够跟应用程序有相同生命的RequestQueue。
上一节展示了如何使用便利的Volley.newRequestQueue方法(通过利用Volley默认的行为)来设置RequestQueue。此节将展示创建RequestQueue的精确步骤,以允许在应用程序中应用自定义的行为。
此节同时描述创建RequestQueue单例的推荐实践,这会让RequestQueue跟应用程序的声明期一样长。
RequestQueue需要两件事来完成其工作:执行请求传输的网络以及操控缓存的缓存。在Volley中的工具箱中有这两件事情的可用标准实现:DiskBasedCache通过内存索引提供了每次响应一个文件(one-file-per-response)的缓存,BasicNetwork根据优先的HTTP客户端提供了网络传输。
BasicNetwork是Volley的默认网络实现。必须要用应用程序使用的连接到网络的HTTP客户端来初始化BasicNetwork。典型的连接是HttpURLConnection。
以下代码片段展示了设置RequestQueue的步骤:
RequestQueue mRequestQueue;
// Instantiate the cache
Cache cache = new DiskBasedCache(getCacheDir(), 1024 * 1024); // 1MB cap
// Set up the network to use HttpURLConnection as the HTTP client.
Network network = new BasicNetwork(new HurlStack());
// Instantiate the RequestQueue with the cache and network.
mRequestQueue = new RequestQueue(cache, network);
// Start the queue
mRequestQueue.start();
String url ="http://www.example.com";
// Formulate the request and handle the response.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
// Do something with the response
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// Handle error
}
});
// Add the request to the RequestQueue.
mRequestQueue.add(stringRequest);
// ...
若只会做一次请求且不会离开线程池,可以在需要的时候才创建RequestQueue,在响应回来或错误发生时(使用在发送简单请求汇总描述的Volley.newRequestQueue()方法)就调用stro()来停止RequestQueue。但更常的使用情况是创建RequestQueue作为单例来保持其生命期跟应用程序一样长,这将在下一节中描述。
若应用程序会持续不断的使用网络,最高效的方式莫过于设置一个RequestQueue的单例以让其跟应用程序保持一样的生命期。可以通过多种方式实现这个目标。推荐的方法是实现一个类来包装RequestQueue和其它的Volley功能。另外一种方式是实现一个Application的子类并在Application.onCreate()中设置RequestQueue。但不鼓励后一种实现方式,一个静态的单例能够以更多模块化的方法提供相同的功能。
一个关键要点是RequestQueue必须被Application上下文而不是Activity上下文初始化。这将确保RequestQueue的生命期会跟应用程序一样,而不是在活动每次被重新创建时RequestQueue也被重新创建(例如,当用户选择设备时)。
以下例子演示提供RequestQueue和ImageLoader功能的单类:
public class MySingleton {
private static MySingleton mInstance;
private RequestQueue mRequestQueue;
private ImageLoader mImageLoader;
private static Context mCtx;
private MySingleton(Context context) {
mCtx = context;
mRequestQueue = getRequestQueue();
mImageLoader = new ImageLoader(mRequestQueue,
new ImageLoader.ImageCache() {
private final LruCache<String, Bitmap>
cache = new LruCache<String, Bitmap>(20);
@Override
public Bitmap getBitmap(String url) {
return cache.get(url);
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
cache.put(url, bitmap);
}
});
}
public static synchronized MySingleton getInstance(Context context) {
if (mInstance == null) {
mInstance = new MySingleton(context);
}
return mInstance;
}
public RequestQueue getRequestQueue() {
if (mRequestQueue == null) {
// getApplicationContext() is key, it keeps you from leaking the
// Activity or BroadcastReceiver if someone passes one in.
mRequestQueue = Volley.newRequestQueue(mCtx.getApplicationContext());
}
return mRequestQueue;
}
public <T> void addToRequestQueue(Request<T> req) {
getRequestQueue().add(req);
}
public ImageLoader getImageLoader() {
return mImageLoader;
}
}
以下是使用单类来执行RequestQueue操作的例子:
// Get a RequestQueue
RequestQueue queue = MySingleton.getInstance(this.getApplicationContext()).
getRequestQueue();
// ...
// Add a request (in this example, called stringRequest) to your RequestQueue.
MySingleton.getInstance(this).addToRequestQueue(stringRequest);
2015.12.08
学习如何使用Vollery其中一种out-of-the-box请求类型(原始字符串、图片以及JSON)来发送请求。
此节描述使用Volley所支持的常用的请求类型:
- StringRequest。特定的一个URL并在响应中接收一个原始的字符串。见Setting Up a Request Queue中的例子。
- ImageRequest。特定的URL且在响应中接收图片。
- JsonObjectRequest和JsonArrayRequest(都是JsonRequest的子类)。特定的URL且在响应中分别获取JSON对象或数组。
如果希望响应是其中一种类型,可能不必自定义实现一个请求。此节描述如何使用这些标准的请求类型。欲见怎么实现自定义请求的更多信息,见 Implementing a Custom Request。
Volley提供以下类来请求图片。这些类在彼此顶层上提供处理图片不同级别的支持:
- ImageRequest - 一个根据给定URL获取图片的封装请求并用编码的位图回调。它同时也提供了像指定尺寸和重新设置尺寸的方便特性。它主要的优势是Volley的线程调度确保耗费较大的图片操作(编码、重设尺寸)在一个工作线程中自动的进行。
- ImageLoader - 根据远程URLs操纵载入和缓存图片的一个帮助类。ImageLoader是大数量ImageRequest的一个配器,如当放置多张缩略图到ListView中。ImageLoader提供了内存缓存来放置到Volley缓存的前面,这用来阻止闪烁。这让无阻碍或延迟主线程的情况下获取缓存操作变成可能,若使用磁盘I/O是不可能实现的。ImageLoader也能够响应合并(每次响应程序都要在视图上设置位图并会引起布局传递图片)。合并让同时传递多个响应成为可能,并且还提升了性能。
- NetworkImageView - 构建于ImageLoader且在通过URL从网络提取图片的情况下能够高效的代替ImageView。如果视图从层中分离,NetworkImageView同时也能管理取消挂起的请求。
以下是使用ImageView的例子。它根据指定的URL来检索图片并将其展示到应用程序中。注意代码片段通过一个类单例来和RequestQueue交互(见 Setting Up a RequestQueue获取更多的讨论话题)。
ImageView mImageView;
String url = "http://i.imgur.com/7spzG.png";
mImageView = (ImageView) findViewById(R.id.myImage);
...
// Retrieves an image specified by the URL, displays it in the UI.
ImageRequest request = new ImageRequest(url,
new Response.Listener<Bitmap>() {
@Override
public void onResponse(Bitmap bitmap) {
mImageView.setImageBitmap(bitmap);
}
}, 0, 0, null,
new Response.ErrorListener() {
public void onErrorResponse(VolleyError error) {
mImageView.setImageResource(R.drawable.image_load_error);
}
});
// Access the RequestQueue through your singleton class.
MySingleton.getInstance(this).addToRequestQueue(request);
可以通过使用ImageLoader和NetworkImageView来高效的管理多张图片的展示(如在ListView中展示多张图片)。在布局XML文件中,使用NetworkImageView跟使用ImageView一样的方式,如下例:
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/networkImageView"
android:layout_width="150dp"
android:layout_height="170dp"
android:layout_centerHorizontal="true" />
可以通过使用ImageLoader来展示一张图片,如下例:
ImageLoader mImageLoader;
ImageView mImageView;
// The URL for the image that is being loaded.
private static final String IMAGE_URL =
"http://developer.android.com/images/training/system-ui.png";
...
mImageView = (ImageView) findViewById(R.id.regularImageView);
// Get the ImageLoader through your singleton class.
mImageLoader = MySingleton.getInstance(this).getImageLoader();
mImageLoader.get(IMAGE_URL, ImageLoader.getImageListener(mImageView,
R.drawable.def_image, R.drawable.err_image));
然而,如果是填充ImageView,NetworkImageView也可以做此项工作。如下例:
ImageLoader mImageLoader;
NetworkImageView mNetworkImageView;
private static final String IMAGE_URL =
"http://developer.android.com/images/training/system-ui.png";
...
// Get the NetworkImageView that will display the image.
mNetworkImageView = (NetworkImageView) findViewById(R.id.networkImageView);
// Get the ImageLoader through your singleton class.
mImageLoader = MySingleton.getInstance(this).getImageLoader();
// Set the URL of the image that should be loaded into this view, and
// specify the ImageLoader that will be used to make the request.
mNetworkImageView.setImageUrl(IMAGE_URL, mImageLoader);
以上代码片段根据Setting Up a RequestQueue中的描述通过类单例来访问RequestQueue和ImageLoader。这种方法确保应用程序创建的类单例能够跟应用程序的生命期一样长。这对ImageLoader(操作载入和缓存图片的帮助类)很重要,因为内存缓存的主要功能是无闪烁旋转。使用单例模式允许位图缓存的生命期长于活动。如果在活动中创建ImageLoader,ImageLoader在用户每次选择设备时都会被重建,这就将会引起闪烁。
Volley工具箱提供了通过DiskBasedCache类实现的标准缓存。此类直接将文件缓存在硬盘的指定目录中。但欲使用ImageLoader,应该提供一个自定义的内存LRU位图缓存来实现ImageLoader.ImageCache接口。您可能想将缓存设置为一个单例,更多关于此话题的信息见 Setting Up a RequestQueue。
以下是实现内存类LruBitmapCache的例子。它扩展于LruCache类并实现了ImageLoader.ImageCache接口:
import android.graphics.Bitmap;
import android.support.v4.util.LruCache;
import android.util.DisplayMetrics;
import com.android.volley.toolbox.ImageLoader.ImageCache;
public class LruBitmapCache extends LruCache<String, Bitmap> implements ImageCache {
public LruBitmapCache(int maxSize) {
super(maxSize);
}
public LruBitmapCache(Context ctx) {
this(getCacheSize(ctx));
}
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
@Override
public Bitmap getBitmap(String url) {
return get(url);
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
put(url, bitmap);
}
// Returns a cache size equal to approximately three screens worth of images.
public static int getCacheSize(Context ctx) {
final DisplayMetrics displayMetrics = ctx.getResources().
getDisplayMetrics();
final int screenWidth = displayMetrics.widthPixels;
final int screenHeight = displayMetrics.heightPixels;
// 4 bytes per pixel
final int screenBytes = screenWidth * screenHeight * 4;
return screenBytes * 3;
}
}
以下是如何实例化ImageLoader来使用此缓存的例子:
RequestQueue mRequestQueue; // assume this exists.
ImageLoader mImageLoader = new ImageLoader(mRequestQueue, new LruBitmapCache(
LruBitmapCache.getCacheSize()));
Volley提供以下类来供JSON请求:
- JsonArrayRequest - 根据给定URL来检索JSONArray响应对象的请求。
- JsonObjectRequest - 根据给定的URL来检索JSONObject对象的请求,允许将JSONObject作为请求对象的一部分的选择。
这两个类都是基于常见的基础类JsonRequest。使用它们的模式跟使用其他类型的请求一样。以下代码片段提取JSON种子并将其作为文本展示在用户界面中:
TextView mTxtDisplay;
ImageView mImageView;
mTxtDisplay = (TextView) findViewById(R.id.txtDisplay);
String url = "http://my-json-feed";
JsonObjectRequest jsObjRequest = new JsonObjectRequest
(Request.Method.GET, url, null, new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
mTxtDisplay.setText("Response: " + response.toString());
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
// TODO Auto-generated method stub
}
});
// Access the RequestQueue through your singleton class.
MySingleton.getInstance(this).addToRequestQueue(jsObjRequest);
根据Gson实现自定义的JSON请求的例子,见下一节“实现自定义请求”。
2015.12.09
学习如何实现自定义请求。
此节描述如何实现自定义请求类型,这些类型可以不用Volley的out-of-the-box支持。
大多数请求的实现在工具箱内且随时可用;如果响应是字符串、图片或JSON,可能就不再需要自定义实现Request。
欲实现自定义请求,需要做以下工作:
- 扩展Requst< T >类,< T >代表解析响应请求的类型。如解析响应是字符串,通过扩展Request< String >来创建自定义请求。见Volley工具箱的StringRequest和ImageRequest类,它们都是扩展的Request< T >。
- 实现parseNetworkResponse()和deliverResponse()方法,后续将详细描述如何实现它们。
Response包装了为传递、给定类型(如字符串,图片或JSON)的解析响应。以下是实现parseNetworkResponse()样例:
@Override
protected Response<T> parseNetworkResponse(
NetworkResponse response) {
try {
String json = new String(response.data,
HttpHeaderParser.parseCharset(response.headers));
return Response.success(gson.fromJson(json, clazz),
HttpHeaderParser.parseCacheHeaders(response));
}
// handle errors
...
}
注意以下几点:
如果使用的协议不是标准的缓存语义,可以自己构建Cache.Entry,但推荐按照以下形式定义请求:
return Response.success(myDecodedObject,
HttpHeaderParser.parseCacheHeaders(response));
Volley在一个工作线程中调用parseNetworkResponse()。这就确保了耗时的解析工作(诸如解码JPEG到位图)就不会阻碍用户界面线程。
Volley在主线程中用parseNetworkResponse()返回的对象调用此函数。大多数请求调用此回调接口,如下示例:
protected void deliverResponse(T response) {
listener.onResponse(response);
Gson是用反射将JSON转换为java对象的库。可以定义跟关联的JSON键相同名字的java对象,传递Gson对象,Gson会填充域。以下是使用Gson来实现Volley请求的解析:
public class GsonRequest<T> extends Request<T> {
private final Gson gson = new Gson();
private final Class<T> clazz;
private final Map<String, String> headers;
private final Listener<T> listener;
/** * Make a GET request and return a parsed object from JSON. * * @param url URL of the request to make * @param clazz Relevant class object, for Gson's reflection * @param headers Map of request headers */
public GsonRequest(String url, Class<T> clazz, Map<String, String> headers,
Listener<T> listener, ErrorListener errorListener) {
super(Method.GET, url, errorListener);
this.clazz = clazz;
this.headers = headers;
this.listener = listener;
}
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
return headers != null ? headers : super.getHeaders();
}
@Override
protected void deliverResponse(T response) {
listener.onResponse(response);
}
@Override
protected Response<T> parseNetworkResponse(NetworkResponse response) {
try {
String json = new String(
response.data,
HttpHeaderParser.parseCharset(response.headers));
return Response.success(
gson.fromJson(json, clazz),
HttpHeaderParser.parseCacheHeaders(response));
} catch (UnsupportedEncodingException e) {
return Response.error(new ParseError(e));
} catch (JsonSyntaxException e) {
return Response.error(new ParseError(e));
}
}
}
若您喜欢可随时调用Volley提供的JsonArrayRequest和JsonArrayObject类。见“使用标准请求类型”获取更多信息。
[2015.11.29 - 11:32]