Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android最佳實踐之Notification、下拉刷新、內存及性能建議等

Android最佳實踐之Notification、下拉刷新、內存及性能建議等

編輯:關於Android編程

Notification通知

參考地址:http://developer.android.com/training/notify-user/index.html
通知(Notification)是Android中使用的非常多的一個事件提示機制。

創建一個Notification

例子中的Notification是基於Support Library中的NotificationCompat.Builder類。我們使用時要繼承這個類,它提供了各個平台最好的Notification支持。

創建一個Notification Builder

直接上代碼:

NotificationCompat.Builder mBuilder =
    new NotificationCompat.Builder(this)
    .setSmallIcon(R.drawable.notification_icon)
    .setContentTitle("My notification")
    .setContentText("Hello World!");

其中,三個參數分別指通知的icon,標題和內容。

定義Notification的Action

action不是必需的,但應該為Notification提供至少一個action,它直接從Notification跳轉到Activity。在Notification中action是PendingIntent中的Intent定義的。當從Notification啟動一個Activity,你必須保證用戶的導航體驗。在下面的代碼片段中,我們沒有創建一個人工的Back Stack。

Intent resultIntent = new Intent(this, ResultActivity.class);
...
// Because clicking the notification opens a new ("special") activity, there's
// no need to create an artificial back stack.
PendingIntent resultPendingIntent =
    PendingIntent.getActivity(
    this,
    0,
    resultIntent,
    PendingIntent.FLAG_UPDATE_CURRENT
);

設置Notification的點擊事件

將NotificationCompat.Builder和Pendingintent聯系起來,使用一下代碼:

PendingIntent resultPendingIntent;
...
mBuilder.setContentIntent(resultPendingIntent);

發布一個Notification

