进行网络操作——翻译自developer.android.com Building Apps with Connectivity& the Cloud

这节课里面包含了连接网络的基本任务,监视网络连接,包括网络变化,并且让用户可以控制网络的使用。同样表述了如何解析和使用xml数据。

这节课中包含一个展示怎样典型的网络操作的示例应用。

经过这节课的学习,你将会拥有使得android app可以高效地网络下载以及解析数据的能力,与此同时使用最少的网络流量。

提示:要查看Volly请参照 课程Transmitting Network Data Using Volley一节,这是一个使得android app的网络连接更加简单和快速的http库。volley可以在open 的AOSP仓库中获取。Volley可以让你对于网络操作的组织更加合理并提高你的app网络操作的性能。

课程列表:

连接到网络

学习怎样连接网络,选择hTTP客户端,以及在非UI线程上面进行网络操作。

管理网络使用

学习怎样检查设备的网络连接,创建可以控制网络的UI,响应网络中的一些变化。

解析XML数据

学习怎样解析和使用xml数据。

连接到网络

这节课教你创建一个简单的使用网络的app。及时你在app中使用最简单的网络连接,也最好使用这个例子中展示的实践。

要进行网络连接,你需要在manifest中添加下面的权限。

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

选择一个HTTP客户端

大多数Adrnodi应用使用HTTP来在网络连接中接受和发送数据。android平台中包含HttpURLConnection客户端,它支持HTTPs,流上传和下载,设置超时时间,IPv6,以及连接池。

检查网络连接

当你的app使用网络连接之前,他应该检查网络连接是否可用。使用getActiveNetWorkInfo()方法,以及isConnceted()方法。注意,设备可能不在网络的范围之内,以及用户可能禁用看wifi连接以及移动数据。更多的讨论参见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
    }
    ...
}

在其他线程中进行网络操作

网络操作可能引起不可预期的延迟。为了防止这样的事情影响用户交互,所有网络的操作要在非UI线程当中。AsyncTask类提供了一种最简单个的分离出UI线程的方法。更多的这个话题的讨论请参见博客 Muitithreading  For Perfoermance。

在下面的代码片当中,myClickahandler方法触发了一个new DownloasWebpageTask().execute(stringUrl)。其中的DownloadWebpageTask类是AsyncTask的子类。DownloadWebpageTask类重写了下面的方法。

- doInBackgound()中执行了方法downloadUri(). 它会把网页uri作为参数传递。方法 downloadUrl() 获取并运行网页的内容。当结束的时候,就返回一个结果字符串。

- onPostExecute()获取返回字符串并在UI中进行显示。

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()方法,app会传递指定的URL给AsyncTask的子类 DownloadWebpageTask。

2.AsyncTask中的doInBackgound方法调用downloadUrl()方法。

3.downloadUri方法接受一个URL字符串作为参数并使用它创建了一个URL对象。

4.URL对象被用来建立一个HttpURLConnection。

5.一旦连接建立,HttpURLConncetion对象使用InputStream获取网页内容。

6.InputStream被传入到readIt()方法中,它把流转换成字符处。

7.最后,AsycTask的onPostExecute()方法吧字符串显示到UI。

连接和下载数据

在你进行网络交互的线程里面,你可以实用HttpURIConnection GET操作来获取你的数据。当你调用connect以后,你通过使用getInputSteam()方法来获取一个读取数据的InputStream。

在下面的代码片当中,doInbackgound方法电泳了downloadURL()方法。这个方法获取传入的URL并使用它,通过HttpURLConncetion来连接网络。一旦连接被建立,app就可以使用getInputStream获取InputStream来获取数据。

// Given a URL, establishes an HttpUrlConnection and retrieves
// the web page content as a InputStream, which it returns as
// a string.使用给的URL建立一个HttpUrlConnection并使用可以返回字符串的InputStream获取网页的内容。
private String downloadUrl(String myurl) throws IOException {
    InputStream is = null;
    // Only display the first 500 characters of the retrieved  只显示获取的网页中的前500个字符。
    // 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  保证在app用完以后关闭InputStream。
    // finished using it.
    } finally {
        if (is != null) {
            is.close();
        }
    }
}

