进行网络操作——翻译自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下进行。


Preferences panel Setting a network preference

下面就是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();
}




















































  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值