NotificationCompat.Builder mBuilder;
...
// Sets an ID for the notification
int mNotificationId = 001;
// Gets an instance of the NotificationManager service
NotificationManager mNotifyMgr = 
        (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
// Builds the notification and issues it.
mNotifyMgr.notify(mNotificationId, mBuilder.build());

啟動一個Activity保持Navigation

Notification啟動Activity,有兩種場景:一種是普通的Activity,另一種是特殊的Activity。

創建一個普通的Activity的PendingIntent

1、在manifest中創建一個activity:
 

<activity android:label="@string/app_name" android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN">
        <category android:name="android.intent.category.LAUNCHER">
    </category></action></intent-filter>
</activity>
<activity android:name=".ResultActivity" android:parentactivityname=".MainActivity">
    <meta-data android:name="android.support.PARENT_ACTIVITY" android:value=".MainActivity">
</meta-data></activity>

2、創建一個Back Stack:

int id = 1;
...
Intent resultIntent = new Intent(this, ResultActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
// Adds the back stack
stackBuilder.addParentStack(ResultActivity.class);
// Adds the Intent to the top of the stack
stackBuilder.addNextIntent(resultIntent);
// Gets a PendingIntent containing the entire back stack
PendingIntent resultPendingIntent =
        stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
...
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setContentIntent(resultPendingIntent);
NotificationManager mNotificationManager =
    (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mNotificationManager.notify(id, builder.build());

創建一個特殊的Activity PendingIntent

一個特殊的Activity不需要創建Back Stack,所以不是必須在Manifest中定義Activity層級(設置父Activity),也不需要addParentStack()方法來創建一個Back棧。取而代之的是,在Manifest中設置Activity選項。比如:

...

android:taskAffinity=”“:和FLAG_ACTIVITY_NEW_TASK聯合起來使用,可以保證Activity不會進入App默認的Stack中。任何存在的有默認affinity的Stack不受影響。
android:excludeFromRecents=”true”:從Recents中排除新的task,這樣導航就不會通過Back訪問到它。

// Instantiate a Builder object.
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
// Creates an Intent for the Activity
Intent notifyIntent =
        new Intent(new ComponentName(this, ResultActivity.class));
// Sets the Activity to start in a new, empty task
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 
        Intent.FLAG_ACTIVITY_CLEAR_TASK);
// Creates the PendingIntent
PendingIntent notifyIntent =
        PendingIntent.getActivity(
        this,
        0,
        notifyIntent,
        PendingIntent.FLAG_UPDATE_CURRENT
);

// Puts the PendingIntent into the notification builder
builder.setContentIntent(notifyIntent);
// Notifications are issued by sending them to the
// NotificationManager system service.
NotificationManager mNotificationManager =
    (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Builds an anonymous Notification object from the builder, and
// passes it to the NotificationManager
mNotificationManager.notify(id, builder.build());

更新一個Notification

當你需要對於相同類型的事件多次發出通知,你應該避免發一個全新的Notification。相反,你應該考慮更新之前存在的通知,通過改變它的一些值或添加一些內容,或兩者兼而有之。

修改Notification

創建一個可以修改的Notification,需要使用NotificationManager.notify(ID, notification)給Notification定義一個ID,那麼後面更新時,使用相同的id進行更新。

mNotificationManager =
        (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
// Sets an ID for the notification, so it can be updated
int notifyID = 1;
mNotifyBuilder = new NotificationCompat.Builder(this)
    .setContentTitle("New Message")
    .setContentText("You've received new messages.")
    .setSmallIcon(R.drawable.ic_notify_status)
numMessages = 0;
// Start of a loop that processes data and then notifies the user
...
    mNotifyBuilder.setContentText(currentText)
        .setNumber(++numMessages);
    // Because the ID remains unchanged, the existing notification is
    // updated.
    mNotificationManager.notify(
            notifyID,
            mNotifyBuilder.build());
...

刪除Notification

以下情況之一Notification就不可見了(被刪除了):

用戶單獨或通過使用“全部清除”(如果可以清除通知)清除Notification 當創建Notification時設置setAutoCancel(),用戶點擊了Notification自動消失 對指定ID的Notification進行cancel(),這樣會取消包括正在運行的通知 使用cancelAll(),清除之前發布的所有Notification

使用Big View樣式

Notification有兩種樣式,一種的Normal的,一個是BigView的。BigView的Notification只有當被拉下來時才會顯示。
BigView的樣式是從Android 4.1才開始引入,之前的老版本不支持。下面介紹如何在Normal的Notification集成進BigView的樣式。
notifications-normalview
圖1:notifications-normalview
notifications-bigview
圖2:notifications-bigview

創建一個新的Notification啟動新的Activity

Demo程序中使用IntentService子類(PingService)來創建和發布Notification。
下面的代碼片段是在IntentService的onHandleIntent()方法中,創建PendingIntent的:

Intent resultIntent = new Intent(this, ResultActivity.class);
resultIntent.putExtra(CommonConstants.EXTRA_MESSAGE, msg);
resultIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 
        Intent.FLAG_ACTIVITY_CLEAR_TASK);

// Because clicking the notification launches a new ("special") activity, 
// there's no need to create an artificial back stack.
PendingIntent resultPendingIntent =
         PendingIntent.getActivity(
         this,
         0,
         resultIntent,
         PendingIntent.FLAG_UPDATE_CURRENT
);

// This sets the pending intent that should be fired when the user clicks the
// notification. Clicking the notification launches a new activity.
builder.setContentIntent(resultPendingIntent);

構造一個Big View

下面的代碼展示如何在big view中顯示Button:

// Sets up the Snooze and Dismiss action buttons that will appear in the
// big view of the notification.
Intent dismissIntent = new Intent(this, PingService.class);
dismissIntent.setAction(CommonConstants.ACTION_DISMISS);
PendingIntent piDismiss = PendingIntent.getService(this, 0, dismissIntent, 0);

Intent snoozeIntent = new Intent(this, PingService.class);
snoozeIntent.setAction(CommonConstants.ACTION_SNOOZE);
PendingIntent piSnooze = PendingIntent.getService(this, 0, snoozeIntent, 0);

下面的代碼片段展示創建Builder對象。它設置了BigView的big text樣式,也設置了它的內容作為提醒消息。它使用addAction增加了Snooze和Dismiss按鈕(這兩個按鈕綁定了PendingIntent)顯示在Notification的BigView中。

// Constructs the Builder object.
NotificationCompat.Builder builder =
        new NotificationCompat.Builder(this)
        .setSmallIcon(R.drawable.ic_stat_notification)
        .setContentTitle(getString(R.string.notification))
        .setContentText(getString(R.string.ping))
        .setDefaults(Notification.DEFAULT_ALL) // requires VIBRATE permission
        /*
         * Sets the big view "big text" style and supplies the
         * text (the user's reminder message) that will be displayed
         * in the detail area of the expanded notification.
         * These calls are ignored by the support library for
         * pre-4.1 devices.
         */
        .setStyle(new NotificationCompat.BigTextStyle()
                .bigText(msg))
        .addAction (R.drawable.ic_stat_dismiss,
                getString(R.string.dismiss), piDismiss)
        .addAction (R.drawable.ic_stat_snooze,
                getString(R.string.snooze), piSnooze);

在Notification中顯示進度條

Notification中可以顯示一個動態的進度條,用來顯示正在進行的任務。如何你能計算任務的事件和長度,可以使用“確定”的指示器(進度條),如果不能確定任務長短,則使用“不確定”指示器(活動指示器)。

顯示一個固定長度的進度條指示器(Progress Indicator)

通過添加setProgress(max, progress, false),然後發布Notification則可以在Notification中增加一個進度條,第三個參數指的就是任務長度是否確定(indeterminate (true) or determinate (false))。
當progress慢慢增長到max時,可以繼續顯示Notification也可以清除掉。不管怎樣,可以通過setProgress(0, 0, false)清掉進度條:

int id = 1;
...
mNotifyManager =
        (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
mBuilder = new NotificationCompat.Builder(this);
mBuilder.setContentTitle("Picture Download")
    .setContentText("Download in progress")
    .setSmallIcon(R.drawable.ic_notification);
// Start a lengthy operation in a background thread
new Thread(
    new Runnable() {
        @Override
        public void run() {
            int incr;
            // Do the "lengthy" operation 20 times
            for (incr = 0; incr <= 100; incr+=5) {
                    // Sets the progress indicator to a max value, the
                    // current completion percentage, and "determinate"
                    // state
                    mBuilder.setProgress(100, incr, false);
                    // Displays the progress bar for the first time.
                    mNotifyManager.notify(id, mBuilder.build());
                        // Sleeps the thread, simulating an operation
                        // that takes time
                        try {
                            // Sleep for 5 seconds
                            Thread.sleep(5*1000);
                        } catch (InterruptedException e) {
                            Log.d(TAG, "sleep failure");
                        }
            }
            // When the loop is finished, updates the notification
            mBuilder.setContentText("Download complete")
            // Removes the progress bar
                    .setProgress(0,0,false);
            mNotifyManager.notify(id, mBuilder.build());
        }
    }
// Starts the thread by calling the run() method in its Runnable
).start();

progress_bar_summary
圖:任務完成,清掉進度條<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxoMyBpZD0="顯示一個持續的活動指示器">顯示一個持續的活動指示器

要顯示一個不確定(持續)的進度條,可以設置setProgress(0, 0, true)。

// Sets the progress indicator to a max value, the current completion
// percentage, and "determinate" state
mBuilder.setProgress(100, incr, false);
// Issues the notification
mNotifyManager.notify(id, mBuilder.build());

將上面的代碼替換成下面的

 // Sets an activity indicator for an operation of indeterminate length
mBuilder.setProgress(0, 0, true);
// Issues the notification
mNotifyManager.notify(id, mBuilder.build());

activity_indicator
圖:一個正在進行任務的活動指示器

例子下載:NotificationExample

下拉刷新(Swipe-to-Refresh)

參考地址:http://developer.android.com/training/swipe/index.html
Android平台提供了下拉刷新(swipe-to-refresh)的組件,讓用戶可以手動拉動去刷新數據。

注意:這個需要最新的 Android v4 Support Library APIs支持。

在你的App中添加Swipe-to-Refresh

Swipe-to-Refresh是通過SwipeRefreshLayout組件實現的。通過用戶下拉,提供一個有特色的進度條,然後觸發回調刷新數據。可以在ListView或者GridView的布局文件外加一個SwipeRefreshLayout的父元素來實現下拉刷新。
比如在ListView外面加一個SwipeRefreshLayout如下:



    

也可以在ListFragment外面套一層SwipeRefreshLayout。SwipeRefreshLayout自動為id為@android:id/list的ListView添加下拉刷新功能。

在Action Bar上添加刷新按鈕

可以在Action Bar上添加一個刷新按鈕,這樣用戶不用通過下拉手勢來刷新數據。比如,可以通過鍵盤或軟鍵盤來觸發刷新事件。
你可以通過android:showAsAction=never設置一個Menu,不要設置成Button,來添加一個刷新“按鈕”。



	 

響應刷新請求

響應刷新手勢

響應刷新手勢需要實現SwipeRefreshLayout.OnRefreshListener接口下的onRefresh()方法,你應該將真正刷新的動作寫在一個獨立的方法裡,這樣不管是下拉刷新還是點擊ActionBar上的刷新按鈕,都可以復用。
當更新完數據,調用setRefreshing(false),這個將使SwipeRefreshLayout去掉進度條並更新UI,下面就是一個使用onRefresh() 調用 myUpdateOperation()方法更新ListView數據的例子:

/*
 * Sets up a SwipeRefreshLayout.OnRefreshListener that is invoked when the user
 * performs a swipe-to-refresh gesture.
 */
mySwipeRefreshLayout.setOnRefreshListener(
    new SwipeRefreshLayout.OnRefreshListener() {
        @Override
        public void onRefresh() {
            Log.i(LOG_TAG, "onRefresh called from SwipeRefreshLayout");

            // This method performs the actual data-refresh operation.
            // The method calls setRefreshing(false) when it's finished.
            myUpdateOperation();
        }
    }
);

響應刷新“按鈕”

當用戶通過onOptionsItemSelected()方法觸發菜單中的刷新行為,需要手工設置setRefreshing(true),來執行刷新操作,然後調用真正刷新數據的方法,在刷新完成時調用setRefreshing(false)。例如:

/*
 * Listen for option item selections so that we receive a notification
 * when the user requests a refresh by selecting the refresh action bar item.
 */
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {

        // Check if user triggered a refresh:
        case R.id.menu_refresh:
            Log.i(LOG_TAG, "Refresh menu item selected");

            // Signal SwipeRefreshLayout to start the progress indicator
            mySwipeRefreshLayout.setRefreshing(true);

            // Start the refresh background task.
            // This method calls setRefreshing(false) when it's finished.
            myUpdateOperation();

            return true;
    }

    // User didn't trigger a refresh, let the superclass handle this action
    return super.onOptionsItemSelected(item);

}

例子下載:SwipeRefreshLayoutBasic
SwipeRefreshLayoutListFragment

搜索功能(SearchView)

參考地址:http://developer.android.com/training/search/index.html
Android自帶的搜索框可以很方便為用戶提供一致的用戶體驗。有兩種實現方法,一種是SearchView(Android3.0提供),另一種為兼容老版本Android系統提供了一個默認的搜索對話框(default search dialog)。

創建一個搜索界面(Search Interface)

Android3.0以後,系統提供了一個SearchView放在ActionBar作為首選的搜索方式。

注意:後面會提到兼容Android2.1及以前版本,如何使用SearchView.

在App Bar上添加SearchView


在Activity的onCreateOptionsMenu() 方法中,解析菜單資源(res/menu/options_menu.xml)。

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.options_menu, menu);

    return true;
}

創建一個Searchable 配置

SearchView的搜索行為配置放在文件res/xml/searchable.xml中,這個文件中最少需要配置一個android:label屬性(類似於Manifest中 或 的android:label屬性)。我們建議加多一個android:hint 屬性提示用戶需要在搜索框輸入啥。



在Manifest文件中指定 屬性指向searchable.xml,那麼需要如下配置:


    ...
    

在onCreateOptionsMenu()方法創建之前,將搜索配置與SearchView通過setSearchableInfo(SearchableInfo)進行關聯。

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.options_menu, menu);

    // Associate searchable configuration with the SearchView
    SearchManager searchManager =
           (SearchManager) getSystemService(Context.SEARCH_SERVICE);
    SearchView searchView =
            (SearchView) menu.findItem(R.id.search).getActionView();
    searchView.setSearchableInfo(
            searchManager.getSearchableInfo(getComponentName()));

    return true;
}

