Android最佳实践之Notification、下拉刷新、内存及性能建议等

来源:互联网 发布:下载模组的软件 编辑:程序博客网 时间:2024/06/17 13:14

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 notificationint mNotificationId = 001;// Gets an instance of the NotificationManager serviceNotificationManager 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:name=".MainActivity"    android:label="@string/app_name" >    <intent-filter>        <action android:name="android.intent.action.MAIN" />        <category android:name="android.intent.category.LAUNCHER" />    </intent-filter></activity><activity    android:name=".ResultActivity"    android:parentActivityName=".MainActivity">    <meta-data        android:name="android.support.PARENT_ACTIVITY"        android:value=".MainActivity"/></activity>

2、创建一个Back Stack:

int id = 1;...Intent resultIntent = new Intent(this, ResultActivity.class);TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);// Adds the back stackstackBuilder.addParentStack(ResultActivity.class);// Adds the Intent to the top of the stackstackBuilder.addNextIntent(resultIntent);// Gets a PendingIntent containing the entire back stackPendingIntent 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选项。比如:

<activity    android:name=".ResultActivity"...    android:launchMode="singleTask"    android:taskAffinity=""    android:excludeFromRecents="true"></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 ActivityIntent notifyIntent =        new Intent(new ComponentName(this, ResultActivity.class));// Sets the Activity to start in a new, empty tasknotifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |         Intent.FLAG_ACTIVITY_CLEAR_TASK);// Creates the PendingIntentPendingIntent notifyIntent =        PendingIntent.getActivity(        this,        0,        notifyIntent,        PendingIntent.FLAG_UPDATE_CURRENT);// Puts the PendingIntent into the notification builderbuilder.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 NotificationManagermNotificationManager.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 updatedint 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就不可见了(被删除了):

  1. 用户单独或通过使用“全部清除”(如果可以清除通知)清除Notification
  2. 当创建Notification时设置setAutoCancel(),用户点击了Notification自动消失
  3. 对指定ID的Notification进行cancel(),这样会取消包括正在运行的通知
  4. 使用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 threadnew 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
图:任务完成,清掉进度条

显示一个持续的活动指示器

要显示一个不确定(持续)的进度条,可以设置setProgress(0, 0, true)。

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

将上面的代码替换成下面的

 // Sets an activity indicator for an operation of indeterminate lengthmBuilder.setProgress(0, 0, true);// Issues the notificationmNotifyManager.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如下:

<android.support.v4.widget.SwipeRefreshLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:id="@+id/swiperefresh"    android:layout_width="match_parent"    android:layout_height="match_parent">    <ListView        android:id="@android:id/list"        android:layout_width="match_parent"        android:layout_height="match_parent" /></android.support.v4.widget.SwipeRefreshLayout>

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

在Action Bar上添加刷新按钮

可以在Action Bar上添加一个刷新按钮,这样用户不用通过下拉手势来刷新数据。比如,可以通过键盘或软键盘来触发刷新事件。
你可以通过android:showAsAction=never设置一个Menu,不要设置成Button,来添加一个刷新“按钮”。

<menu xmlns:android="http://schemas.android.com/apk/res/android" >    <item        android:id="@+id/menu_refresh"        android:showAsAction="never"        android:title="@string/menu_refresh"/></menu>

响应刷新请求

响应刷新手势

响应刷新手势需要实现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. */@Overridepublic 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

<?xml version="1.0" encoding="utf-8"?><menu xmlns:android="http://schemas.android.com/apk/res/android">    <item android:id="@+id/search"          android:title="@string/search_title"          android:icon="@drawable/ic_search"          android:showAsAction="collapseActionView|ifRoom"          android:actionViewClass="android.widget.SearchView" /></menu>

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

@Overridepublic 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 属性提示用户需要在搜索框输入啥。

<?xml version="1.0" encoding="utf-8"?><searchable xmlns:android="http://schemas.android.com/apk/res/android"        android:label="@string/app_name"        android:hint="@string/search_hint" />

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

<activity ... >    ...    <meta-data android:name="android.app.searchable"            android:resource="@xml/searchable" /></activity>

在onCreateOptionsMenu()方法创建之前,将搜索配置与SearchView通过setSearchableInfo(SearchableInfo)进行关联。

@Overridepublic 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 android:name=".SearchResultsActivity" ... >    ...    <intent-filter>        <action android:name="android.intent.action.SEARCH" />    </intent-filter>    ...</activity>

在你的搜索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()中创建表的代码之后:

@Overridepublic 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)。

<uses-sdk android:minSdkVersion="7" android:targetSdkVersion="15" /><application>...

为旧版本设备提供 Search Dialog

在选择搜索菜单时,旧版中调用onSearchRequested()来显示一个搜索对话框。

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

在运行时检查Android的版本