注意到getResourse方法返回连接的状态码。这对于获取连接的附加的信息十分有用。状态码200表示连接成功。

把输入流转化成一个字符串

一个InputStream是以字节为单位的可读资源。一旦你获取到了InputStream,通常你需要转码或者转化到目标数据类型。例如,如果你正在下载图片数据,你可能会如这样来解码和显示。

InputStream is = null;
...
Bitmap bitmap = BitmapFactory.decodeStream(is);
ImageView imageView = (ImageView) findViewById(R.id.image_view);
imageView.setImageBitmap(bitmap);

在上面的例子里面,InputStream承载了网页的文本。下面是怎样把InputStream转化成字符串,来让activity可以在UI上面显示的。

// 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);
}

管理网络使用

这节课中教你怎样写一个对于网络具有细粒度管理的app。如果你的app要进行很多的网络操作,你需要给用户提供让他们自己控制数据习惯的方法,,比如你的应用多久同步一次数据,是否仅在wifi环境下进行加载和,在数据漫游的时候是否使用数据等等。给用户提供这些细节的设置,可以让在进行后台数据限定的时候不太可能限制你的app,因为他们可以精细的定制你的应用使用多少流量。

关于怎样让应用在下载和网络连接的时候使用最少的电量,请参见Optimizing Battery Life 和 Transferring Data Without Draining the Battery。

检查设备的网络连接

设备可以具有各种各样的网络连接。这节课关注使用wifi或者移动流量。想看完全的网络列表,请查看ConnectivityManager。

wifi是一种典型的快速网络。同时,移动数据都是计量的,昂贵的。一个对于app获取大数据的策略就是只有在wiif可用的时候才会开始。

在你进行网络操作之前,检查当前的网络连接状态是一个很好的习惯。此外,这可以防止你的app错误的使用了网络的类型。如果你个网络连接不可用,app应该优雅地给予响应。检查网络连接,你通常使用如下的类:

- ConnectivityManager: 响应关于网络连接的查询。它也可以在网络连接发生变化的时候提醒app。

- NetworkInfo: 描述给出的网络(当前是wifi还是流量)状态。

下面的代码片检测当前的网络的类型是数据流量还是wifi。他决定了这些网络接口是否可用(也就是网络连接是不是可能的)同时(或者  或)可连接(也就是网络连接是否存在并可以建立socket来传递数据)。

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方法,因为isConnected可以处理移动网络、飞行模式

以及限制后台数据的情况。

一个更加简明的检查网络是否可用的方法如下。方法getActiveNetworkInfo()方法返回一个NetworInfo实例,代表了接口第一个发现的连接着的网络,或者在没有任何网络连接的情况下放回null(意味着互联网不可用)。

public boolean isOnline() {
    ConnectivityManager connMgr = (ConnectivityManager)
            getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo = connMgr.getActiveNetworkInfo();
    return (networkInfo != null && networkInfo.isConnected());
}  

更细粒度的获取网络状态可以使用NetworkInfo.DetailedState,但是一般都不是很必要。

管理网络使用

你可以构建一个偏好设置的activity让用户可以显式的设置app对于网络资源的使用。例如:

- 你可以让用户只可以在wifi连接下进行视频上传。

- 你可以设定同步或者不同步,在特定的网络连接以及时间区间,等等。

要写一个支持网络访问以及可以管理网络的app,你需要在manifest里面设置正确的权限以及intent过滤器。

- 下面引用的 manifest包含下面几个授权:

* android.permission.INTERNTE——允许应用打开网络socket。

* androdi.permssion.ACCESS_NETWORK_STATE_允许用户获取网络的信息。