調用getSearchableInfo()方法返回的SearchableInfo對象是由searchable.xml文件生成的。當searchable.xml正確關聯到了SearchView,當提交一個查詢時SearchView將啟動一個帶ACTION_SEARCH Intent的Activity。你需要在Activity中處理查詢動作。

創建一個Searchable Activity

創建一個處理搜索結果的Activity,需要指定ACTION_SEARCH的Action。例如:


    ...
    
        
    
    ...

在你的搜索Activity的onCreate()中處理搜索行為。

如果你的搜索Activity的啟動模式是android:launchMode=”singleTop”,那麼還需要在onNewIntent()中處理搜索動作。在singleTop模式下,只會創建一個Activity實例。後面啟動這個Activity,不會創建新的Activity。
代碼如下:

public class SearchResultsActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        ...
        handleIntent(getIntent());
    }

    @Override
    protected void onNewIntent(Intent intent) {
        ...
        handleIntent(intent);
    }

    private void handleIntent(Intent intent) {

        if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
            String query = intent.getStringExtra(SearchManager.QUERY);
            //use the query to search your data somehow
        }
    }
    ...
}

如果你現在運行App,SearchView將接收用戶的查詢並啟動一個ACTION_SEARCH的Activity處理搜索結果。接下來由你決定如何保存和搜索用戶要搜索的內容了。

