#### 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 提高程序的可维护性