- 你可以声明一个intent filter 包含action ACTION_MANAGE_NETWORK_USAGE(在android4.0引入)来告诉系统在你的应用中有一个可以控制数据使用的activity。ACTION_MANAGE_NETWORK_USAGE显示一个特定的app的网络数据使用管理设置。如果你的app中有一个让用户控制网络使用的activity,你需要在你的intent filter中声明。在例子app中,这样的一个intent对应的是类SettingActivity,它显示一个设置ui,让用户决定下载源。

构建一个设置app

如你在上面看到的 manifest的引用,示例app有一个intent fileter,对应 ACTION_MANAGE_NETWORK_USAGE action。SettingsActivity是PreferenceActivity的子类。它显示定制屏幕来让用户指定下面的几项:

* 是否显示每一个xml 填充实体的照耀,或者及你进显示每一个实体的连接。

* 是否在任何连接可用的情况下下载xml填充还是只在wifi下进行。

 

下面就是SettingsActivity。要注意其中构建了一个OnSharedPreferenceChangeListener。当用户改变了一个设置,就会触发onSharedPreferenceChanged(), 它会把refreshDisplay设置为true。这会在用户返回main activity的时候刷新屏幕。

public class SettingsActivity extends PreferenceActivity implements OnSharedPreferenceChangeListener {
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
       
        // Loads the XML preferences file加载xml preference 文件
        addPreferencesFromResource(R.xml.preferences);
    }
 
    @Override
    protected void onResume() {
        super.onResume();

        // Registers a listener whenever a key changes       当按键变化时候,注册一个listener      
        getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
    }
 
    @Override
    protected void onPause() {
        super.onPause();

       // Unregisters the listener set in onResume().注销在onResume()中注册的listener。
       // It‘s best practice to unregister listeners when your app isn‘t using them to cut down on 在你的app不使用的时候注销listener是很好的习惯,可以减少不必要的系统开销。
       // unnecessary system overhead. You do this in onPause().            //在onPause()中进行这样的操作。
       getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);    
    }
 
    // When the user changes the preferences selection, 当用户改变了偏好选择,onSharedPreferenceChanged会用一个新栈来重启mainActivity。把refreshDispaly标志位设置为true来指示main activity应该更新它的显示。 main activity会查询 PreferenceManager来获取最新的设置。
    // 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//设置resfereshDisplay为true这样当用户返回main activity的时候,就会更新显示最新的内容。
        // activity, the display refreshes to reflect the new settings.
        NetworkActivity.refreshDisplay = true;
    }
}

响应Preference 变化

当用户在设置屏幕里面改变preference,通常都会对app的行为有影响。在下面的代码片里面,app在onStart()里面检查preference 设置。如果设置和网络连接之间存在比配,那么ap就会下载填充,并更新显示(比如设置到wifi并且现在也恰好是wifi连接)。

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.是否有wifi连接
    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.跟踪网络连接变化的BroadcastReceiver。
    private NetworkReceiver receiver = new NetworkReceiver();
   
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
       
        // Registers BroadcastReceiver to track network connection changes.//注册监听网络连接的BroadCastReceiver
        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.当app销毁的时候注销BroadcastReceiver。
        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获取preference的字符串值,后一个参数是如果这个值没有找到的话的默认值。
        // 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//检查网络设置,并分别设置wifiConnected和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.//使用AsyncTask的子类来从stackoverflow上下载xml feed。
    public void loadPage() {
        if (((sPref.equals(ANY)) && (wifiConnected || mobileConnected))
                || ((sPref.equals(WIFI)) && (wifiConnected))) {
            // AsyncTask subclass  同步子类
            new DownloadXmlTask().execute(URL);
        } else {
            showErrorPage();
        }
    }
...
   
}

探测网络连接变化

这个谜题的最后一块就是BroadcastReceiver的子类NetworkReceiver。当设备的网络连接发生变化的时候,NetworkReceiver会拦截action CONNECTIVITY_ACTION,决定网络连接是什么状态,并且将标志位wifiConnected 和mobileConnected设置为对应的true或者false。要点是下一次用户返回这个app的时候,如果NetwordActivity.refresh设置为true,app会只下载最新的fedd并更新显示。