存儲和搜索數據

存數數據有多種方式,有線上的數據庫,有本地的數據庫SQLite,有txt文本文件,你可以選擇最好最合適的方式去存儲數據。這裡創建SQLite的虛擬表來展示強大的全文本搜索功能。這張表的數據有txt文件中填充,裡面的內容是每一行一個單詞和他對應的定義。

創建一個虛擬表

public class DatabaseTable {
    private final DatabaseOpenHelper mDatabaseOpenHelper;

    public DatabaseTable(Context context) {
        mDatabaseOpenHelper = new DatabaseOpenHelper(context);
    }
}

在DatabaseTable類中擴展SQLiteOpenHelper創建一個內部類。SQLiteOpenHelper類定義了抽象方法,你必須重寫裡面的抽象方法,以便在必要時可以創建表和升級表。例如,下面是在字典App中聲明創建數據庫表一些代碼:

public class DatabaseTable {

    private static final String TAG = "DictionaryDatabase";

    //The columns we'll include in the dictionary table
    public static final String COL_WORD = "WORD";
    public static final String COL_DEFINITION = "DEFINITION";

    private static final String DATABASE_NAME = "DICTIONARY";
    private static final String FTS_VIRTUAL_TABLE = "FTS";
    private static final int DATABASE_VERSION = 1;

    private final DatabaseOpenHelper mDatabaseOpenHelper;

    public DatabaseTable(Context context) {
        mDatabaseOpenHelper = new DatabaseOpenHelper(context);
    }

    private static class DatabaseOpenHelper extends SQLiteOpenHelper {

        private final Context mHelperContext;
        private SQLiteDatabase mDatabase;

        private static final String FTS_TABLE_CREATE =
                    "CREATE VIRTUAL TABLE " + FTS_VIRTUAL_TABLE +
                    " USING fts3 (" +
                    COL_WORD + ", " +
                    COL_DEFINITION + ")";

        DatabaseOpenHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
            mHelperContext = context;
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            mDatabase = db;
            mDatabase.execSQL(FTS_TABLE_CREATE);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
                    + newVersion + ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS " + FTS_VIRTUAL_TABLE);
            onCreate(db);
        }
    }
}

向表裡加數據

下面的代碼想你展示如何從txt文件(res/raw/definitions.txt)中讀取數據插入到表裡,使用了多線程來讀取數據,以防阻塞UI線程。(注:你可能需要寫一個回調來處理讀取完成的動作)

