享受Android應(yīng)用程序的Java技術(shù)盛宴
盡管很多人反感“Android應(yīng)該改名叫Java GE”這種說法,但是沒人能否認(rèn)Java語言是Android開發(fā)人員所選的必備工具。Android運(yùn)行時使用自己的虛擬機(jī)Dalvik,這并不是多數(shù)程序開發(fā)人員使用的普通Java虛擬機(jī)。Dalvik支持Java編程語言的大部分功能—但并不是全部。在本文中,您將學(xué)習(xí)高級Java功能及其如何在Android中實(shí)現(xiàn)。這些功能包括并發(fā)性、聯(lián)網(wǎng)和數(shù)據(jù)庫訪問。
51CTO推薦:Android開發(fā)專題
Android應(yīng)用程序一個最常見的任務(wù)就是檢索數(shù)據(jù)或通過網(wǎng)絡(luò)將數(shù)據(jù)發(fā)送到遠(yuǎn)程服務(wù)器。這一操作的結(jié)果通常是一些您想要展示給用戶的新數(shù)據(jù)。這意味著您需要修改用戶界面。大多數(shù)開發(fā)人員知道您將不會執(zhí)行一個潛在的長期運(yùn)行任務(wù),例如,在主UI線程上通過網(wǎng)絡(luò)訪問數(shù)據(jù)(特別使用一個網(wǎng)絡(luò)連接非常慢的手機(jī))。凍結(jié)您的應(yīng)用程序直至長期運(yùn)行任務(wù)完成。事實(shí)上,如果這個任務(wù)超過5秒,Android操作系統(tǒng)將出現(xiàn)臭名昭著的Application Not Responding對話框。
圖1.Android臭名昭著的Application Not Responding對話框
您不可能知道用戶網(wǎng)絡(luò)連接能有多慢。為了避免冒險,您必須在不同的線程上執(zhí)行任務(wù),或者至少不在主UI線程上執(zhí)行。許多Android應(yīng)用程序,但不是全部,需要處理多線程,由此引起并發(fā)。應(yīng)用程序經(jīng)常需要本地保存數(shù)據(jù),Android數(shù)據(jù)庫是一個很好的選擇。這三個場景(不同線程,并發(fā)和本地保存數(shù)據(jù))在Java環(huán)境中有許多標(biāo)準(zhǔn)方法可以用來處理。然而,正如您將要看到的,Android提供不同的選擇。讓我們逐個看看,看看其優(yōu)點(diǎn)和缺點(diǎn)。
Android網(wǎng)絡(luò)
通過網(wǎng)絡(luò)使用Java編程進(jìn)行調(diào)用是簡單的,我們熟悉的java.net包含幾個執(zhí)行此操作的類。這些類大多數(shù)在Android中都可用,事實(shí)上,您可以使用像java.net.URL和java.net.URLConnection這樣的類,就像您在其他Java應(yīng)用程序中那樣。然而,Android包括pacheHttpClient庫,這是在Android上連接網(wǎng)絡(luò)的***方法。即使您使用常用Java類,Android實(shí)現(xiàn)仍然使用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[i], oldStocks[i].getId());
- stock.setCurrentPrice(Double.parseDouble(values[1]));
- stock.setName(values[2]);
- newStocks.add(stock);
- line = reader.readLine();
- i++;
- }
- return newStocks;
- }
在這段代碼中有一組Stock對象。這是基本的數(shù)據(jù)結(jié)構(gòu)對象,保存用戶擁有股票信息(比如,代號、價格等)以及更多的個人信息(比如,用戶付了多少錢)。您可以使用HttpClient類從Yahoo Finance檢索動態(tài)數(shù)據(jù)(例如,這支股票目前的價格)。HttpClient包含一個HttpUriRequest,在本例中,您可以使用HttpGet,這是HttpUriRequest的一個子類。類似地,當(dāng)您需要向遠(yuǎn)程服務(wù)器發(fā)送數(shù)據(jù)時,可以使用HttpPost類,當(dāng)您從客戶端得到HttpResponse時,您能接觸到響應(yīng)的潛在InputStream、對其進(jìn)行緩沖、解析來獲取股票信息。
現(xiàn)在,您看到了如何通過網(wǎng)絡(luò)檢索數(shù)據(jù)、如何用這個數(shù)據(jù)來通過使用多線程智能地更新Android UI。
#p#
Android并發(fā)性實(shí)踐
如果您在應(yīng)用程序的主UI線程上運(yùn)行清單1中的代碼,可能會出現(xiàn)Application Not Responding對話框,具體視用戶網(wǎng)絡(luò)速度而定。因此必須確定生成一個線程來獲取數(shù)據(jù)。清單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的標(biāo)題聲明這是naïve代碼,確實(shí)是。在這個例子中,您將調(diào)用清單1中的fetchStockData方法,將其封裝在Runnable對象中,并在一個新線程中執(zhí)行。在這個新線程中,您可以訪問stocks,一個封裝Activity(此類創(chuàng)建了UI)的成員變量。顧名思義,這是Stock對象的一個數(shù)據(jù)結(jié)構(gòu)(本例中是java.util.ArrayList)。換句話說,您在兩個線程之間共享數(shù)據(jù),主UI線程和衍生(spawned)線程(在清單2中調(diào)用)。當(dāng)您修改了衍生線程中的共享數(shù)據(jù)時,通過在Activity對象上調(diào)用refresh方法來更新UI。
如果您編寫了Java Swing應(yīng)用程序,您可能需要遵循一個像這樣的模式。然而,這在Android中將不能正常工作。衍生線程根本不能修改UI。因此在不凍結(jié)UI,但另一方面,在數(shù)據(jù)收到之后又允許您修改UI的情況下,您怎樣檢索數(shù)據(jù)?android.os.Handler類允許您在線程之間協(xié)調(diào)和通信。清單3顯示了一個使用Handler的已更新refreshStockData方法。
清單3.實(shí)際工作的多線程—通過使用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。相反的,當(dāng)您將消息發(fā)送到Handler,然后由Handler來修改UI。也要注意,在線程中您不能修改stocks成員變量,正如您之前所做的。相反地您可以修改數(shù)據(jù)的本地副本。嚴(yán)格地來說,這是不是必須的,但這更為安全。
清單3說明了在并發(fā)編程中一些非常普遍的模式:復(fù)制數(shù)據(jù)、將數(shù)據(jù)解析到執(zhí)行長期任務(wù)的線程中、將結(jié)果數(shù)據(jù)傳遞回主UI線程、以及根據(jù)所屬數(shù)據(jù)更新主UI線程。Handlers是Android中的主要通信機(jī)制,它們使這個模式易于實(shí)現(xiàn)。然而,清單3中仍然有一些樣本代碼。幸好,Android提供方法來封裝和消除大多數(shù)樣本代碼。清單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樣本代碼明顯減少。您不能創(chuàng)建任何線程或Handlers。使用AsyncTask來封裝所有樣本代碼。要創(chuàng)建AsyncTask,您必須實(shí)現(xiàn)doInBackground方法。該方法總是在獨(dú)立的線程中執(zhí)行,因此您可以自由調(diào)用長期運(yùn)行任務(wù)。它的輸入類型來自您所創(chuàng)建的AsyncTask的類型參數(shù)。在本例中,***個類型參數(shù)是Stock,因此doInBackground獲得傳遞給它的一組Stock對象。類似地,它返回一個ArrayList
有了AsyncTask,您就完全可以簡化多線程代碼。它可以將許多并發(fā)陷阱從您的開發(fā)路徑刪除,您仍然可以使用AsyncTask尋找一些潛在問題,例如,在doInBackground方法對象執(zhí)行的同時設(shè)備上的方向發(fā)生改變時可能發(fā)生什么。更多關(guān)于如何處理這類案例的技術(shù),見參考資料的鏈接。
現(xiàn)在我們開始討論另一個常見任務(wù),其中Android明顯背離常用的Java方法——使用數(shù)據(jù)庫進(jìn)行處理。
#p#
Android數(shù)據(jù)庫連通性
Android中一個非常有用的特征就是存在本地關(guān)系數(shù)據(jù)庫。保證您能在本地文件中存儲您的數(shù)據(jù),但通常更有用的是使用一個關(guān)系型數(shù)據(jù)庫管理系統(tǒng)(RelationalDatabaseManagementSystem,RDBMS)來存儲。Android提供給您常用的SQLite數(shù)據(jù)庫來進(jìn)行處理,因?yàn)閷τ谙馎ndroid這類嵌入式系統(tǒng)它是高度優(yōu)化的。它被Android上的核心應(yīng)用程序所用。例如,用戶地址簿是存儲在一個SQLite數(shù)據(jù)庫中。現(xiàn)在,對于給定的Android的Java實(shí)現(xiàn),您可以使用JDBC來訪問這些數(shù)據(jù)庫。出人意料的是,Android甚至包括構(gòu)成主要部分JDBC API的java.sql和javax.sql包。然而,當(dāng)涉及使用本地Android數(shù)據(jù)庫進(jìn)行處理時,這毫無用處。相反地,您想要使用android.database和android.database.sqlite包。清單5是一個使用這些類存儲和檢索數(shù)據(jù)的示例。
清單5.使用Android進(jìn)行數(shù)據(jù)庫訪問
- 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數(shù)據(jù)庫。因?yàn)槟鷮⒁褂靡粋€嵌入式數(shù)據(jù)庫,不僅是您的應(yīng)用程序要使用它,而且也要通過應(yīng)用程序來創(chuàng)建它。您需要提供代碼來創(chuàng)建該數(shù)據(jù)庫。Android提供一個有用的抽象幫助類SQLiteOpenHelper。要完成這一操作,您需要擴(kuò)展這個抽象類并提供代碼通過使用onCreate方法創(chuàng)建您的數(shù)據(jù)庫。當(dāng)您有一個幫助程序?qū)嵗龝r,就可以獲取一個SQLiteDatabase實(shí)例,您可以用來執(zhí)行任意SQL語句。您的數(shù)據(jù)庫類有兩個較為方便的方法。***個是addStock,用于將新股票保存到數(shù)據(jù)庫中。注意,您使用了一個SQLiteStatement實(shí)例,這類似于一個java.sql.PreparedStatement。需要注意的是,在您的類構(gòu)造器中如何對其進(jìn)行編譯,使其在每次調(diào)用addStock時都能重復(fù)利用。在每個addStock調(diào)用中,SQLiteStatement的變量(INSERT_SQL字符串中的問號)必然要將數(shù)據(jù)傳遞給addStock。再一次強(qiáng)調(diào),這類似于PreparedStatement,您可以從JDBC了解它。
另一個方法是getStocks。顧名思義,它從數(shù)據(jù)庫中檢索所有股票。注意,您再次使用一個SQL字符串,正如您在JDBC中所用的那樣。您可以在SQLiteDatabase類上通過使用rawQuery方法來進(jìn)行處理。這個類也有幾個查詢方法,讓您可以不使用SQL直接查詢數(shù)據(jù)庫。所有這些方法都返回一個Cursor對象,和java.sql.ResultSet非常相似。您可以將Cursor移動到從數(shù)據(jù)庫中返回的數(shù)據(jù)所在行,在每一行,您可以使用getInt、getString和其他的方法來檢索您要查詢的數(shù)據(jù)庫中各列相關(guān)的值。再一次強(qiáng)調(diào),這和ResultSet十分相似。也和ResultSet比較相似,當(dāng)您完成操作之后,關(guān)閉Cursor也十分重要的。如果您沒有關(guān)閉Cursors,那么可能會迅速地耗盡內(nèi)存并導(dǎo)致您的應(yīng)用程序崩潰。
查詢本地數(shù)據(jù)庫是一個比較慢的過程,特別是,如果您有多行數(shù)據(jù)或者您需要在多個表之間運(yùn)行復(fù)雜的查詢語句。然而,數(shù)據(jù)庫查詢或插入超過5秒且出現(xiàn)一個Application Not Responding對話框,這種情況不太可能發(fā)生,但是當(dāng)您的數(shù)據(jù)庫忙于讀取和寫入數(shù)據(jù)時,凍結(jié)您的UI是不明智的。當(dāng)然,避免這種情況***的辦法是使用AsyncTask。清單6展示了這個示例。
清單6.在一個單獨(dú)的線程上插入數(shù)據(jù)庫
- 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);
- }
- });
您可以先為按鈕創(chuàng)建一個實(shí)踐監(jiān)聽器。當(dāng)用戶點(diǎn)擊按鈕時,您可以從各個小部件(確切地說是EditText小部件)讀取股票數(shù)據(jù)并填入一個新的Stock對象。您可以創(chuàng)建一個AsyncTask,并通過doInBackground方法從清單5中調(diào)用addStock方法。如此,addStock將在一個背景線程上執(zhí)行,而不是在主UI線程上。完成之后,將新Stock對象從數(shù)據(jù)庫傳遞到在主UI線程上執(zhí)行的addStockAndRefresh方法。
作者簡介:Michael Galpin是eBay的一名架構(gòu)師。他從1998年開始做職業(yè)程序員,并擁有加州理工學(xué)院數(shù)學(xué)專業(yè)的學(xué)士學(xué)位。
【編輯推薦】