设立一个接受不必要调用的BroadcastReceiver是对系统资源的浪费。例子的app在onCreade中注册了BroadcastReceiver  NetwordReceiver,并且在onCreate里面进行注销。这比在manifest中声明<receiver>标签更加轻量级。当你在<receiver>标签中声明的时候,可以随时唤醒你的 app,尽管你的app已经几个星期都没有运行过了。通过在main Activity中注册和注销BroadcastReceiver,可以确保当用户离开app的时候app不会被唤醒。如果你在manifest中声明了<receiver>,并且你清楚自己什么时候需要使用,你可以使用setCompanentEnableSetting()来启用或者停用。

下面就是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//检查用户偏好和网络连接,根据结果决定是否刷新屏幕或者保留现在的显示。如果用户选择是仅wifi,那么检查设备时候有网络连接。
    // 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//如果设备有wifi连接,设置refreshDisplay为true。这使得用户返回主界面的时候进行界面刷新。
        // 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//如果设置是任意网络(按照排除结果应该是移动数据)把更新标志设置为true。
    // (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//除此之外,app不能下载内容,因为没有网络连接(移动数据或者wifi),或者因为用户的设置是wifi,而现在没有网络连接。设置refreeshDisplay为false。
    // 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是网络连接app常用的一种任务。这节课解释怎样解析xml文档并使用其中的数据。

选择一个解析器

我们推荐XmlPullParser,它是android上高效的可维护的解析xml的方法。历史上android对于这个接口有两种实现。

* KXmlParser 通过 XmlPullParserFactory.newPullParser().

* ExpatPullParser, 通过 Xml.newPullParser().

使用哪一种都是可以的,例子中使用的是ExpatPullParser,通过Xml.newPullParser()。

解析填充内容

解析一个填充的第一步就是决定哪一个阈是你感兴趣的。解析器就会提取这些领域的数据,并忽略其他的部分。

下面是一个我们例子中的xml填充的摘要。每一个对于StackOverlow.com的pos都出现在填充当中,作为一个包含了内部多个标签的实体标签。

<?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>

示例app从entry标签中提取数据,以及他的标签标题,链接,和摘要。

实例化解析器

下一个步骤就是实例化一个解析器并开始解析进程。在这个代码片里面,实例化一个parser,不去执行namespace,并使用提供的InputStream作为他的输入。它将随着nextTag()的调用来开始解析进程,并触发readFeed()方法,它提取并处理app感兴趣的数据。

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()方法没有真的做处理数据填充(源)的工作。它寻找  entry 标签作为递归处理填充的起始点。如果一个标签不是 entry标签,就会跳过它。一旦整个填充都被递归的处理了,readFeed()就会返回包含从填充中提取的数据的List(包含内部数据)。接着parser就会返回这个List。

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

解析xml的步骤如下:

1.如同Analyze the Feed中描述的那样,定义你想要包含在你app中 的tag。整个例子从entry标签,以及其子标签title,link, summary中提取数据。

2.创建下面的方法:

* 一个read方法来读取你感兴趣的标签,比如说readEntry(),readTitle()等等。解析器冲输入流当中读取标签。当他们遇到对应的标签名字,比如说entry, title,link或者summary的时候,就会调用对应的标签的方法。或者跳过这个标签。

*从不同的标签中读取数据,以及前进到下一个标签中的方法。例如:

- 对于title和summary标签,解析器调用readText()方法。这个方法通过代用parser.getText()方法来为这些标签提取数据。

- 对于link标签, 首先提取links的标签,来判断这个link是不是我们感兴趣的类型。接着使用parser.getAttributeValue()来提取link的值。

- 对于entry标签, parser调用readEntry()。这个方法解析entry的内部标签并返回拥有数据成员title, link, summary的Entry对象。

* 一个帮助者skip()方法用来递归。更过的关于这个话题的讨论,请参见Skip Tags You Don’t care about。