private void loadDictionary() {
        new Thread(new Runnable() {
            public void run() {
                try {
                    loadWords();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }

private void loadWords() throws IOException {
    final Resources resources = mHelperContext.getResources();
    InputStream inputStream = resources.openRawResource(R.raw.definitions);
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

    try {
        String line;
        while ((line = reader.readLine()) != null) {
            String[] strings = TextUtils.split(line, "-");
            if (strings.length < 2) continue;
            long id = addWord(strings[0].trim(), strings[1].trim());
            if (id < 0) {
                Log.e(TAG, "unable to add word: " + strings[0].trim());
            }
        }
    } finally {
        reader.close();
    }
}

public long addWord(String word, String definition) {
    ContentValues initialValues = new ContentValues();
    initialValues.put(COL_WORD, word);
    initialValues.put(COL_DEFINITION, definition);

    return mDatabase.insert(FTS_VIRTUAL_TABLE, null, initialValues);
}

調用loadDictionary()向表裡插入數據。這個方法在DatabaseOpenHelper中的onCreate()中創建表的代碼之後:

@Override
public void onCreate(SQLiteDatabase db) {
    mDatabase = db;
    mDatabase.execSQL(FTS_TABLE_CREATE);
    loadDictionary();
}

搜索查詢

寫一些SQL語句進行查詢:

public Cursor getWordMatches(String query, String[] columns) {
    String selection = COL_WORD + " MATCH ?";
    String[] selectionArgs = new String[] {query+"*"};

    return query(selection, selectionArgs, columns);
}

private Cursor query(String selection, String[] selectionArgs, String[] columns) {
    SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
    builder.setTables(FTS_VIRTUAL_TABLE);

    Cursor cursor = builder.query(mDatabaseOpenHelper.getReadableDatabase(),
            columns, selection, selectionArgs, null, null, null);

    if (cursor == null) {
        return null;
    } else if (!cursor.moveToFirst()) {
        cursor.close();
        return null;
    }
    return cursor;
}

調用getWordMatches()方法進行查詢,查詢結果返回一個Cursor。這個例子在搜索Activity中的handleIntent()中調用getWordMatches()。

DatabaseTable db = new DatabaseTable(this);

...

private void handleIntent(Intent intent) {

    if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
        String query = intent.getStringExtra(SearchManager.QUERY);
        Cursor c = db.getWordMatches(query, null);
        //process Cursor and display results
    }
}

保持向下兼容

SearchView和ActionBar是Android3.0及以上版本的才有的,為兼容舊版本,需要使用系統的搜索對話框(search dialog)來顯示在App的上層。

設置Minimum and Target API

在Manifest文件中設置最小支持3.0以前,Target是3.0以後的,這樣系統會在Android3.0及以上的機器上使用ActionBar而在舊版本機器上使用傳統菜單(Menu)。




...

為舊版本設備提供 Search Dialog

在選擇搜索菜單時,舊版中調用onSearchRequested()來顯示一個搜索對話框。

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case R.id.search:
            onSearchRequested();
            return true;
        default:
            return false;
    }
}

在運行時檢查Android的版本

在onCreateOptionsMenu() 中檢查Android的版本,判斷如果是3.0及以上,則使用SearchView。

@Override
public boolean onCreateOptionsMenu(Menu menu) {

    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.options_menu, menu);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        SearchManager searchManager =
                (SearchManager) getSystemService(Context.SEARCH_SERVICE);
        SearchView searchView =
                (SearchView) menu.findItem(R.id.search).getActionView();
        searchView.setSearchableInfo(
                searchManager.getSearchableInfo(getComponentName()));
        searchView.setIconifiedByDefault(false);
    }
    return true;
}

Demo地址:https://github.com/bendeng/SearchView

鍵盤輸入

參考地址:http://developer.android.com/training/keyboard-input/index.html
Android系統提供了屏幕上使用的軟鍵盤和實體鍵盤兩種鍵盤輸入方式。

指定鍵盤類型

在 元素中使用android:inputType屬性指定輸入類型,比如‘phone’,則鍵盤只能輸入電話號碼的數字:

edittext-phone.png
使用textPassword屬性值來指定輸入密碼:

    

ime_password.png

允許拼寫建議或其他行為

textAutoCorrect允許你自動糾正拼寫錯誤。下面例子就是讓第一個字母大寫以及自動糾錯的屬性:

ime_autocorrect.png

指定輸入法的Action

如果你的TextView不允許多行(多行屬性這樣設置android:inputType=”textMultiLine”),那麼軟鍵盤右下角的按鈕顯示NextDone。你可以設置更多的Action比如Send或者Go。設置鍵盤Action的Button,設置android:imeOptions屬性即可:

edittext-actionsend.png
通過設置TextView.OnEditorActionListener 的監聽,監聽action的事件,比如Send

EditText editText = (EditText) findViewById(R.id.search);
editText.setOnEditorActionListener(new OnEditorActionListener() {
    @Override
    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
        boolean handled = false;
        if (actionId == EditorInfo.IME_ACTION_SEND) {
            sendMessage();
            handled = true;
        }
        return handled;
    }
});

Activity啟動時顯示輸入法

在Manifest中設置Activity的屬性android:windowSoftInputMode即可:


    
        ...
    
    ...

按需顯示輸入法

下面的例子時在一個View中輸入某些內容,調用requestFocus()先獲取焦點,然後使用showSoftInput()打開輸入法:

public void showSoftKeyboard(View view) {
    if (view.requestFocus()) {
        InputMethodManager imm = (InputMethodManager)
                getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
    }
}

注意:一旦輸入法顯示後,就不能通過代碼將其隱藏。只能通過比如實體返回鍵隱藏。

指定UI如何響應輸入法顯示

  
        ...
    

adjustResize很重要,它會讓輸入法顯示擋住Activity下面的部分時自動上移,露出輸入框。

使用Tab鍵導航

定義focus順序使用android:nextFocusForward屬性。下面的例子,焦點從button1 到editText1 再到button2:

使用方向鍵導航

如下例子:

處理鍵盤Action。單個鍵或加轉換鍵

可以通過實現KeyEvent.Callback接口,攔截或直接處理鍵盤的事件,而不是默認的讓系統處理鍵盤輸入。每一個Activity和View都實現了KeyEvent.Callback接口,我們只需要覆蓋其中的方法即可:

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
    switch (keyCode) {
        case KeyEvent.KEYCODE_D:
            moveShip(MOVE_LEFT);
            return true;
        case KeyEvent.KEYCODE_F:
            moveShip(MOVE_RIGHT);
            return true;
        case KeyEvent.KEYCODE_J:
            fireMachineGun();
            return true;
        case KeyEvent.KEYCODE_K:
            fireMissile();
            return true;
        default:
            return super.onKeyUp(keyCode, event);
    }
}

