#### 2.4.5 使用ContentProvider
**ContentProvider是Android中提供的專門用于不同應用間進行數據共享的方式,從這一點來看,它天生就適合進程間通信**。
**和Messenger一樣,ContentProvider的底層實現同樣也是Binder**,由此可見,**Binder在Android系統中是何等的重要**。
雖然**ContentProvider的底層實現是Binder,但是它的使用過程要比AIDL簡單許多,這是因為系統已經為我們做了封裝,使得我們無須關心底層細節即可輕松實現IPC**。
ContentProvider雖然使用起來很簡單,包括自己創建一個ContentProvider也不是什么難事,盡管如此,**它的細節還是相當多,比如CRUD操作、防止SQL注入和權限控制等**。由于章節主題限制,在本節中,筆者暫時不對ContentProvider的使用細節以及工作機制進行詳細分析,而是為讀者**介紹采用ContentProvider進行跨進程通信的主要流程**,至于使用細節和內部工作機制會在后續章節進行詳細分析。
**系統預置了許多ContentProvider,比如通訊錄信息、日程表信息等,要跨進程訪問這些信息,只需要通過ContentResolver的query、update、insert和delete方法即可**。
* [ ] **實現一個自定義的ContentProvider,來演示進程間通信**
在本節中,我們來**實現一個自定義的ContentProvider,并演示如何在其他應用中獲取ContentProvider中的數據從而實現進程間通信這一目的**。
首先,我們創建一個ContentProvider,名字就叫BookProvider。創建一個自定義的ContentProvider很簡單,**只需要繼承ContentProvider類并實現六個抽象方法即可:onCreate、query、update、insert、delete和getType**。
這六個抽象方法都很好理解,
* **onCreate**代表ContentProvider的創建,一般來說我們**需要做一些初始化工作**;
* **getType用來返回一個Uri請求所對應的MIME類型(媒體類型),比如圖片、視頻等**,這個媒體類型還是有點復雜的,**如果我們的應用不關注這個選項,可以直接在這個方法中返回null或者“*/*”**;
* 剩下的四個方法對應于CRUD操作,即**實現對數據表的增刪改查功能**。
根據Binder的工作原理,我們知道**這六個方法均運行在ContentProvider的進程中,除了onCreate由系統回調并運行在主線程里,其他五個方法均由外界回調并運行在Binder線程池中**,這一點在接下來的例子中可以再次證明。
**ContentProvider主要以表格的形式來組織數據,并且可以包含多個表,對于每個表格來說,它們都具有行和列的層次性,行往往對應一條記錄,而列對應一條記錄中的一個字段,這點和數據庫很類似**。
除了表格的形式,**ContentProvider還支持文件數據,比如圖片、視頻等**。**文件數據和表格數據的結構不同,因此處理這類數據時可以在ContentProvider中返回文件的句柄給外界從而讓文件來訪問ContentProvider中的文件信息**。
**Android系統所提供的MediaStore功能就是文件類型的ContentProvider**,詳細實現可以參考MediaStore。另外,雖然ContentProvider的底層數據看起來很像一個SQLite數據庫,但是**ContentProvider對底層的數據存儲方式沒有任何要求,我們既可以使用SQLite數據庫,也可以使用普通的文件,甚至可以采用內存中的一個對象來進行數據的存儲**,這一點在后續的章節中會再次介紹,所以這里不再深入了。
下面看一個最簡單的示例,它演示了ContentProvider的工作工程。首先創建一個BookProvider類,它繼承自ContentProvider并實現了ContentProvider的六個必須需要實現的抽象方法。
在下面的代碼中,我們什么都沒干,盡管如此,這個BookProvider也是可以工作的,只是它無法向外界提供有效的數據而已。
// ContentProvider.java
public class BookProvider extends ContentProvider {
private static final String TAG = "BookProvider";
@Override
public boolean onCreate() {
Log.d(TAG, "onCreate, current thread:" + Thread.currentThread().
getName());
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Log.d(TAG, "query, current thread:" + Thread.currentThread().
getName());
return null;
}
@Override
public String getType(Uri uri) {
Log.d(TAG, "getType");
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
Log.d(TAG, "insert");
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
Log.d(TAG, "delete");
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
Log.d(TAG, "update");
return 0;
}
}
接著我們需要**注冊這個BookProvider**,如下所示。**其中`android:authorities`是Content-Provider的唯一標識,通過這個屬性外部應用就可以訪問我們的BookProvider,因此,`android:authorities`必須是唯一的**,這里**建議讀者在命名的時候加上包名前綴**。
為了**演示進程間通信**,我們**讓BookProvider運行在獨立的進程中并給它添加了權限,這樣外界應用如果想訪問BookProvider,就必須聲明“`com.ryg.PROVIDER`”這個權限**。
* **ContentProvider的權限**
**ContentProvider的權限還可以細分為讀權限和寫權限,分別對應android:readPermission和android:writePermission屬性,如果分別聲明了讀權限和寫權限,那么外界應用也必須依次聲明相應的權限才可以進行讀/寫操作,否則外界應用會異常終止**。
關于權限這一塊,請讀者自行查閱相關資料,本章不進行詳細介紹。
<provider
android:name=".provider.BookProvider"
android:authorities="com.ryg.chapter_2.book.provider"
android:permission="com.ryg.PROVIDER"
android:process=":provider" >
</provider>
注冊了ContentProvider以后,我們就可以在外部應用中訪問它了。為了方便演示,這里仍然選擇在同一個應用的其他進程中去訪問這個BookProvider,至于在單獨的應用中去訪問這個BookProvider,和同一個應用中訪問的效果是一樣的,讀者可以自行試一下(**注意要聲明對應權限**)。
//ProviderActivity.java
public class ProviderActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_provider);
Uri uri = Uri.parse("content://com.ryg.chapter_2.book.provider");
getContentResolver().query(uri, null, null, null, null);
getContentResolver().query(uri, null, null, null, null);
getContentResolver().query(uri, null, null, null, null);
}
}
在上面的代碼中,我們通過ContentResolver對象的query方法去查詢BookProvider中的數據,**其中“`content://com.ryg.chapter_2.book.provider`”唯一標識了BookProvider**,而這個標識正是我們前面為BookProvider的android:authorities屬性所指定的值。我們運行后看一下log。
從下面log可以看出,**BookProvider中的query方法被調用了三次,并且這三次調用不在同一個線程中。可以看出,它們運行在一個Binder線程中**,前面提到**update、insert和delete方法同樣也運行在Binder線程中**。另外,**onCreate運行在main線程中,也就是UI線程**,所以我們不能在onCreate中做耗時操作。
D/BookProvider(2091): onCreate, current thread:main
D/BookProvider(2091): query, current thread:Binder Thread #2
D/BookProvider(2091): query, current thread:Binder Thread #1
D/BookProvider(2091): query, current thread:Binder Thread #2
D/MyApplication(2091): application start, process name:com.ryg.chapter_
2:provider
到這里,整個ContentProvider的流程我們已經跑通了,雖然ContentProvider中沒有返回任何數據。接下來,在上面的基礎上,我們繼續完善BookProvider,從而使其能夠對外部應用提供數據。
繼續本章提到的那個例子,現在我們要提供一個BookProvider,外部應用可以通過BookProvider來訪問圖書信息,為了更好地演示ContentProvider的使用,用戶還可以通過BookProvider訪問到用戶信息。為了完成上述功能,我們**需要一個數據庫來管理圖書和用戶信息**,這個數據庫不難實現,代碼如下:
~~~
package com.ryg.chapter_2.provider;
public class DbOpenHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "book_provider.db";
public static final String BOOK_TABLE_NAME = "book";
public static final String USER_TALBE_NAME = "user";
private static final int DB_VERSION = 1;
private String CREATE_BOOK_TABLE = "CREATE TABLE IF NOT EXISTS "
+ BOOK_TABLE_NAME + "(_id INTEGER PRIMARY KEY," + "name TEXT)";
private String CREATE_USER_TABLE = "CREATE TABLE IF NOT EXISTS "
+ USER_TALBE_NAME + "(_id INTEGER PRIMARY KEY," + "name TEXT,"
+ "sex INT)";
public DbOpenHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK_TABLE);
db.execSQL(CREATE_USER_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO ignored
}
}
~~~
上述代碼是一個**最簡單的數據庫的實現**,我們**借助SQLiteOpenHelper來管理數據庫的創建、升級和降級**。下面我們就要通過BookProvider向外界提供上述數據庫中的信息了。
我們知道,**ContentProvider通過Uri來區分外界要訪問的的數據集合**,在本例中**支持外界對BookProvider中的book表和user表進行訪問,為了知道外界要訪問的是哪個表,我們需要為它們定義單獨的Uri和Uri_Code(一個對應book表一個對應user表),并將Uri和對應的Uri_Code相關聯**,我們可以**使用UriMatcher的addURI方法將Uri和Uri_Code關聯到一起**。
這樣,**當外界請求訪問BookProvider時,我們就可以根據請求的Uri來得到Uri_Code,有了Uri_Code我們就可以知道外界想要訪問哪個表,然后就可以進行相應的數據操作了**,具體代碼如下所示。
public class BookProvider extends ContentProvider {
private static final String TAG = "BookProvider";
public static final String AUTHORITY = "com.ryg.chapter_2.book.
provider";
public static final Uri BOOK_CONTENT_URI = Uri.parse("content://"
+ AUTHORITY + "/book");
public static final Uri USER_CONTENT_URI = Uri.parse("content://"
+ AUTHORITY + "/user");
public static final int BOOK_URI_CODE = 0;
public static final int USER_URI_CODE = 1;
private static final UriMatcher sUriMatcher = new UriMatcher(
UriMatcher.NO_MATCH);
static {
sUriMatcher.addURI(AUTHORITY, "book", BOOK_URI_CODE);
sUriMatcher.addURI(AUTHORITY, "user", USER_URI_CODE);
}
...
}
從上面代碼可以看出,我們**分別為book表和user表指定了Uri**,
分別為“`content://com.ryg.chapter_2.book.provider/book`”和
“`content://com.ryg.chapter_2.book. provider/user`”,
**這兩個Uri所關聯的Uri_Code分別為0和1**。這個關聯過程是通過下面的語句來完成的:
```
sUriMatcher.addURI(AUTHORITY, "book", BOOK_URI_CODE);
sUriMatcher.addURI(AUTHORITY, "user", USER_URI_CODE);
```
將Uri和Uri_Code管理以后,我們就可以**通過如下方式來獲取外界所要訪問的數據源,根據Uri先取出Uri_Code,根據Uri_Code再得到數據表的名稱,知道了外界要訪問的表**,接下來就可以響應外界的增刪改查請求了。
~~~
private String getTableName(Uri uri) {
String tableName = null;
switch (sUriMatcher.match (uri)) {
case BOOK_URI_CODE:
tableName = DbOpenHelper.BOOK_TABLE_NAME;
break;
case USER_URI_CODE:
tableName = DbOpenHelper.USER_TALBE_NAME;
break;
default:
break;
}
return tableName;
}
~~~
接著,我們就可以**實現query、update、insert、delete方法了**。
如下是query方法的實現,首先我們要從Uri中取出外界要訪問的表的名稱,然后根據外界傳遞的查詢參數就可以進行數據庫的查詢操作了,這個過程比較簡單。
~~~
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Log.d (TAG, "query, current thread:" + Thread.currentThread ().getName ());
String table = getTableName (uri);
if (table == null) {
throw new IllegalArgumentException ("Unsupported URI: " + uri);
}
return mDb.query (table, projection, selection, selectionArgs, null, null, sortOrder, null);
}
~~~
另外三個方法的實現思想和query是類似的,**只有一點不同,那就是update、insert和delete方法會引起數據源的改變,這個時候我們需要通過ContentResolver的notifyChange方法來通知外界當前ContentProvider中的數據已經發生改變**。
要**觀察一個ContentProvider中的數據改變情況,可以通過ContentResolver的registerContentObserver方法來注冊觀察者,通過unregisterContentObserver方法來解除觀察者**。
對于這三個方法,這里不再詳細解釋了,BookProvider的完整代碼如下:
~~~
package com.ryg.chapter_2.provider;
public class BookProvider extends ContentProvider {
private static final String TAG = "BookProvider";
public static final String AUTHORITY = "com.ryg.chapter_2.book.provider";
public static final Uri BOOK_CONTENT_URI = Uri.parse ("content://"
+ AUTHORITY + "/book");
public static final Uri USER_CONTENT_URI = Uri.parse ("content://"
+ AUTHORITY + "/user");
public static final int BOOK_URI_CODE = 0;
public static final int USER_URI_CODE = 1;
private static final UriMatcher sUriMatcher = new UriMatcher (
UriMatcher.NO_MATCH);
public static final String METHOD_ADD_BOOK = "addBook";
public static final String METHOD_GET_BOOK = "getBook";
private List<Book> mPrivateBooks = new ArrayList<Book> ();
static {
sUriMatcher.addURI (AUTHORITY, "book", BOOK_URI_CODE);
sUriMatcher.addURI (AUTHORITY, "user", USER_URI_CODE);
}
private Context mContext;
private SQLiteDatabase mDb;
@Override
public boolean onCreate() {
Log.d (TAG, "onCreate, current thread:"
+ Thread.currentThread ().getName ());
mContext = getContext ();
//ContentProvider創建時,初始化數據庫。注意:這里僅僅是為了演示,實際使用中不推薦在主線程中進行耗時的數據庫操作
initProviderData ();
return true;
}
private void initProviderData() {
mDb = new DbOpenHelper (mContext).getWritableDatabase ();
mDb.execSQL ("delete from " + DbOpenHelper.BOOK_TABLE_NAME);
mDb.execSQL ("delete from " + DbOpenHelper.USER_TALBE_NAME);
mDb.execSQL ("insert into book values(3,'Android');");
mDb.execSQL ("insert into book values(4,'Ios');");
mDb.execSQL ("insert into book values(5,'Html5');");
mDb.execSQL ("insert into user values(1,'jake',1);");
mDb.execSQL ("insert into user values(2,'jasmine',0);");
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Log.d (TAG, "query, current thread:" + Thread.currentThread ().getName ());
String table = getTableName (uri);
if (table == null) {
throw new IllegalArgumentException ("Unsupported URI: " + uri);
}
return mDb.query (table, projection, selection, selectionArgs, null, null, sortOrder, null);
}
@Override
public String getType(Uri uri) {
Log.d (TAG, "getType");
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
Log.d (TAG, "insert");
String table = getTableName (uri);
if (table == null) {
throw new IllegalArgumentException ("Unsupported URI: " + uri);
}
mDb.insert (table, null, values);
mContext.getContentResolver ().notifyChange (uri, null);
return uri;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
Log.d (TAG, "delete");
String table = getTableName (uri);
if (table == null) {
throw new IllegalArgumentException ("Unsupported URI: " + uri);
}
int count = mDb.delete (table, selection, selectionArgs);
if (count > 0) {
getContext ().getContentResolver ().notifyChange (uri, null);
}
return count;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
Log.d (TAG, "update");
String table = getTableName (uri);
if (table == null) {
throw new IllegalArgumentException ("Unsupported URI: " + uri);
}
int row = mDb.update (table, values, selection, selectionArgs);
if (row > 0) {
getContext ().getContentResolver ().notifyChange (uri, null);
}
return row;
}
private String getTableName(Uri uri) {
String tableName = null;
switch (sUriMatcher.match (uri)) {
case BOOK_URI_CODE:
tableName = DbOpenHelper.BOOK_TABLE_NAME;
break;
case USER_URI_CODE:
tableName = DbOpenHelper.USER_TALBE_NAME;
break;
default:
break;
}
return tableName;
}
/**
* @param method 只是一個標記,它不是具體的一個方法名,可以使任意值
* @param arg 沒有時,為null
* @param extras
* @return
*/
@Override
public Bundle call(String method, String arg, Bundle extras) {
Bundle bundle = null;
String msg = String.format ("call method [%s] in %s", method, Thread.currentThread ().getName ());
Log.d (TAG, msg);
Log.d (TAG, "arg:" + arg + "bundle:" + extras);
if (method.equals (METHOD_ADD_BOOK)) {
extras.setClassLoader (Book.class.getClassLoader ());
Book book = extras.getParcelable ("book");
mPrivateBooks.add (book);
} else if (method.equals (METHOD_GET_BOOK)) {
bundle = new Bundle ();
int bookId = extras.getInt ("_id");
for (Book book : mPrivateBooks) {
if (book.bookId == bookId) {
bundle.putParcelable ("book", book);
break;
}
}
}
return bundle;
}
}
~~~
>[info]需要注意的是,**query、update、insert、delete四大方法是存在多線程并發訪問的,因此方法內部要做好線程同步**。
在本例中,**由于采用的是SQLite并且只有一個SQLiteDatabase的連接,所以可以正確應對多線程的情況。具體原因是SQLiteDatabase內部對數據庫的操作是有同步處理的,但是如果通過多個SQLiteDatabase對象來操作數據庫就無法保證線程同步,因為SQLiteDatabase對象之間無法進行線程同步**。
**如果ContentProvider的底層數據集是一塊內存的話,比如是List,在這種情況下同List的遍歷、插入、刪除操作就需要進行線程同步,否則就會引起并發錯誤**,這點是尤其需要注意的。
到這里BookProvider已經實現完成了,接著我們在外部訪問一下它,看看是否能夠正常工作。
~~~
package com.ryg.chapter_2.provider;
public class ProviderActivity extends Activity {
private static final String TAG = "ProviderActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate (savedInstanceState);
setContentView (R.layout.activity_provider);
// Uri uri = Uri.parse("content://com.ryg.chapter_2.book.provider");
// getContentResolver().query(uri, null, null, null, null);
// getContentResolver().query(uri, null, null, null, null);
// getContentResolver().query(uri, null, null, null, null);
Uri bookUri = Uri.parse ("content://com.ryg.chapter_2.book.provider/book");
ContentValues values = new ContentValues ();
values.put ("_id", 6);
values.put ("name", "程序設計的藝術");
getContentResolver ().insert (bookUri, values);
Cursor bookCursor = getContentResolver ().query (bookUri, new String[]{"_id", "name"}, null, null, null);
while (bookCursor.moveToNext ()) {
Book book = new Book ();
book.bookId = bookCursor.getInt (0);
book.bookName = bookCursor.getString (1);
Log.d (TAG, "query book:" + book.toString ());
}
bookCursor.close ();
Uri userUri = Uri.parse ("content://com.ryg.chapter_2.book.provider/user");
Cursor userCursor = getContentResolver ().query (userUri, new String[]{"_id", "name", "sex"}, null, null, null);
while (userCursor.moveToNext ()) {
User user = new User ();
user.userId = userCursor.getInt (0);
user.userName = userCursor.getString (1);
user.isMale = userCursor.getInt (2) == 1;
Log.d (TAG, "query user:" + user.toString ());
}
userCursor.close ();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
Uri specialBookUri = Uri.parse ("content://com.ryg.charter_2.book.provider");
Bundle bundle = new Bundle ();
bundle.putInt ("_id", 2);
Bundle getResult = getContentResolver ().call (specialBookUri, BookProvider.METHOD_GET_BOOK, "arg_1", bundle);
getResult.setClassLoader (Book.class.getClassLoader ());
Log.d (TAG, "get book with id 2:" + getResult.getParcelable ("book"));
Bundle addBookBundle = new Bundle ();
addBookBundle.putParcelable ("book", new Book (0, "數學之美"));
Bundle setResult = getContentResolver ().call (specialBookUri, BookProvider.METHOD_ADD_BOOK, "arg_2", addBookBundle);
Log.d (TAG, "add book with id 0:" + setResult);
bundle.putInt ("_id", 0);
Bundle getResult0 = getContentResolver ().call (specialBookUri, BookProvider.METHOD_GET_BOOK, "arg_1", bundle);
getResult0.setClassLoader (Book.class.getClassLoader ());
Log.d (TAG, "get book with id 0:" + getResult0.getParcelable ("book"));
}
}
}
~~~
默認情況下,BookProvider的數據庫中有三本書和兩個用戶,在上面的代碼中,我們首先添加一本書:“程序設計的藝術”。接著查詢所有的圖書,這個時候應該查詢出四本書,因為我們剛剛添加了一本。然后查詢所有的用戶,這個時候應該查詢出兩個用戶。是不是這樣呢?我們運行一下程序,看一下log。
D/BookProvider( 1127): insert
D/BookProvider( 1127): query, current thread:Binder Thread #1
D/ProviderActivity( 1114): query book:[bookId:3, bookName:Android]
D/ProviderActivity( 1114): query book:[bookId:4, bookName:Ios]
D/ProviderActivity( 1114): query book:[bookId:5, bookName:Html5]
D/ProviderActivity( 1114): query book:[bookId:6, bookName:程序設計的藝術]
D/MyApplication( 1127): application start, process name:com.ryg.chapter_
2:provider
D/BookProvider( 1127): query, current thread:Binder Thread #3
D/ProviderActivity( 1114): query user:User:{userId:1, userName:jake,
isMale:true}, with child:{null}
D/ProviderActivity( 1114): query user:User:{userId:2, userName:jasmine,
isMale:false}, with child:{null}
從上述log可以看到,我們的確查詢到了4本書和2個用戶,這說明BookProvider已經能夠正確地處理外部的請求了,讀者可以自行驗證一下update和delete操作,這里就不再驗證了。同時,**由于ProviderActivity和BookProvider運行在兩個不同的進程中,因此,這也構成了進程間的通信**。
**ContentProvider除了支持對數據源的增刪改查這四個操作,還支持自定義調用,這個過程是通過ContentResolver的Call方法和ContentProvider的Call方法來完成的**。
關于使用ContentProvider來進行IPC就介紹到這里,ContentProvider本身還有一些細節這里并沒有介紹,讀者可以自行了解,本章側重的是各種進程間通信的方法以及它們的區別,因此針對某種特定的方法可能不會介紹得面面俱到。另外,ContentProvider在后續章節還會有進一步的講解,主要包括細節問題和工作原理,讀者可以閱讀后面的相應章節。
- 前言
- 第1章 Activity的生命周期和啟動模式
- 1.1 Activity的生命周期全面分析
- 1.1.1 典型情況下的生命周期分析
- 1.1.2 異常情況下的生命周期分析
- 1.2 Activity的啟動模式
- 1.2.1 Activity的LaunchMode
- 1.2.2 Activity的Flags
- 1.3 IntentFilter的匹配規則
- 第2章 IPC機制
- 2.1 Android IPC簡介
- 2.2 Android中的多進程模式
- 2.2.1 開啟多進程模式
- 2.2.2 多進程模式的運行機制
- 2.3 IPC基礎概念介紹
- 2.3.1 Serializable接口
- 2.3.2 Parcelable接口
- 2.3.3 Binder
- 2.4 Android中的IPC方式
- 2.4.1 使用Bundle
- 2.4.2 使用文件共享
- 2.4.3 使用Messenger
- 2.4.4 使用AIDL
- 2.4.5 使用ContentProvider
- 2.4.6 使用Socket
- 2.5 Binder連接池
- 2.6 選用合適的IPC方式
- 第3章 View的事件體系
- 3.1 View基礎知識
- 3.1.1 什么是View
- 3.1.2 View的位置參數
- 3.1.3 MotionEvent和TouchSlop
- 3.1.4 VelocityTracker、GestureDetector和Scroller
- 3.2 View的滑動
- 3.2.1 使用scrollTo/scrollBy
- 3.2.2 使用動畫
- 3.2.3 改變布局參數
- 3.2.4 各種滑動方式的對比
- 3.3 彈性滑動
- 3.3.1 使用Scroller7
- 3.3.2 通過動畫
- 3.3.3 使用延時策略
- 3.4 View的事件分發機制
- 3.4.1 點擊事件的傳遞規則
- 3.4.2 事件分發的源碼解析
- 3.5 View的滑動沖突
- 3.5.1 常見的滑動沖突場景
- 3.5.2 滑動沖突的處理規則
- 3.5.3 滑動沖突的解決方式
- 第4章 View的工作原理
- 4.1 初識ViewRoot和DecorView
- 4.2 理解MeasureSpec
- 4.2.1 MeasureSpec
- 4.2.2 MeasureSpec和LayoutParams的對應關系
- 4.3 View的工作流程
- 4.3.1 measure過程
- 4.3.2 layout過程
- 4.3.3 draw過程
- 4.4 自定義View
- 4.4.1 自定義View的分類
- 4.4.2 自定義View須知
- 4.4.3 自定義View示例
- 4.4.4 自定義View的思想
- 第5章 理解RemoteViews
- 5.1 RemoteViews的應用
- 5.1.1 RemoteViews在通知欄上的應用
- 5.1.2 RemoteViews在桌面小部件上的應用
- 5.1.3 PendingIntent概述
- 5.2 RemoteViews的內部機制
- 5.3 RemoteViews的意義
- 第6章 Android的Drawable
- 6.1 Drawable簡介
- 6.2 Drawable的分類
- 6.2.1 BitmapDrawable2
- 6.2.2 ShapeDrawable
- 6.2.3 LayerDrawable
- 6.2.4 StateListDrawable
- 6.2.5 LevelListDrawable
- 6.2.6 TransitionDrawable
- 6.2.7 InsetDrawable
- 6.2.8 ScaleDrawable
- 6.2.9 ClipDrawable
- 6.3 自定義Drawable
- 第7章 Android動畫深入分析
- 7.1 View動畫
- 7.1.1 View動畫的種類
- 7.1.2 自定義View動畫
- 7.1.3 幀動畫
- 7.2 View動畫的特殊使用場景
- 7.2.1 LayoutAnimation
- 7.2.2 Activity的切換效果
- 7.3 屬性動畫
- 7.3.1 使用屬性動畫
- 7.3.2 理解插值器和估值器 /
- 7.3.3 屬性動畫的監聽器
- 7.3.4 對任意屬性做動畫
- 7.3.5 屬性動畫的工作原理
- 7.4 使用動畫的注意事項
- 第8章 理解Window和WindowManager
- 8.1 Window和WindowManager
- 8.2 Window的內部機制
- 8.2.1 Window的添加過程
- 8.2.2 Window的刪除過程
- 8.2.3 Window的更新過程
- 8.3 Window的創建過程
- 8.3.1 Activity的Window創建過程
- 8.3.2 Dialog的Window創建過程
- 8.3.3 Toast的Window創建過程
- 第9章 四大組件的工作過程
- 9.1 四大組件的運行狀態
- 9.2 Activity的工作過程
- 9.3 Service的工作過程
- 9.3.1 Service的啟動過程
- 9.3.2 Service的綁定過程
- 9.4 BroadcastReceiver的工作過程
- 9.4.1 廣播的注冊過程
- 9.4.2 廣播的發送和接收過程
- 9.5 ContentProvider的工作過程
- 第10章 Android的消息機制
- 10.1 Android的消息機制概述
- 10.2 Android的消息機制分析
- 10.2.1 ThreadLocal的工作原理
- 10.2.2 消息隊列的工作原理
- 10.2.3 Looper的工作原理
- 10.2.4 Handler的工作原理
- 10.3 主線程的消息循環
- 第11章 Android的線程和線程池
- 11.1 主線程和子線程
- 11.2 Android中的線程形態
- 11.2.1 AsyncTask
- 11.2.2 AsyncTask的工作原理
- 11.2.3 HandlerThread
- 11.2.4 IntentService
- 11.3 Android中的線程池
- 11.3.1 ThreadPoolExecutor
- 11.3.2 線程池的分類
- 第12章 Bitmap的加載和Cache
- 12.1 Bitmap的高效加載
- 12.2 Android中的緩存策略
- 12.2.1 LruCache
- 12.2.2 DiskLruCache
- 12.2.3 ImageLoader的實現446
- 12.3 ImageLoader的使用
- 12.3.1 照片墻效果
- 12.3.2 優化列表的卡頓現象
- 第13章 綜合技術
- 13.1 使用CrashHandler來獲取應用的crash信息
- 13.2 使用multidex來解決方法數越界
- 13.3 Android的動態加載技術
- 13.4 反編譯初步
- 13.4.1 使用dex2jar和jd-gui反編譯apk
- 13.4.2 使用apktool對apk進行二次打包
- 第14章 JNI和NDK編程
- 14.1 JNI的開發流程
- 14.2 NDK的開發流程
- 14.3 JNI的數據類型和類型簽名
- 14.4 JNI調用Java方法的流程
- 第15章 Android性能優化
- 15.1 Android的性能優化方法
- 15.1.1 布局優化
- 15.1.2 繪制優化
- 15.1.3 內存泄露優化
- 15.1.4 響應速度優化和ANR日志分析
- 15.1.5 ListView和Bitmap優化
- 15.1.6 線程優化
- 15.1.7 一些性能優化建議
- 15.2 內存泄露分析之MAT工具
- 15.3 提高程序的可維護性