下面的代码片展示了解析器怎样解析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.//解析entry的内容。如果遇到了titile summary或者link标签,就把他们交给对应的read方法来处理,或者跳过这个标签。
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.//解析填充源中的title标签
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.// 处理填充源当中的link标签。
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.//处理其中的summary标签
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.对于title和summary标签获取其中的文本。
private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {
    String result = "";
    if (parser.next() == XmlPullParser.TEXT) {
        result = parser.getText();
        parser.nextTag();
    }
    return result;
}
  ...
}

跳过你不关心的标签

按照上面的解析方法,还有一个步骤就是跳过你不感兴趣的标签。下面是parser的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_TAG之间的事件都要包含进去。

* 为了确保他在正确的END_TAG标签上停住而不是原始的START_TAG之后的第一个标签,要保持跟踪内部的层级。

这样的话,如果当前的元素有内部的 元素,那么depth的值直到所有 的原始START_TAG和END_TAG之间的标签都别消费了以后才能为0。例如我们看看parser是怎样跳过<author>元素的,它内部有两个元素<name>和<uri>。

*第一次进入while循环以后,在<author>以后的下一个标签是<name>的START_TAG,depth的值增长到了2.

* 第二次进入while循环的时候,parser遇到的第二个标签是</name> END_TAG。depth的值减小到了1.

*第三次进入while循环的时候,遇到的是<uri>的START_TAG。depth的值增长到了2.

*第四次进入while循环的时候,遇到的是</uri>的END_TAG。depth的值降低到了1.

*第五次进入while循环的时候,遇到的时候</author>的END_TAG。depth的值降低到了0,表明了<author>标签被成功的跳过了。

消费XML数据

案例中的应用使用了AsyncTask来获取和解析XML填充。这使得处理可以脱离主线程。当处理完成的时候,就会在main Activity中更新UI(NetworkActivity)。

在下面 的引用中,loadPage()方法做了下面的事情:

*使用xml填充源的URL初始化一个string变量。

* 如果用户设置那个和网络连接允许,就会触发一个new DownloadXmlTask().execute(url)。这个语句实例化了一个DownloadXmlTask对象,(AsyncTask子类)并调用了他的execute方法,他会下载并解析填充的内容源。并在UI上显示。

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,实现了下面的方法:

* doInBackgound()执行了方法loadXmlFromNetwork()。它将URL作为一个参数传入。loadXmlFromNetwork()方法获取并处理填充源。当他完成的时候,就返回结果的字符串。

* onPostExecute()获取返回的字符串并在ui上显示。

// 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.实例化一个StatckOverflowXmlParser。同样创建 了一个List变量盛装Entry对象,(entries),title, url 以及summary,盛装从xml填充元的这些域中获取出来的值。

2.调用downloadUrl()方法,获取这个feed填充元并作为一个InputStream返回。

3.使用stackOverflowXmlParser来解析InputStream。StackOverflowXmlParser弹出包含从填充源中数据解析出来的实体的List。

4.处理这个List实体,把HtML标记和填充元数据项结合。

5.通过AsyncTask的onPostExcute()方法返回要在mainActivity UI显示的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();
}
时间: 2024-10-06 00:42:55

进行网络操作——翻译自developer.android.com Building Apps with Connectivity& the Cloud的相关文章

Android开发训练之第五章——Building Apps with Connectivity &amp; the Cloud

Building Apps with Connectivity & the Cloud These classes teach you how to connect your app to the world beyond the user's device. You'll learn how to connect to other devices in the area, connect to the Internet, backup and sync your app's data, and

NFC Basics(基本NFC)——翻译自developer.android.com

NFC Basics 关于收发NDEF格式的消息,以及相关的api.非NDEFdata以及其他更深入技术请参见Advanced NFC. 在android端使用NDEF有两种情况: - 从标签读取NDEF数据 - 从另个android手机中使用androidBeam来获取信息. android使用标签派发系统来处理读取NDEF数据的情况,通过分析发现的标签,给数据分类,然后启动一个关注这个分类数据的app.应用程序可以通过注册Intent过滤器(Intent Filter)来获取关注的标签信息.