響應加了Shift 或Control鍵的按鍵事件。簡單的處理是使用 isShiftPressed()isCtrlPressed()判斷Shift或Ctrl鍵是否按下了,以此來做邏輯處理:

@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
    switch (keyCode) {
        ...
        case KeyEvent.KEYCODE_J:
            if (event.isShiftPressed()) {
                fireLaser();
            } else {
                fireMachineGun();
            }
            return true;
        case KeyEvent.KEYCODE_K:
            if (event.isShiftPressed()) {
                fireSeekingMissle();
            } else {
                fireMissile();
            }
            return true;
        default:
            return super.onKeyUp(keyCode, event);
    }
}

管理APP內存

參考地址:http://developer.android.com/training/articles/memory.html
內存(RAM)在任何軟件開發中都是非常有價值的資源,在移動開發中尤甚。在Android中不存在交換分區(但使用paging和 memory-mapping(mmapped)管理內存),盡管Dalvik虛擬機有內存回收機制,但關於內存的分配和釋放必須重視的事情,每個App都有限制的可使用內存。
為了讓垃圾回收器能及時回收內存,要避免引起內存洩漏(Memory Leak)
Android如何徹底地清理App內存呢?答案是只能通過解除對象的引用,讓其可以被垃圾回收器回收。

共享內存(Sharing Memory)

為了滿足一切需要,Android嘗試使用多進程共享內存,它采用下面的方式:

每一個進程都是由Zygote進程分支出來。Zygote進程在系統啟動時創建,並加載通用的framework代碼和資源(比如Activity主題)。當啟動一個新的進程時,系統就從Zygote進程分支一個進程出來並加載新的app需要的代碼,這樣從framework分配的RAM page大部分都允許被所有App共享了。 大部分的靜態數據被mmapped到一個進程中。這不僅允許進程之間共享相同的數據,也可以在需要的時候被置換出去。這樣的靜態數據包括:Dalvik代碼(.odex文件),app資源以及傳統的本地代碼so文件。 在很多地方,Android使用明確的分配共享內存區域的方法在多個進程中共享動態RAM。比如cursor緩存使用content provider和客戶端的共享內存。

分配和回收內存

每一個App的 Dalvik heap都是限制的。如果需要的話,可以改大點,但也根據不同設備或系統版本限制大小不一樣。 heap的邏輯大小和heap使用的物理內存大小是不一樣的。當檢查你的app的heap時,Android計算一個叫PSS(Proportional Set Size,計算與其它進程共享的dirty和clean page,但僅僅是在數量上的比例來計算)的值。這個值就是系統認為你占用的物理內存。 Dalvik heap不會heap的壓縮邏輯大小,也就是說Android不會重組heap來釋放空間。Android只會在heap中有未使用的空間時壓縮邏輯堆大小。

限制App的內存

Android為每個App設置了一個嚴格的heap大小的限制。這個精確的heap大小不同的設備不一樣,它決定了app可以使用的堆內存大小。一旦達到這個限制,就會拋出OutOfMemoryError(OOM).異常。我們可以通過getMemoryClass()方法獲取app可以使用的最大內存大小。

切換App

Android不使用交換分區,但保存那些不在前台(不可見)的進程到最近最少使用(least-recent used LRU)的cache中。這樣,在一個App離開後,不會退出,再次啟動時,可以快速的切換到這個app。
LRU中的進程cache會影響系統的整體性能,所以在系統監測到剩余內存很低時,會將LRU中的進程進行殺死,但也會考慮那些特別占用內存的進程。

App應該如何管理內存

盡量少用Service。最好的方式是使用IntentService,它在後台運行,任務結束活會結束自己的生命周期 UI隱藏時釋放內存。釋放UI資源直接影響系統緩存進程的容量,並直接影響用戶體驗的質量。當用戶離開UI,你可以在Activity中實現onTrimMemory()(API 14才有,老版本使用onLowMemory())方法,並監聽TRIM_MEMORY_UI_HIDDEN級別。但也不是必須在onTrimMemory(TRIM_MEMORY_UI_HIDDEN)中釋放UI資源,當Activity恢復時可以馬上恢復。 當內存吃緊時釋放內存。onTrimMemory()方法有幾個級別:TRIM_MEMORY_RUNNING_MODERATERIM_MEMORY_RUNNING_LOWTRIM_MEMORY_RUNNING_CRITICALTRIM_MEMORY_BACKGROUNDTRIM_MEMORY_MODERATETRIM_MEMORY_COMPLETE。 檢查你需要使用多少內存。使用getMemoryClass()方法獲取可以使用的最大內存。當在Manifest中設置Application的屬性largeHeap為true時,App可以分配到更大內存。使用getLargeMemoryClass()方法可以獲取這個更大的內存值。但永遠不要為了解決OOM異常來使用這個屬性分配更多內存。 避免bitmap浪費內存。bitmap在Android中是非常占用內存的資源。

在 Android 2.3.x (API 10)及以前版本,bitmap對象的像素數據是放在本地內存中的而不是堆內存,這樣很難去debug,大部分的堆內存分析工具不能看到本地內存分配。然而,在Android3.0(API 11)開始,bitmap的像素數據分配到了Dalvik heap中,這樣提高了垃圾回收的幾率以及更易調試。

使用優化的數據容器