在onCreateOptionsMenu() 中检查Android的版本,判断如果是3.0及以上,则使用SearchView。

@Overridepublic 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    android:id="@+id/phone"    android:layout_width="fill_parent"    android:layout_height="wrap_content"    android:hint="@string/phone_hint"    android:inputType="phone" />

edittext-phone.png
使用textPassword属性值来指定输入密码:

<EditText    android:id="@+id/password"    android:hint="@string/password_hint"    android:inputType="textPassword"    ... />    

ime_password.png

允许拼写建议或其他行为

textAutoCorrect允许你自动纠正拼写错误。下面例子就是让第一个字母大写以及自动纠错的属性:

<EditText    android:id="@+id/message"    android:layout_width="wrap_content"    android:layout_height="wrap_content"    android:inputType=        "textCapSentences|textAutoCorrect"    ... />

ime_autocorrect.png

指定输入法的Action

如果你的TextView不允许多行(多行属性这样设置android:inputType=”textMultiLine”),那么软键盘右下角的按钮显示NextDone。你可以设置更多的Action比如Send或者Go。设置键盘Action的Button,设置android:imeOptions属性即可:

<EditText    android:id="@+id/search"    android:layout_width="fill_parent"    android:layout_height="wrap_content"    android:hint="@string/search_hint"    android:inputType="text"    android:imeOptions="actionSend" />

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即可:

<application ... >    <activity        android:windowSoftInputMode="stateVisible" ... >        ...    </activity>    ...</application>

按需显示输入法

下面的例子时在一个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如何响应输入法显示

  <activity        android:windowSoftInputMode="stateVisible|adjustResize" ... >        ...    </activity>

adjustResize很重要,它会让输入法显示挡住Activity下面的部分时自动上移,露出输入框。

使用Tab键导航

定义focus顺序使用android:nextFocusForward属性。下面的例子,焦点从button1 到editText1 再到button2:

<RelativeLayout ...>    <Button        android:id="@+id/button1"        android:layout_alignParentTop="true"        android:layout_alignParentRight="true"        android:nextFocusForward="@+id/editText1"        ... />    <Button        android:id="@+id/button2"        android:layout_below="@id/button1"        android:nextFocusForward="@+id/button1"        ... />    <EditText        android:id="@id/editText1"        android:layout_alignBottom="@+id/button2"        android:layout_toLeftOf="@id/button2"        android:nextFocusForward="@+id/button2"        ...  />    ...</RelativeLayout>

使用方向键导航

如下例子:

<Button    android:id="@+id/button1"    android:nextFocusRight="@+id/button2"    android:nextFocusDown="@+id/editText1"    ... /><Button    android:id="@id/button2"    android:nextFocusLeft="@id/button1"    android:nextFocusDown="@id/editText1"    ... /><EditText    android:id="@id/editText1"    android:nextFocusUp="@id/button1"    ...  />

处理键盘Action。单个键或加转换键

可以通过实现KeyEvent.Callback接口,拦截或直接处理键盘的事件,而不是默认的让系统处理键盘输入。每一个Activity和View都实现了KeyEvent.Callback接口,我们只需要覆盖其中的方法即可:

@Overridepublic 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键是否按下了,以此来做逻辑处理:

@Overridepublic 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应该如何管理内存

  1. 尽量少用Service。最好的方式是使用IntentService,它在后台运行,任务结束活会结束自己的生命周期
  2. UI隐藏时释放内存。释放UI资源直接影响系统缓存进程的容量,并直接影响用户体验的质量。当用户离开UI,你可以在Activity中实现onTrimMemory()(API 14才有,老版本使用onLowMemory())方法,并监听TRIM_MEMORY_UI_HIDDEN级别。但也不是必须在onTrimMemory(TRIM_MEMORY_UI_HIDDEN)中释放UI资源,当Activity恢复时可以马上恢复。
  3. 当内存吃紧时释放内存。onTrimMemory()方法有几个级别:TRIM_MEMORY_RUNNING_MODERATERIM_MEMORY_RUNNING_LOWTRIM_MEMORY_RUNNING_CRITICALTRIM_MEMORY_BACKGROUNDTRIM_MEMORY_MODERATETRIM_MEMORY_COMPLETE
  4. 检查你需要使用多少内存。使用getMemoryClass()方法获取可以使用的最大内存。当在Manifest中设置Application的属性largeHeap为true时,App可以分配到更大内存。使用getLargeMemoryClass()方法可以获取这个更大的内存值。但永远不要为了解决OOM异常来使用这个属性分配更多内存
  5. 避免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属性将其放到单独的进程中运行:<service android:name=".PlaybackService"
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);    }}

这里定义了一个私有的内部类(FooInnerFooInner中访问其外部类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<URL, Integer, Long> {    // 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 0
原创粉丝点击