App组件之服务Service——翻译自developer.android.com

服务Services Service是一种应用组件,它可以在后台长时间地运行并且没有用户界面.其他的应用组件可以启动一个service,并且这个service会一直在后台运行下去,不论用户是否切换到了其他的应用.另外,其他的组件可以绑定一个service来进行交互,甚至进行跨进程通信(IPC).例如服务可以处理网络传输,播放音乐,处理文件IO,或者和content provider进行交互,这些都是在后台进行. 服务有下面两种基本形式: Started方式: 当一个组件(比如一个activity

内容提供者基础 Content Provider Basics——翻译自developer.android.com

#内容提供者基础 Content Provider Basics content provicer 管理着中心数据仓库的访问.一个provider是Android的应用的一部分,它可以提供数据工作的UI. 然而,content provider基本都是被其他的应用访问,使用一个provider客户端对象来访问provider.provider和provider client一同创立了一个持续的标准的数据接口,它可以提供进程内通信以及安全数据访问. 这个主题讲解下面内容的基础部分: - conte

Android Interface Definition Language (AIDL)——翻译自developer.android.com

Android 接口定义语言(AIDL) AIDL类似你可能接触过的其他的IDL.它可以让你定义编程接口,使得客户端和服务端可以达成一致,从而使用IPC(interprocess communication)相互通信. 在Android里面,一个进程通常不能访问其他进程的内存.所以说,他们需要将对象分解为操作系统可以理解的基本的部分,从而使得这些对象为你突破界限.然而突破界限的代码太繁杂,所以Android使用AIDL替你打理. 提示:只有当你想要不同app的客户端都可以对你的service I

Layout布局——翻译自developer.android.com

布局 布局定义了可见的ui结构,比如activity的UI或者app组件.你可以通过下面两种方法来生命布局. -         在xml文件中声明ui.Android提供了一套直接的xml词汇,对应view的类和子类.比如说组件和布局. -         在运行时构建布局元素.你的可以通过编程来在运行时创建布局对象,或者改变他们的属性. Android框架允许你使用一种或者集中方法来创建ui.例如你可以用xml声明app的默认的布局,包括在其中显示的元素和他们的属性.接下来你可以同哦过代码来

九、Android学习第八天——广播机制与WIFI网络操作(转)

(转自:http://wenku.baidu.com/view/af39b3164431b90d6c85c72f.html) 九.Android学习第八天——广播机制与WIFI网络操作 今天熟悉了Android中的广播机制与WIFI网络的一些基本操作,总结如下: Android的广播机制 我们知道广播机制中,发送方不会关心接收方时候接收到数据或者如何去处理数据. 这里总结下Android中BroadcastReceiver的注册方法: (一)在应用程序中进行注册 (二)在Manifest.xml

android中网络操作使用总结(http)

Android是作为智能手机的操作系统,我们开发的应用,大多数也都需要连接网络,通过网络发送数据.获取数据,因此作为一个应用开发者必须熟悉怎么进行网络访问与连接.通常android中进行网络连接一般是使用scoket或者http,http是最多的情况,这里,我来总结下,怎么进行http网络访问操作. android是采用java语言进行开发的,android的包中包含java的URLConnection和apache 的httpclient,因此我们可以使用这两个工具进行网络连接和操作.同时,为

android网络操作使用汇总(http)

Android是作为智能手机的操作系统,我们开发的应用,大多数也都须要连接网络,通过网络发送数据.获取数据,因此作为一个应用开发人员必须熟悉怎么进行网络訪问与连接. 通常android中进行网络连接通常是使用scoket或者http,http是最多的情况.这里,我来总结下.怎么进行http网络訪问操作. android是採用java语言进行开发的,android的包中包括java的URLConnection和apache 的httpclient,因此我们能够使用这两个工具进行网络连接和操作.同一