Android framework中提供了優化的數據容器,比如SparseArraySparseBooleanArrayLongSparseArray。通用的HashMap在內存效率上較低是因為要為每一個映射准備一個單獨的Entry對象。SparseArray更高效是因為它避免了key的自動裝箱操作。

注意內存開銷

枚舉通常需要超過兩倍內存作為靜態常量。你應嚴格避免使用Android的枚舉。 Java類(包括匿名內部類)使用500字節的代碼 每一個類的實例有12-16字節的開銷 將一個Entry put到HashMap中需要額外32字節的開銷

小心代碼抽象

一般,開發人員將抽象簡單地作為一個“良好的編程實踐“,因為抽象可以提高代碼的靈活性和維護性。然而,抽象代價巨大:通常需要大量更多的需要執行的代碼,需要更多的時間和更多的RAM代碼映射到內存中。如果你使用抽象沒有明顯的好處,你應該避免使用他們。

使用超小的*protobufs 序列化數據*

Protocol buffers
是語言中立的,平台無關的,可擴展的機制,由谷歌設計,用於序列化結構化數據。相對於XML,更小,更快,更簡單。如果采用protobufs,那麼整個客戶端都要使用這種數據結構。常規的protobufs會生成冗長的代碼,會引發各種問題,並很占用內存空間

避免依賴注入框架

這些框架往往執行的過程中初始化掃描代碼的注釋,從而需要大量的代碼映射到內存,即使你不需要它。這些映射page分配到clean RAM中,所以Android可以刪除它們,但直到頁面在內存中已經離開很長一段時間才會發生。

剩下的全是理論:
1、小心使用外部lib,魚目混雜
2、優化整體性能
3、使用ProGuard剔除無用代碼
4、最後生成APK時使用zipalign
5、分析你的內存使用情況
6、使用多進程。在Service中設置process屬性將其放到單獨的進程中運行: android:process=":background" />

性能建議

參考地址:http://developer.android.com/training/articles/perf-tips.html
編寫高質量的代碼有2點基本原則:
1、沒必要做的工作盡量不做
2、能不用分配內存的地方盡量不分配

有一些棘手的問題是Android版本分化嚴重,有時在設備A上運行性能良好,在設備B上效果較差,甚至各種設備上的表現總不一致。我們需要讓自己的代碼在各種設備上都性能較好,需要在很多細節上下功夫。

避免創建不必要的對象

大部分Andriod程序是Java開發,和C/C++手動分配和釋放內存不一樣,,Java中有垃圾回收器。但這並不意味著我們可以肆意的創建對象到內存中,而不管這些對象的生命周期。每一次的對象創建,都對垃圾回收器進行垃圾回收是一次觸動,我們的原則是盡量不需要創建的對象就不要創建。比如:
1、我們需要拼接一個Sting字符串,很多人習慣使用多個String就進行相加“+”,這其實會造成多次的String對象創建,這是完全沒必要的。我們使用StringBuffer或者StirngBuilder(非線程安全)進行append,則完全可以避免這種情況發生。
2、平行的一維數組比一個多維數組效率更高。
?int數組比Integer數組要好很多,這適用於其它基本數據類型
?當存儲元組類型(foo,bar)對象時,使用foo[]和bar[]兩個數組要比(foo,bar)好很多。當然設計API給別人訪問時要做出讓步,自己內部使用時,最好使用平行的一維數組

使用靜態方法

當不需要訪問一個對象的內部屬性時,嘗試使用靜態方法。靜態方法比普通方法的調用快15-20%。

使用Static Final修飾常量

static int intVal = 42;
static String strVal = "Hello, world!";

編譯器生成一個類的初始化方法,要調用方法。這個方法在第一次使用時調用,它儲存42到intVal變量中,並從類文件string常量表中提取一個String引用賦給strVal。當這些值在後面被引用時,他們通過字段查找進行訪問。我們使用final關鍵詞改善這種情況:

static final int intVal = 42;
static final String strVal = "Hello, world!";

這樣類不用再調用方法,因為這些常量進入了dex文件的靜態域初始化中。代碼訪問intVal,直接使用42,而對strVal的訪問也相對廉價的string常量命令,因為不用查找字段來獲取了。

注意:這種優化只針對基本數據類型和String常量,不是任意的數據類型。然而,不管何時,使用final static都是好的實踐。

避免內部的Getters/Setters

很多語言都習慣使用get和set方法來訪問對象屬性,這是一個好的習慣。但在Android中,這種方式比直接訪問屬性成員要昂貴很多。在public的接口中,使用get和set時合理的,但在類的內部,直接訪問成員變量比使用簡單的get方法要快很多。不使用JIT,快3倍;使用JIT,快7倍。

使用增強的循環語法

下面是遍歷一個數組的代碼:

static class Foo {
    int mSplat;
}

Foo[] mArray = ...

public void zero() {
    int sum = 0;
    for (int i = 0; i < mArray.length; ++i) {
        sum += mArray[i].mSplat;
    }
}

public void one() {
    int sum = 0;
    Foo[] localArray = mArray;
    int len = localArray.length;

    for (int i = 0; i < len; ++i) {
        sum += localArray[i].mSplat;
    }
}

public void two() {
    int sum = 0;
    for (Foo a : mArray) {
        sum += a.mSplat;
    }
}

zero()方法是最慢的,因為JIT不能避免循環中每一次取數組length的開銷。one()快一點。它將數組賦給一個本地變量,並取了長度。這樣避免了每一次循環都要查找外部變量。two()最快,並且不需要JIT(one()需要JIT)。你應該默認使用增強的循環來遍歷。

