Android系统源码分析之-ContentProvider
距离上一次写博客已经半年多了,这半年发生了很多事情,也有了很多感触,最主要是改变了忙碌了工作,更加重视身体的健康,为此也把工作地点从深圳这个一线城市换到了珠海,工作相对没有那么累,身体感觉也好了很多。所以在工作完成之余,也有了更多的时间来自我学习和提高,后续会用更多时间来写更多实用的东西,帮助我们理解安卓系统的原理,帮助我们快速、稳定的开发。
这一篇我们接着之前的计划,完成四大组件的最后一个ContentProvider的分析。ContentProvider是一个抽象类,用来提供访问数据的统一格式的接口。ContentProvider的作用是多应用间共享数据,如果用户需要直接使用则可以直接在里面使用数据库来保存数据,也可以通用ContentResolver使用URI来存储数据,使用URI用户不需要知道内部怎么存储只需要知道如何使用该存储方式。
ContentProvider
ContentProvider的描述及使用:
在之前我们分析过ContentProvider的启动比Application的启动早,所以使用时需要知道这种情况。在使用ContentResolver时是通过URI来访问的,URI的结构:content://cn.codemx.myprovider/item/123,我们划分一下:
[ 1 ][ 2 ][ 3 ][ 4 ]
[content://][cn.codemx.myprovider.settings][/item][/123]
- 第一个组件:是一个协议名称,它的值固定为:“content://”,是Content Provider组件的专用访问协议
- 第二个组件:是一个Content Provider组件的android:authority属性值。这个组件类似于URL中的域名,因此我们要保证它是全局唯一的,一般使用它所描述的ContentProvider组件的包名来命名。
- 第三个组件:是一个资源相对路径,用来描述要访问的资源类型。如果一个ContentProvider只有一种资源,那么忽略这个组件,否则通过它来指定要访问的资源的类型。
- 第四个组件:是一个资源ID,用来描述具体的资源。
举个例子:
<provider
android:name="com.android.launcher3.LauncherProvider"
android:authorities="cn.codemx.myprovider.settings"
android:exported="true"
android:readPermission="cn.codemx.myprovider.permission.READ_SETTINGS"
android:writePermission="cn.codemx.myprovider.permission.WRITE_SETTINGS"/>
我有个LauncherProvider继承ContentProvider,在AndroidManifest.xml中要像上面一样声明,其中authorities是位置认证,在使用时是这样使用的:
public static final String TABLE_NAME = "favorites";
/**
* The content:// style URL for this table
*/
public static final Uri CONTENT_URI = Uri.parse("content://" +
ProviderConfig.AUTHORITY + "/" + TABLE_NAME);
其中ProviderConfig.AUTHORITY为:
public class ProviderConfig {
public static final String AUTHORITY = "cn.codemx.myprovider.settings".intern();
}
这个就是上面AndroidManifest.xml中声明的那个authorities属性值。上面获取了Uri就可以通过Uri来获取或者保存数据了。
ContentProvider启动过程:
- Activity通过MyContentProvider.CONTENT_URI来访问MyContentProvider组件,以便可以获得它的内容
- Activity组件所运行在的应用程序进程发现它里面不存在一个用来访问MyContentProvider组件的代理对象,于是通过MyContentProvider.CONTENT_URI来请求AMS返回一个用来访问MyContentProvider组件的代理对象
- AMS发现MyContentProvider还没有起来,于是先创建一个新的应用进程,然后在这个新创建的应用进程中启动MyContentProvider组件
- MyContentProvider组件启动之后,就会将自己发布到AMS中,以便AMS可以将它的一个代理对象返回Activity组件。
ContentProvider方法:
onCreate() // 执行初始化工作;
insert(Uri, ContentValues) // 插入新数据;
delete(Uri, String, String[]) // 删除已有数据;
update(Uri, ContentValues, String, String[]) // 更新数据;
query(Uri, String[], String, String[], String) // 查询数据;
getType(Uri) // 获取数据MIME类型。
ContentProvider不像Activity一样有生命周期,只有一个onCreate方法用来创建。然后就是提供了增、删、改、查用来操作数据的接口,通过Uri来管理数据。
ContentResolver的获取:
上面我们提到ContentProvider主要是提供接口,并没有具体实现,如果需要具体实现,一种是通过操作数据库,一种是通过URI操作ContentResolver,数据库的这里不再介绍,只介绍一下ContentResolver。
通常我们获取ContentResolver的代码如下:
ContentResolver cr = context.getContentResolver(); //获取ContentResolver
前面Android系统源码分析--Context章节我们分析过Context的子类及继承关系,知道,Context的所有实现都是在ContextImp中,所以context.getContentResolver()方法的实现也是一样。
ContextImpl.getContentResolver():
@Override
public ContentResolver getContentResolver() {
return mContentResolver;
}
mContentResolver是定义在ContextImpl中类型为ApplicationContentResolver的变量:
private final ApplicationContentResolver mContentResolver;
这里我们可以知道我们获取到的ContentResolver实际上是ApplicationContentResolver,所以具体操作应该都是在ApplicationContentResolver中实现的。我们先看看这个mContentResolver是在哪里初始化的。
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
...
mContentResolver = new ApplicationContentResolver(this, mainThread, user);
}
上面代码可以知道这个是在ContextImpl构造函数中初始化的。而ContentResolver中的所有操作都在ApplicationContentResolver实现。
ContentResolver.query
先看时序图:
![](https://img.haomeiwen.com/i543562/af6832af6a080738.jpg)
1.ContentResolver.query
public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
@Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder,
@Nullable CancellationSignal cancellationSignal) {
Preconditions.checkNotNull(uri, "uri");
// 返回的是ContentProvider的中的mTransport(Transport)
IContentProvider unstableProvider = acquireUnstableProvider(uri);
if (unstableProvider == null) {
return null;
}
IContentProvider stableProvider = null;
Cursor qCursor = null;
try {
...
try {
qCursor = unstableProvider.query(mPackageName, uri, projection,
selection, selectionArgs, sortOrder, remoteCancellationSignal);
} catch (DeadObjectException e) {
// The remote process has died... but we only hold an unstable
// reference though, so we might recover!!! Let's try!!!!
// This is exciting!!1!!1!!!!1
unstableProviderDied(unstableProvider);
// 返回的是ContentProvider的中的mTransport(Transport)
stableProvider = acquireProvider(uri);
if (stableProvider == null) {
return null;
}
qCursor = stableProvider.query(mPackageName, uri, projection,
selection, selectionArgs, sortOrder, remoteCancellationSignal);
}
if (qCursor == null) {
return null;
}
...
// Wrap the cursor object into CursorWrapperInner object.
final IContentProvider provider = (stableProvider != null) ? stableProvider
: acquireProvider(uri);
final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);
stableProvider = null;
qCursor = null;
return wrapper;
} catch (RemoteException e) {
...
return null;
} finally {
// 释放资源
...
}
}
首先是判断Uri是否是空,如果空抛出异常,如果不是获取unstableProvider,从名字看是不稳定的Provider,往下看还有个stableProvider(稳定的Provider),其实从代码看没有区别,只是第一次获取是不稳定的,出现异常就再次获取就是稳定的。所以我们只看一个即可。从注释可以知道unstableProvider其实是ContentProvider中的Transport对象mTransport,这个我们先提出来,后面通过代码看是不是对的。
2.ContentResolver.acquireUnstableProvider
public final IContentProvider acquireUnstableProvider(Uri uri) {
// uri以"content"开头
if (!SCHEME_CONTENT.equals(uri.getScheme())) {
return null;
}
// 获取Manifest文件中的authorities属性(例如:cn.codemx.myprovider.settings)
String auth = uri.getAuthority();
if (auth != null) {
// 具体实现ApplicationContentResolver中
return acquireUnstableProvider(mContext, uri.getAuthority());
}
return null;
}
这里主要是验证协议名是不是content,然后验证属性是不是存在,如果存在则开始获取实现IContentProvider接口的对象。
3.ApplicationContentResolver.acquireUnstableProvider
protected IContentProvider acquireUnstableProvider(Context c, String auth) {
return mMainThread.acquireProvider(c,
ContentProvider.getAuthorityWithoutUserId(auth),
resolveUserIdFromAuthority(auth), false);
}
这里调用的是ActivityThread.acquireProvider方法。
4.ActivityThread.acquireProvider
public final IContentProvider acquireProvider(
Context c, String auth, int userId, boolean stable) {
// 返回的是ContentProvider的中的mTransport(Transport)
final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);
if (provider != null) {// 如果不为空,说明访问过,如果是第一次访问,为空
return provider;
}
...
IActivityManager.ContentProviderHolder holder = null;
try {
// 核心部分
holder = ActivityManagerNative.getDefault().getContentProvider(
getApplicationThread(), auth, userId, stable);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
if (holder == null) {
Slog.e(TAG, "Failed to find provider info for " + auth);
return null;
}
// Install provider will increment the reference count for us, and break
// any ties in the race.
holder = installProvider(c, holder, holder.info,
true /*noisy*/, holder.noReleaseNeeded, stable);
return holder.provider;
}
先根据auth获取是否存在实现IContentProvider的类,如果存在直接返回,不存在则获取IActivityManager.ContentProviderHolder对象,然后获取实现IContentProvider接口的类。我们先看acquireExistingProvider方法。
5.ActivityThread.acquireExistingProvider
public final IContentProvider acquireExistingProvider(
Context c, String auth, int userId, boolean stable) {
synchronized (mProviderMap) {
final ProviderKey key = new ProviderKey(auth, userId);
// mProviderMap保存当前应用程序进程访问过的ContentProvider组件代理对象
final ProviderClientRecord pr = mProviderMap.get(key);
if (pr == null) {// 如果不存在说明还没有访问过
return null;
}
// 返回的是ContentProvider的中的mTransport(Transport)
IContentProvider provider = pr.mProvider;
IBinder jBinder = provider.asBinder();
// 检查线程是否还存在
if (!jBinder.isBinderAlive()) {
...
return null;
}
// Only increment the ref count if we have one. If we don't then the
// provider is not reference counted and never needs to be released.
ProviderRefCount prc = mProviderRefCountMap.get(jBinder);
if (prc != null) {
// 增加引用计数
incProviderRefLocked(prc, stable);
}
return provider;
}
}
首先是去缓存中获取,是否存在封装了ContentProvider对象的ProviderClientRecord对象,如果不存在则说明第一次启动,还没添加到缓存中,如果存在,那么说明已经启动过,则会有需要的实现IContentProvider接口的对象。再看第4步中如果返回对象不为空则直接返回,如果是空则要先获取IActivityManager.ContentProviderHolder。
6.ActivityManagerProxy.getContentProvider
public ContentProviderHolder getContentProvider(IApplicationThread caller,
String name, int userId, boolean stable) throws RemoteException {
...
// 通过Binder代理对象mRemote发送一个类型为GET_CONTENT_PROVIDER_TRANSACTION的进程间通信消息
mRemote.transact(GET_CONTENT_PROVIDER_TRANSACTION, data, reply, 0);
...
return cph;
}
这里我们在前面几章涉及到了很多次,就不再说了,主要是从这里调用AMS(ActivityManagerService)中的对应方法getContentProvider
7.AMS.getContentProvider
public final ContentProviderHolder getContentProvider(
IApplicationThread caller, String name, int userId, boolean stable) {
...
return getContentProviderImpl(caller, name, null, stable, userId);
}
这里比较简单,调用AMS.getContentProviderImpl方法。
8.AMS.getContentProviderImpl
private ContentProviderHolder getContentProviderImpl(IApplicationThread caller,
String name, IBinder token, boolean stable, int userId) {
ContentProviderRecord cpr;
ContentProviderConnection conn = null;
ProviderInfo cpi = null;
synchronized (this) {
// 描述进程信息的对象
ProcessRecord r = null;
if (caller != null) {// 如果调用者的线程存在,根据caller获取对应的进程
r = getRecordForAppLocked(caller);
// 如果找不到caller对应的进程抛出异常
...
}
...
// 根据名称和userId获取ContentProviderRecord对象,用来检测是不是ContentProvider已经发布了
cpr = mProviderMap.getProviderByName(name, userId);
// If that didn't work, check if it exists for user 0 and then
// verify that it's a singleton provider before using it.
if (cpr == null && userId != UserHandle.USER_SYSTEM) {// 如果不存在并且不是系统的userId
// 检测mProviderMap中是否存在系统的ContentProviderRecord对象,如果已经启动则存在
cpr = mProviderMap.getProviderByName(name, UserHandle.USER_SYSTEM);
if (cpr != null) {
cpi = cpr.info;
if (isSingleton(cpi.processName, cpi.applicationInfo,
cpi.name, cpi.flags)
&& isValidSingletonCall(r.uid, cpi.applicationInfo.uid)) {
...
} else {
cpr = null;
cpi = null;
}
}
}
// 检查ContentProvider是否正在运行(第一次还没有运行)
boolean providerRunning = cpr != null && cpr.proc != null && !cpr.proc.killed;
if (providerRunning) {// ContentProvider已经运行
...
if (r != null && cpr.canRunHere(r)) {// 进程存在
ContentProviderHolder holder = cpr.newHolder(null);
holder.provider = null;
return holder;
}
...
// provider实例已经存在,因此我们直接返回
conn = incProviderCountLocked(r, cpr, token, stable);
// 如何stable和unstable的总引用计数为1,那么更新LruProcess列表
if (conn != null && (conn.stableCount + conn.unstableCount) == 1) {
if (cpr.proc != null && r.setAdj <= ProcessList.PERCEPTIBLE_APP_ADJ) {
...
updateLruProcessLocked(cpr.proc, false, null);
}
}
final int verifiedAdj = cpr.proc.verifiedAdj;
// 更新provider进程的adj
boolean success = updateOomAdjLocked(cpr.proc);
...
if (!success) {// 如果更新不成功,那么减少引用计数并杀死provider进程
...
ppDiedLocked(cpr.proc);// 杀死进程
...
// 已经杀死了,所以要恢复初始状态
providerRunning = false;
conn = null;
} else {// 更新成功了
cpr.proc.verifiedAdj = cpr.proc.setAdj;
}
...
}
if (!providerRunning) {// 还没有运行,第一次启动
try {
// 根据ContentProvider的AUTHORITY来获取对应的配置信息(ProviderInfo)
cpi = AppGlobals.getPackageManager().
resolveContentProvider(name,
STOCK_PM_FLAGS | PackageManager.GET_URI_PERMISSION_PATTERNS, userId);
} catch (RemoteException ex) {
}
// 没有找到对应的ProviderInfo,因此返回空
if (cpi == null) {
return null;
}
// Provider是否为单例
boolean singleton = isSingleton(cpi.processName, cpi.applicationInfo,
cpi.name, cpi.flags)
&& isValidSingletonCall(r.uid, cpi.applicationInfo.uid);
...
// 通过ComponentName获取providerMap中的cpr
ComponentName comp = new ComponentName(cpi.packageName, cpi.name);
// 先去缓存中找
cpr = mProviderMap.getProviderByClass(comp, userId);
// 是否是第一次启动
final boolean firstClass = cpr == null;
if (firstClass) {// 第一次启动
final long ident = Binder.clearCallingIdentity();
try {
// 获取应用信息
ApplicationInfo ai =
AppGlobals.getPackageManager().
getApplicationInfo(
cpi.applicationInfo.packageName,
STOCK_PM_FLAGS, userId);
...
// 创建一个cpr(ContentProviderRecord)
cpr = new ContentProviderRecord(this, cpi, ai, comp, singleton);
} catch (RemoteException ex) {
...
} finally {
...
}
}
...
if (r != null && cpr.canRunHere(r)) {// 进程存在且正在运行
return cpr.newHolder(null);
}
...
final int N = mLaunchingProviders.size();
int i;
for (i = 0; i < N; i++) {
if (mLaunchingProviders.get(i) == cpr) {
break;
}
}
// ContentProvider还没有被启动
if (i >= N) {
final long origId = Binder.clearCallingIdentity();
try {
...
// 获取运行ContentProvider的进程
ProcessRecord proc = getProcessRecordLocked(
cpi.processName, cpr.appInfo.uid, false);
// 如果进程已经存在并且没有被杀死
if (proc != null && proc.thread != null && !proc.killed) {
if (!proc.pubProviders.containsKey(cpi.name)) {
// 将描述ContentProvider的对象(ContentProviderRecord)以包名为键放到进程进行缓存
proc.pubProviders.put(cpi.name, cpr);
try {
// 安装ContentProvider
proc.thread.scheduleInstallProvider(cpi);
} catch (RemoteException e) {
}
}
} else {// 进程不存在
// 启动进程
proc = startProcessLocked(cpi.processName,
cpr.appInfo, false, 0, "content provider",
new ComponentName(cpi.applicationInfo.packageName,
cpi.name), false, false, false);
if (proc == null) {// 进程启动失败
return null;
}
}
cpr.launchingApp = proc;
mLaunchingProviders.add(cpr);
} finally {
...
}
}
if (firstClass) {// 第一次启动要保存ContentProvider
mProviderMap.putProviderByClass(comp, cpr);
}
// 保存
mProviderMap.putProviderByName(name, cpr);
conn = incProviderCountLocked(r, cpr, token, stable);
if (conn != null) {
conn.waiting = true;
}
}
}
...
return cpr != null ? cpr.newHolder(conn) : null;
}
这里代码比较多,但是思路比较清晰,首先是找对应的进程是否存在,因为我们正在启动ContentProvider,所以正常进程时存在的,然后根据进程找描述ContentProvider的描述对象ContentProviderRecord,如果第一次启动,这个描述对象是不存在的,如果不是第一次那么则存在该对象,其中providerRunning参数用来判断ContentProvider是否已经运行的,如果运行了直接返回,如果没有运行则创建ContentProvider的描述对象ContentProviderRecord,然后判断进程是否存在,存在则调用ApplicationThread.scheduleInstallProvider方法安装ContentProvider,不存在则直接启动进程。安装完成后缓存这些创建的ContentProvider相关的对象,方便下次使用直接从缓存获取。
9.ApplicationThread.scheduleInstallProvider
public void scheduleInstallProvider(ProviderInfo provider) {
sendMessage(H.INSTALL_PROVIDER, provider);
}
这里是发送消息到ApplicationThread中的H(继承Handler)中,然后调用ActivityThread.handleInstallProvider
10.ActivityThread.handleInstallProvider
public void handleInstallProvider(ProviderInfo info) {
final StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
installContentProviders(mInitialApplication, Lists.newArrayList(info));
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
}
调用installContentProviders方法。
11.ActivityThread.installContentProviders
private void installContentProviders(
Context context, List<ProviderInfo> providers) {
final ArrayList<IActivityManager.ContentProviderHolder> results =
new ArrayList<IActivityManager.ContentProviderHolder>();
for (ProviderInfo cpi : providers) {
...
// 加载ContentProvider,然后调用ContentProvider的onCreate方法
IActivityManager.ContentProviderHolder cph = installProvider(context, null, cpi,
false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);
if (cph != null) {
cph.noReleaseNeeded = true;
results.add(cph);
}
}
try {
ActivityManagerNative.getDefault().publishContentProviders(
getApplicationThread(), results);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
这里有两步,第一步:安装ContentProvider,然后调用ContentProvider.onCreate方法,第二步:调用AMP.publishContentProviders发布ContentProvider。我们先看第一步。
12.ActivityThread.installProvider
private IActivityManager.ContentProviderHolder installProvider(Context context,
IActivityManager.ContentProviderHolder holder, ProviderInfo info,
boolean noisy, boolean noReleaseNeeded, boolean stable) {
ContentProvider localProvider = null;
IContentProvider provider;
if (holder == null || holder.provider == null) {
...
try {
final java.lang.ClassLoader cl = c.getClassLoader();
// 根据名字通过类加载方式加载ContentProvider
localProvider = (ContentProvider) cl.
loadClass(info.name).newInstance();
// 获取provider,也就是ContentProvider中的mTransport(Transport),这个provider赋值给了retHolder,所以前面在获取的时候就是这个provider(Transport)
provider = localProvider.getIContentProvider();
...
// 添加ProviderInfo,然后调用ContentProvider的onCreate方法
localProvider.attachInfo(c, info);
} catch (java.lang.Exception e) {
...
return null;
}
} else {// localProvider == null
provider = holder.provider;
...
}
IActivityManager.ContentProviderHolder retHolder;
// 创建IActivityManager.ContentProviderHolder并且缓存ContentProvider相关信息
...
return retHolder;
}
这里通过反射获取ContentProvider,然后调用ContentProvider的onCreate方法。创建完成后会缓存到IActivityManager.ContentProviderHolder中,并且返回IActivityManager.ContentProviderHolder对象。在11步中我们提到两步,一个就是安装,一个是发布,下面我看发布的代码。
13.AMP.publishContentProviders
public void publishContentProviders(IApplicationThread caller,
List<ContentProviderHolder> providers) throws RemoteException {
...
mRemote.transact(PUBLISH_CONTENT_PROVIDERS_TRANSACTION, data, reply, 0);
...
}
通过这里调用AMS.publishContentProviders方法。
14.AMS.publishContentProviders
public final void publishContentProviders(IApplicationThread caller,
List<ContentProviderHolder> providers) {
if (providers == null) {
return;
}
synchronized (this) {
// 获取进程描述对象
final ProcessRecord r = getRecordForAppLocked(caller);
...
final long origId = Binder.clearCallingIdentity();
final int N = providers.size();
for (int i = 0; i < N; i++) {
ContentProviderHolder src = providers.get(i);
if (src == null || src.info == null || src.provider == null) {
continue;
}
// 获取每个ContentProvider的描述对象ContentProviderRecord放到mProviderMap中
ContentProviderRecord dst = r.pubProviders.get(src.info.name);
if (dst != null) {
ComponentName comp = new ComponentName(dst.info.packageName, dst.info.name);
mProviderMap.putProviderByClass(comp, dst);
String names[] = dst.info.authority.split(";");
for (int j = 0; j < names.length; j++) {
// 缓存
mProviderMap.putProviderByName(names[j], dst);
}
int launchingCount = mLaunchingProviders.size();
...
synchronized (dst) {
dst.provider = src.provider;
dst.proc = r;
// 通知AMS provider已经"发布"成功
dst.notifyAll();
}
...
}
}
...
}
}
这里主要是从ContentProvider列表中获取对应的描述对象进行缓存,然后通知AMS中的ContentProvider发布完成。
15.ActivityThread.installProvider
这个方法我们上面12中分析了。只是一个是第一次启动,一个已经存在缓存了。
16.Transport.query
结合第1步和第12步我们知道IContentProvider unstableProvider返回的是ContentProvider中的mTransport,因此这里调用的是Transport.query方法
public Cursor query(String callingPkg, Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder,
ICancellationSignal cancellationSignal) {
...
if (enforceReadPermission(callingPkg, uri, null) != AppOpsManager.MODE_ALLOWED) {
...
// 回调目标ContentProvider所定义的query方法
Cursor cursor = ContentProvider.this.query(uri, projection, selection,
selectionArgs, sortOrder, CancellationSignal.fromTransport(
cancellationSignal));
if (cursor == null) {
return null;
}
// Return an empty cursor for all columns.
return new MatrixCursor(cursor.getColumnNames(), 0);
}
final String original = setCallingPackage(callingPkg);
try {
return ContentProvider.this.query(
uri, projection, selection, selectionArgs, sortOrder,
CancellationSignal.fromTransport(cancellationSignal));
} finally {
...
}
}
最终调用了ContentProvider.this.query方法。到此我们对于ContentProvider.this.query方法分析完成了。其实还有其他方法(insert,delete,update)逻辑也是差不多的,分析完一个其他也就很好分析。
到此Android系统四大组件都分析完了,可能感觉还是不够详细,其实还是要自己多看,多看几遍源码就熟悉了。后面开始分析View的绘制流程,因为这个相对来说非常复杂,如果一篇写完会写很长,看的话也很累,所以后面改用分章节去写,没章尽量分离开,尽量简短清晰,一看就能明白。
参考
从源码角度看ContentProvider
理解ContentProvider原理
代码地址:
直接拉取导入开发工具(Intellij idea或者Android studio)
由于coding与腾讯云合作,改变很多,所以后续代码切换到Gitlab。
https://gitlab.com/yuchuangu85/android-25
注
首发地址:http://www.codemx.cn
Android开发群:192508518
微信公众账号:Code-MX
![](https://img.haomeiwen.com/i543562/fa47e92127a9d7f8.jpg)
注:本文原创,转载请注明出处,多谢。