| Java 語言是 Android 開發人員所選的工具。Android 運行時使用自己的虛擬機器 Dalvik,這並不是多數程式開發人員使用的普通 JAVA 虛擬機器。Dalvik 支援 Java 程式設計語言的大部分功能——但並不是全部。eBay軟體架構師Michael Galpin寫了一篇文章《Gourmet Java technology for Android applications》,通過本文,您將學習進階 Java 功能及其如何在 Android 中實現。這些功能包括並發性、連網和資料庫訪問。IBM developerWorks上發布了本文的中文版,現轉載於此。全文如下: 準備開始 本文介紹幾種處理複雜情況的 Android SDK 工具。開發 Android 應用程式,需要最新版 Android SDK,這需要一個 Java Development Kit (JDK)。我使用的是 Android 2.2 和 JDK 1.6.0_17。物理裝置不是必須的;本文中的所有代碼在 SDK 附帶的 Android 模擬器上可以很好的運行。本文假設您對 Android 編程比較熟悉,故不涉及 Android 基礎開發,但是如果您有 Java 程式設計語言的知識,也是可以的。 並發性和連網 Android 應用程式一個最常見的任務就是檢索資料或通過網路將資料發送到遠程伺服器。這一操作的結果通常是一些您想要展示給使用者的新資料。這意味著您需要修改使用者介面。大多數開發人員知道您將不會執行一個潛在的長期運行任務,例如,在主 UI 線程上通過網路訪問資料(特別使用一個網路連接非常慢的手機)。凍結您的應用程式直至長期運行任務完成。事實上,如果這個任務超過5 秒,Android 作業系統將出現臭名昭著的 Application Not Responding 對話方塊,1所示。 圖 1. 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> newnewStocks = new ArrayList<Stock>(oldStocks.length); while (line != null){ String[] values = line.split(","); Stock stock = new Stock(oldStocks, oldStocks.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ï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ï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 移動到從資料庫中返回的資料所在行,在每一行,您可以使用 getInt、getString 和其他的方法來檢索您要查詢的資料庫中各列相關的值。再一次強調,這和 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 開發的基礎構建元件。 作者簡介: Michael Galpin Michael Galpin Michael Galpin 是 eBay 的一名架構師。他經常為 developerWorks 撰寫文章,同時在 TheServerSide.com 和 Java Developer 期刊上發表文章,他還有自己的 部落格。他從 1998 年開始做職業程式員,並擁有加州理工學院數學專業的學士學位。 原文連結:http://www.ibm.com/developerworks/cn/xml/x-gourmetandroid/#f1 |