使用Package 標記而不是private的內部類

public class Foo {
    private class Inner {
        void stuff() {
            Foo.this.doStuff(Foo.this.mValue);
        }
    }

    private int mValue;

    public void run() {
        Inner in = new Inner();
        mValue = 27;
        in.stuff();
    }

    private void doStuff(int value) {
        System.out.println("Value is " + value);
    }
}

這裡定義了一個私有的內部類(FooInner),並在外部類中調用內部類中的成員和方法,這種情況是合理的。問題在於,虛擬機從FooInner中訪問其外部類Foo的私有成員是非法的,因為Foo和Foo$Inner是不同的類,盡管Java允許這樣訪問。編譯器做了下面的操作:

/*package*/ static int Foo.access$100(Foo foo) {
    return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
    foo.doStuff(value);
}

避免使用浮點型

作為經驗,在Android設備上,浮點型數據比整型數據慢2倍。
從速度上來講,float和double在一般現代機器上看不出區別。但在空間占用上,double要多2倍多。

了解和使用系統庫

使用系統自帶的lib,原因就不用多說了。比如String.indexOf()方法及相關的API,Dalvik 虛擬機有了固有的內聯。同樣,使用System.arraycopy()方法比在Nexus one中運行手寫的循環方法要快9倍。

小心使用本地(Native)方法

使用NDK開發app不一定比Java開發應用更有效率。首先,從java->native有一個過渡,是一個開銷,另外JIT不能跨越這個邊界取優化。如果在native分配各種內存和資源,垃圾回收器是無法回收的。使用native你也需要為各個架構的CPU編譯so庫。
native方法的主要用處是當你有了一個native的代碼庫想移植到Android時才用,而不是為了“加速”某些java代碼用它。

性能誤區

在沒有JIT(Android在2.2上引入的JIT)的設備上,通過一個明確的變量類型的變量來調用方法比使用接口變量更有效率(比如,使用Hashmap map比Map map調用方法要更高效,盡管兩個對象都是Hashmap)。實際上不是2倍的慢,只是6%的慢。此外,JIT使這兩者效率沒有區別。
在沒有JIT的設備上,緩存成員訪問大約比重復直接訪問成員快20%,在JIT模式下,成員訪問開銷和本地訪問差不多相同,所以,不用再去優化這一塊了,沒有價值,除非你感覺它是你的代碼更好讀了。(這也適合final、static、以及final static修飾的成員)

延伸閱讀

Profiling with Traceview and dmtracedump
Analyzing UI Performance with Systrace

保持App的響應避免ANR

參考地址:http://developer.android.com/training/articles/perf-anr.html
ANR是Application Not Responding的簡稱,意思是應用程序無響應。在Android系統中,UI操作超過5秒BroadcastReceiver中操作超過10秒,都會導致ANR,系統會彈出ANR的對話框,app會崩潰。
anr.png

如何避免ANR

Android應用程序默認運行在UI線程(主線程)中,UI線程上的長時間操作很可能導致ANR,因此在UI線程上的操作,要盡可能的輕(結束)。在Activity中的onCreate()和onResume()方法中盡可能進行少的工作。網絡請求、數據庫操作、或者解析bitmap等繁重的計算任務等任務,務必放在子線程中進行。
最有效的方式就是使用AsyncTask來創建工作線程,實現其中的doInBackground()方法。

private class DownloadFilesTask extends AsyncTask {
    // Do the long-running work in here
    protected Long doInBackground(URL... urls) {
        int count = urls.length;
        long totalSize = 0;
        for (int i = 0; i < count; i++) {
            totalSize += Downloader.downloadFile(urls[i]);
            publishProgress((int) ((i / (float) count) * 100));
            // Escape early if cancel() is called
            if (isCancelled()) break;
        }
        return totalSize;
    }

    // This is called each time you call publishProgress()
    protected void onProgressUpdate(Integer... progress) {
        setProgressPercent(progress[0]);
    }

    // This is called when doInBackground() is finished
    protected void onPostExecute(Long result) {
        showNotification("Downloaded " + result + " bytes");
    }
}

//這樣執行
new DownloadFilesTask().execute(url1, url2, url3);

你可能想使用Thread或HandlerThread來做,盡管它們比AsyncTask復雜。如果使用它們的話,你需要調用Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND)來設置後台線程的優先級。如果不設置這個優先級的話,這個線程可能會讓app運行換慢,因為這個線程默認是和Ui線程時一樣的優先級。
當實現Thread或HandlerThread時,在等待工作線程來完成任務時請確保你的UI線程不會阻塞-不要調用Thread.wait()Thread.sleep()。我們使用Handler在多線程中通信。
盡管 BroadcastReceiver中的時間要到10秒才會ANR,我們也要避免在其中做耗時的工作。IntentService是個很好的方案。

提示:可以使用StrictMode 來幫助查找在主線程中潛在的耗時操作,如網絡和數據庫操作。

增強響應

一般,100-200ms是用戶感覺到慢和卡的臨界值。這裡一些建議幫助你避免ANR,讓你的app看起來響應快:

在後台線程執行一個耗時操作時,顯示一個進度條(比如ProgressBar) 針對游戲app,將位置移動的計算放在工作線程 如果你的應用有一個耗時的准備階段,考慮顯示一個splash或顯示一個進度條,盡可能快的渲染主界面。 使用性能工具 SystraceTraceview確定app響應的問題
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved