Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android開發 >> 中級開發 >> Android 中實現並發性、聯網和數據訪問

Android 中實現並發性、聯網和數據訪問

編輯:中級開發

簡介: Java™ 語言是 Android 開發人員所選的工具。android 運行時使用自己的虛擬機 Dalvik,這並不是多數程序開發人員使用的普通 Java 虛擬機。Dalvik 支持 Java 編程語言的大部分功能 — 但並不是全部。在本文中,您將學習高級 Java 功能及其如何在 android 中實現。這些功能包括並發性、聯網和數據庫訪問。

准備開始

本文介紹幾種處理復雜情況的 Android SDK 工具。開發 Android 應用程序,需要最新版 android SDK,這需要一個 Java Development Kit (JDK)。我使用的是 android 2.2 和 JDK 1.6.0_17(這些工具的鏈接見 參考資料)。物理設備不是必須的;本文中的所有代碼在 SDK 附帶的 Android 模擬器上可以很好的運行。本文假設您對 Android 編程比較熟悉,故不涉及 android 基礎開發,但是如果您有 Java 編程語言的知識,也是可以的。


並發性和聯網

常見縮略詞

  • API:應用程序編程接口
  • SQL:結構化查詢語言
  • SDK:軟件開發工具包
  • UI:用戶界面
  • XML:可擴展標記語言

Android 應用程序一個最常見的任務就是檢索數據或通過網絡將數據發送到遠程服務器。這一操作的結果通常是一些您想要展示給用戶的新數據。這意味著您需要修改用戶界面。大多數開發人員知道您將不會執行一個潛在的長期運行任務,例如,在主 UI 線程上通過網絡訪問數據(特別使用一個網絡連接非常慢的手機)。凍結您的應用程序直至長期運行任務完成。事實上,如果這個任務超過 5 秒,android 操作系統將出現臭名昭著的 Application Not Responding 對話框,如 圖 1 所示。


圖 1. android 臭名昭著的 Application Not Responding 對話框
Android 的 Application Not Responding 對話框屏幕截圖 
 

您不可能知道用戶網絡連接能有多慢。為了避免冒險,您必須在不同的線程上執行任務,或者至少不在主 UI 線程上執行。許多 Android 應用程序,但不是全部,需要處理多線程,由此引起並發。應用程序經常需要本地保存數據,android 數據庫是一個很好的選擇。這三個場景(不同線程,並發和本地保存數據)在 Java 環境中有許多標准方法可以用來處理。然而,正如您將要看到的,android 提供不同的選擇。讓我們逐個看看,看看其優點和缺點。


android 網絡

通過網絡使用 Java 編程進行調用是簡單的,我們熟悉的 Java.Net包含幾個執行此操作的類。這些類大多數在 android 中都可用,事實上,您可以使用像 Java.Net.URL 和 Java.Net.URLConnection 這樣的類,就像您在其他 Java 應用程序中那樣。然而,Android 包括 pache HttpClIEnt 庫,這是在 android 上連接網絡的首選方法。即使您使用常用 Java 類,android 實現仍然使用 HttpClIEnt。清單 1 顯示了一個使用這個必不可少的庫的示例。(所有源代碼見 下載。)


清單 1. 在 android 上使用 Http ClIEnt 庫

				
private ArrayList<Stock> fetchStockData(Stock[] oldStocks) 
    throws ClientProtocolException, IOException{
    StringBuilder sb = new StringBuilder();
    for (Stock stock : oldStocks){
        sb.append(stock.getSymbol());
        sb.append('+');
    }
    sb.deleteCharAt(sb.length() - 1);
    String urlStr = 
        "http://finance.yahoo.com/d/quotes.csv?f=sb2n&s=" + 
                sb.toString();
    HttpClient client = new DefaultHttpClient();
    HttpGet request = new HttpGet(urlStr.toString());
    HttpResponse response = clIEnt.execute(request);
    BufferedReader reader = new BufferedReader(
            new InputStreamReader(response.getEntity().getContent()));
    String line = reader.readLine();
    int i = 0;
    ArrayList<Stock> newStocks = new ArrayList<Stock>(oldStocks.length);
    while (line != null){
        String[] values = line.split(",");
        Stock stock = new Stock(oldStocks[i], oldStocks[i].getId());
        stock.setCurrentPrice(Double.parseDouble(values[1]));
        stock.setName(values[2]);
        newStocks.add(stock);
        line = reader.readLine();
        i++;
    }
    return newStocks;
}

 

在這段代碼中有一組 Stock 對象。這是基本的數據結構對象,保存用戶擁有股票信息(比如,代號、價格等)以及更多的個人信息(比如,用戶付了多少錢)。您可以使用 HttpClIEnt 類從 Yahoo Finance 檢索動態數據(例如,這支股票目前的價格)。HttpClIEnt包含一個 HttpUriRequest ,在本例中,您可以使用 HttpGet,這是HttpUriRequest 的一個子類。類似地,當您需要向遠程服務器發送數據時,可以使用 HttpPost 類,當您從客戶端得到 HttpResponse時,您能接觸到響應的潛在 InputStream、對其進行緩沖、解析來獲取股票信息。

現在,您看到了如何通過網絡檢索數據、如何用這個數據來通過使用多線程智能地更新 android UI。


android 並發性實踐

如果您在應用程序的主 UI 線程上運行 清單 1 中的代碼,可能會出現 Application Not Responding 對話框,具體視用戶網絡速度而定。因此必須確定生成一個線程來獲取數據。清單 2 顯示了一種解決方法。


清單 2. Na&iUML;ve 多線程(別這樣,這行不通!)

				
private void refreshStockData(){
    Runnable task = new Runnable(){
        public void run() {
            try {
                ArrayList<Stock> newStocks = 
                    fetchStockData(stocks.toArray(
                                  new Stock[stocks.size()]));
                for (int i=0;i<stocks.size();i++){
                    Stock s = stocks.get(i);
                    s.setCurrentPrice(
                                  newStocks.get(i).getCurrentPrice());
                    s.setName(newStocks.get(i).getName());
                    refresh();
                }
            } catch (Exception e) {
                Log.e("StockPortfolioVIEwStocks", 
                            "Exception getting stock data", e);
            }
        }
    };
    Thread t = new Thread(task);
    t.start();
}

 

清單 2 的標題聲明這是 na&iUML;ve 代碼,確實是。在這個例子中,您將調用 清單 1 中的 fetchStockData 方法,將其封裝在 Runnable 對象中,並在一個新線程中執行。在這個新線程中,您可以訪問stocks,一個封裝 Activity(此類創建了 UI)的成員變量。顧名思義,這是 Stock 對象的一個數據結構(本例中是Java.util.ArrayList)。換句話說,您在兩個線程之間共享數據,主 UI 線程和衍生(spawned)線程(在 清單 2 中調用)。當您修改了衍生線程中的共享數據時,通過在 Activity 對象上調用refresh 方法來更新 UI。

如果您編寫了 Java Swing 應用程序,您可能需要遵循一個像這樣的模式。然而,這在 android 中將不能正常工作。衍生線程根本不能修改 UI。因此在不凍結 UI ,但另一方面,在數據收到之後又允許您修改 UI 的情況下,您怎樣檢索數據?android.os.Handler 類允許您在線程之間協調和通信。清單 3 顯示了一個使用 Handler 的已更新 refreshStockData 方法。


清單 3. 實際工作的多線程 — 通過使用 Handler 

				
private void refreshStockData(){
    final ArrayList<Stock> localStocks = 
          new ArrayList<Stock>(stocks.size());
    for (Stock stock : stocks){
        localStocks.add(new Stock(stock, stock.getId()));
    }
    final Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            for (int i=0;i<stocks.size();i++){
                stocks.set(i, localStocks.get(i));
            }
            refresh();
        }
    };
    Runnable task = new Runnable(){
        public void run() {
            try {
                ArrayList<Stock> newStocks = 
                    fetchStockData(localStocks.toArray(
                                  new Stock[localStocks.size()]));
                for (int i=0;i<localStocks.size();i++){
                    Stock ns = newStocks.get(i);
                    Stock ls = localStocks.get(i);
                    ls.setName(ns.getName());
                    ls.setCurrentPrice(ns.getCurrentPrice());
                }
                handler.sendEmptyMessage(RESULT_OK);
            } catch (Exception e) {
                Log.e("StockPortfolioVIEwStocks", 
                            "Exception getting stock data", e);
            } 
        }
    };
    Thread dataThread = new Thread(task);
    dataThread.start();
}

 

在 清單 2 和 清單 3 中的代碼有兩個主要的不同。明顯的差異是Handler 的存在。第二個不同是,在衍生線程中,您不能修改 UI。相反的,當您將消息發送到 Handler,然後由 Handler 來修改 UI。也要注意,在線程中您不能修改 stocks 成員變量,正如您之前所做的。相反地您可以修改數據的本地副本。嚴格地來說,這是不是必須的,但這更為安全。

清單 3 說明了在並發編程中一些非常普遍的模式:復制數據、將數據解析到執行長期任務的線程中、將結果數據傳遞回主 UI 線程、以及根據所屬數據更新主 UI 線程。Handlers 是 android 中的主要通信機制,它們使這個模式易於實現。然而,清單 3 中仍然有一些樣本代碼。幸好,android 提供方法來封裝和消除大多數樣本代碼。清單 4 演示了這一過程。


清單 4. 用一個 AsyncTask 使多線程更容易

				
private void refreshStockData() {
    new AsyncTask<Stock, Void, ArrayList<Stock>>(){
        @Override
        protected void onPostExecute(ArrayList<Stock> result) {
            ViewStocks.this.stocks = result;
            refresh();
        }

        @Override
        protected ArrayList<Stock> doInBackground(Stock... stocks){
            try {
                return fetchStockData(stocks);
            } catch (Exception e) {
                Log.e("StockPortfolioVIEwStocks", "Exception getting stock data", e);
            }
            return null;
        }
    }.execute(stocks.toArray(new Stock[stocks.size()]));
}

 

如您所見,清單 4 比起 清單 3 樣本代碼明顯減少。您不能創建任何線程或 Handlers。使用 AsyncTask 來封裝所有樣本代碼。要創建AsyncTask,您必須實現 doInBackground 方法。該方法總是在獨立的線程中執行,因此您可以自由調用長期運行任務。它的輸入類型來自您所創建的 AsyncTask 的類型參數。在本例中,第一個類型參數是 Stock,因此 doInBackground 獲得傳遞給它的一組 Stock 對象。類似地,它返回一個 ArrayList<Stock>,因為這是 AsyncTask的第三個類型參數。在此例中,我也選擇重寫 onPostExecute 方法。這是一個可選方法,如果您需要使用從 doInBackground 返回的數據來進行一些操作,您可以選用這種方法來實現。這個方法總是在主 UI 線程上被執行,因此對於修改 UI 這是一個很好的選擇。

有了 AsyncTask,您就完全可以簡化多線程代碼。它可以將許多並發陷阱從您的開發路徑刪除,您仍然可以使用 AsyncTask 尋找一些潛在問題,例如,在 doInBackground 方法對象執行的同時設備上的方向發生改變時可能發生什麼。更多關於如何處理這類案例的技術,見參考資料 的鏈接。

現在我們開始討論另一個常見任務,其中 android 明顯背離常用的Java 方法 — 使用數據庫進行處理。


android 數據庫連通性

Android 中一個非常有用的特征就是存在本地關系數據庫。保證您能在本地文件中存儲您的數據,但通常更有用的是使用一個關系型數據庫管理系統(Relational Database Management System,RDBMS)來存儲。Android 提供給您常用的 SQLite 數據庫來進行處理,因為對於像 Android 這類嵌入式系統它是高度優化的。它被 android 上的核心應用程序所用。例如,用戶地址簿是存儲在一個 SQLite 數據庫中。現在,對於給定的 android 的 Java 實現,您可以使用 JDBC 來訪問這些數據庫。出人意料的是,android 甚至包括構成主要部分 JDBC API 的 Java.sql 和 Javax.sql 包。然而,當涉及使用本地 android 數據庫進行處理時,這毫無用處。相反地,您想要使用android.database 和 android.database.sqlite 包。清單 5 是一個使用這些類存儲和檢索數據的示例。


清單 5. 使用 android 進行數據庫訪問 

				
public class StocksDb {
    private static final String DB_NAME = "stocks.db";
    private static final int DB_VERSION = 1;
    private static final String TABLE_NAME = "stock";
    private static final String CREATE_TABLE = "CREATE TABLE " + 
        TABLE_NAME + " (id INTEGER PRIMARY KEY, symbol TEXT, max_price DECIMAL(8,2), " +
            "min_price DECIMAL(8,2), price_paid DECIMAL(8,2), " +
            "quantity INTEGER)";
    private static final String INSERT_SQL = "INSERT INTO " + TABLE_NAME +
            " (symbol, max_price, min_price, price_paid, quantity) " +
            "VALUES (?,?,?,?,?)";
    private static final String READ_SQL = "SELECT id, symbol, max_price, " +
            "min_price, price_paid, quantity FROM " + TABLE_NAME;
    private final Context context;
    private final SQLiteOpenHelper helper;
    private final SQLiteStatement stmt;
    private final SQLiteDatabase db;
    public StocksDb(Context context){
        this.context = context;
        helper = new SQLiteOpenHelper(context, DB_NAME, null, 
                DB_VERSION){
            @Override
            public void onCreate(SQLiteDatabase db) {
                db.execSQL(CREATE_TABLE);
            }

            @Override
            public void onUpgrade(SQLiteDatabase db, int oldVersion, 
                    int newVersion) {
                throw new UnsupportedOperationException();
            }
        };
        db = helper.getWritableDatabase();
        stmt = db.compileStatement(INSERT_SQL);
    }
    public Stock addStock(Stock stock){
        stmt.bindString(1, stock.getSymbol());
        stmt.bindDouble(2, stock.getMaxPrice());
        stmt.bindDouble(3, stock.getMinPrice());
        stmt.bindDouble(4, stock.getPricePaid());
        stmt.bindLong(5, stock.getQuantity());
        int id = (int) stmt.executeInsert();
        return new Stock (stock, id);
    }
    public ArrayList<Stock> getStocks() {
        Cursor results = db.rawQuery(READ_SQL, null);
        ArrayList<Stock> stocks = 
                 new ArrayList<Stock>(results.getCount());
        if (results.moveToFirst()){
            int idCol = results.getColumnIndex("id");
            int symbolCol = results.getColumnIndex("symbol");
            int maxCol = results.getColumnIndex("max_price");
            int minCol = results.getColumnIndex("min_price");
            int priceCol = results.getColumnIndex("price_paid");
            int quanitytCol = results.getColumnIndex("quantity");
            do {
                Stock stock = new Stock(results.getString(symbolCol), 
                        results.getDouble(priceCol), 
                        results.getInt(quanitytCol), 
                                    results.getInt(idCol));
                stock.setMaxPrice(results.getDouble(maxCol));
                stock.setMinPrice(results.getDouble(minCol));
                stocks.add(stock);
            } while (results.moveToNext());
        }
        if (!results.isClosed()){
            results.close();
        }
        return stocks;
    }
    public void close(){
        helper.close();
    }    
}

 

清單 5 中的類完全封裝了一個用於存儲股票信息的 SQLite 數據庫。因為您將要使用一個嵌入式數據庫,不僅是您的應用程序要使用它,而且也要通過應用程序來創建它。您需要提供代碼來創建該數據庫。android 提供一個有用的抽象幫助類 SQLiteOpenHelper。要完成這一操作,您需要擴展這個抽象類並提供代碼通過使用 onCreate方法創建您的數據庫。當您有一個幫助程序實例時,就可以獲取一個SQLiteDatabase 實例,您可以用來執行任意 SQL 語句。

您的數據庫類有兩個較為方便的方法。第一個是 addStock,用於將新股票保存到數據庫中。注意,您使用了一個 SQLiteStatement 實例,這類似於一個 Java.sql.PreparedStatement。需要注意的是,在您的類構造器中如何對其進行編譯,使其在每次調用 addStock 時都能重復利用。在每個 addStock 調用中,SQLiteStatement 的變量(INSERT_SQL 字符串中的問號)必然要將數據傳遞給 addStock。再一次強調,這類似於 PreparedStatement ,您可以從 JDBC 了解它。

另一個方法是 getStocks。顧名思義,它從數據庫中檢索所有股票。注意,您再次使用一個 SQL 字符串,正如您在 JDBC 中所用的那樣。您可以在 SQLiteDatabase 類上通過使用 rawQuery 方法來進行處理。這個類也有幾個查詢方法,讓您可以不使用 SQL 直接查詢數據庫。所有這些方法都返回一個 Cursor 對象,和Java.sql.ResultSet 非常相似。您可以將 Cursor 移動到從數據庫中返回的數據所在行,在每一行,您可以使用 getIntgetString和其他的方法來檢索您要查詢的數據庫中各列相關的值。再一次強調,這和 ResultSet 十分相似。也和 ResultSet 比較相似,當您完成操作之後,關閉 Cursor 也十分重要的。如果您沒有關閉Cursors,那麼可能會迅速地耗盡內存並導致您的應用程序崩潰。

查詢本地數據庫是一個比較慢的過程,特別是,如果您有多行數據或者您需要在多個表之間運行復雜的查詢語句。然而,數據庫查詢或插入超過 5 秒且出現一個 Application Not Responding 對話框,這種情況不太可能發生,但是當您的數據庫忙於讀取和寫入數據時,凍結您的 UI 是不明智的。當然,避免這種情況最好的辦法是使用AsyncTask清單 6 展示了這個示例。


清單 6. 在一個單獨的線程上插入數據庫

				
Button button = (Button) findViewById(R.id.btn);
button.setOnClickListener(new OnClickListener(){
    public void onClick(VIEw v) {
        String symbol = symbolIn.getText().toString();
        symbolIn.setText("");
        double max = Double.parseDouble(maxIn.getText().toString());
        maxIn.setText("");
        double min = Double.parseDouble(minIn.getText().toString());
        minIn.setText("");
        double pricePaid = 
                Double.parseDouble(priceIn.getText().toString());
        priceIn.setText("");
        int quantity = Integer.parseInt(quantIn.getText().toString());
        quantIn.setText("");
        Stock stock = new Stock(symbol, pricePaid, quantity);
        stock.setMaxPrice(max);
        stock.setMinPrice(min);
        new AsyncTask<Stock,Void,Stock>(){
            @Override
            protected Stock doInBackground(Stock... newStocks) {
                // There can be only one!
                return db.addStock(newStocks[0]);
            }
            @Override
            protected void onPostExecute(Stock s){
                addStockAndRefresh(s);
            }
        }.execute(stock);
    }
});

 

您可以先為按鈕創建一個實踐監聽器。當用戶點擊按鈕時,您可以從各個小部件(確切地說是 EditText 小部件)讀取股票數據並填入一個新的 Stock 對象。您可以創建一個 AsyncTask,並通過doInBackground 方法從 清單 5 中調用 addStock 方法。如此,addStock 將在一個背景線程上執行,而不是在主 UI 線程上。完成之後,將新 Stock 對象從數據庫傳遞到在主 UI 線程上執行的addStockAndRefresh 方法。


結束語

本文顯示了,即使 Android 在 Java 環境下僅支持眾多 API 的一個子集,但是其功能卻一點都不遜色。在某些示例中,比如網絡,它完全實現了熟悉的 API,也提供了一些更為便捷的方法。在其他的示例中,比如並發性,Android 添加了額外 API 以及一些必須遵循的慣例。最後,在數據庫訪問案例中,android 提供了完全不同的方法來訪問數據庫,但使用的是熟悉的概念。這不僅僅是標准 Java 和 android Java 技術之間主觀上的差異:它們形成了android 開發的基礎構建元件。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved