RePlugin解析之ContentProvider

引言

与插件中静态广播接收器类似,由于插件中的组件没有在Manifest中注册,如果没有宿主对广播进行注册和转发的话,那么就无法起到静态广播的作用(比如拉起进程),ContentProvider也是一样,也需要宿主对ContentProvider的CRUD操作进行转发

1.插件中代码替换

插件中会涉及到对于ContentProvider的操作有两种,第一种是ContentResolver相关的方法调用; 第二种是ContentProviderClient相关的方法调用。
所以在replugin-plugin-gradle中也有两个负责代码插入的类,第一个是ProviderInjector; 第二个是ProviderInjector2.

1.1 ProviderInjector

首先看一下ProviderInjector的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class ProviderInjector extends BaseInjector {
// 处理以下方法
public static def includeMethodCall = ['query',
'getType',
'insert',
'bulkInsert',
'delete',
'update',
'openInputStream',
'openOutputStream',
'openFileDescriptor',
'registerContentObserver',
'acquireContentProviderClient',
'notifyChange',
]
// 表达式编辑器
def editor
@Override
def injectClass(ClassPool pool, String dir, Map config) {
// 不处理非 build 目录下的类
/*
if (!dir.contains('build' + File.separator + 'intermediates')) {
println "跳过$dir"
return
}
*/
if (editor == null) {
editor = new ProviderExprEditor()
}
Util.newSection()
println dir
Files.walkFileTree(Paths.get(dir), new SimpleFileVisitor<Path>() {
@Override
FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String filePath = file.toString()
def stream, ctCls
try {
if (filePath.contains('PluginProviderClient.class')) {
throw new Exception('can not replace self ')
}
stream = new FileInputStream(filePath)
ctCls = pool.makeClass(stream);
// println ctCls.name
if (ctCls.isFrozen()) {
ctCls.defrost()
}
editor.filePath = filePath
ctCls.getDeclaredMethods().each {
it.instrument(editor)
}
ctCls.getMethods().each {
it.instrument(editor)
}
ctCls.writeFile(dir)
} catch (Throwable t) {
println " [Warning] --> ${t.toString()}"
// t.printStackTrace()
} finally {
if (ctCls != null) {
ctCls.detach()
}
if (stream != null) {
stream.close()
}
}
return super.visitFile(file, attrs)
}
})
}
}

includeMethodCall中包含了所有要替换的地方,injectClass()方法中,ctCls.getDeclaredMethods().each{}和ctCls.getMethods().each{}的作用是遍历全部方法,并且执行instrument方法,逐个扫描方法体内的每一行代码,然后交给ProviderExprEditor的edit()处理对方法体代码的修改。

而ProviderExprEditor代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ProviderExprEditor extends ExprEditor {
static def PROVIDER_CLASS = 'com.qihoo360.replugin.loader.p.PluginProviderClient'
public def filePath
@Override
void edit(MethodCall m) throws CannotCompileException {
String clsName = m.getClassName()
String methodName = m.getMethodName()
if (clsName.equalsIgnoreCase('android.content.ContentResolver')) {
if (!(methodName in ProviderInjector.includeMethodCall)) {
// println "跳过$methodName"
return
}
replaceStatement(m, methodName, m.lineNumber)
}
}
def private replaceStatement(MethodCall methodCall, String method, def line) {
if (methodCall.getMethodName() == 'registerContentObserver' || methodCall.getMethodName() == 'notifyChange') {
methodCall.replace('{' + PROVIDER_CLASS + '.' + method + '(com.qihoo360.replugin.RePlugin.getPluginContext(), $$);}')
} else {
methodCall.replace('{$_ = ' + PROVIDER_CLASS + '.' + method + '(com.qihoo360.replugin.RePlugin.getPluginContext(), $$);}')
}
println ">>> Replace: ${filePath} Provider.${method}():${line}"
}
}

方法很简单,就是遇到使用了ContentResolver中在includeMethodCall中的方法调用,就替换成PluginProviderClient的调用。关于PluginProviderClient的分析在下一节。

1.2 ProviderInjector2

ProviderInjector2的实现其实跟ProviderInjector很类似,只不过它要处理的是ContentProviderClient的调用,而ContentProviderClient的调用只有两个方法: query()和update().

下面是ProviderInjector2的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class ProviderInjector2 extends BaseInjector {
// 处理以下方法
public static def includeMethodCall = ['query', 'update']
// 表达式编辑器
def editor
@Override
def injectClass(ClassPool pool, String dir, Map config) {
// 不处理非 build 目录下的类
/*
if (!dir.contains('build' + File.separator + 'intermediates')) {
println "跳过$dir"
return
}
*/
if (editor == null) {
editor = new ProviderExprEditor2()
}
Util.newSection()
println dir
Files.walkFileTree(Paths.get(dir), new SimpleFileVisitor<Path>() {
@Override
FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String filePath = file.toString()
def stream, ctCls
try {
if (filePath.contains('PluginProviderClient2.class')) {
throw new Exception('can not replace self ')
}
stream = new FileInputStream(filePath)
ctCls = pool.makeClass(stream);
// println ctCls.name
if (ctCls.isFrozen()) {
ctCls.defrost()
}
editor.filePath = filePath
ctCls.getDeclaredMethods().each {
it.instrument(editor)
}
ctCls.getMethods().each {
it.instrument(editor)
}
ctCls.writeFile(dir)
} catch (Throwable t) {
println " [Warning] --> ${t.toString()}"
// t.printStackTrace()
} finally {
if (ctCls != null) {
ctCls.detach()
}
if (stream != null) {
stream.close()
}
}
return super.visitFile(file, attrs)
}
})
}
}

类似地,ctCls.getDeclaredMethods().each{}和ctCls.getMethods().each{}的作用是遍历全部方法,并执行instrument方法,逐个扫描每个方法体内每一行代码,并交给ProviderExprEditor2的edit()方法处理对方法体代码的修改。

而ProviderExprEditor2的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ProviderExprEditor2 extends ExprEditor {
static def PROVIDER_CLASS = 'com.qihoo360.loader2.mgr.PluginProviderClient2'
public def filePath
@Override
void edit(MethodCall m) throws CannotCompileException {
String clsName = m.getClassName()
String methodName = m.getMethodName()
if (clsName.equalsIgnoreCase('android.content.ContentProviderClient')) {
println " ${filePath} ContentProviderClient.${methodName}():${m.lineNumber}"
if (!(methodName in ProviderInjector2.includeMethodCall)) {
// println "跳过$methodName"
return
}
replaceStatement(m, methodName, m.lineNumber)
}
}
def private replaceStatement(MethodCall methodCall, String method, def line) {
methodCall.replace('{$_ = ' + PROVIDER_CLASS + '.' + method + '(com.qihoo360.replugin.RePlugin.getPluginContext(), $$);}')
println ">>> Replace: ${filePath} Provider.${method}():${line}"
}
}

显然,这个类的作用就是将ContentProviderClient的方法调用替换为PluginProviderClient2的同名方法调用,同时再次吐槽一下这个命名规范。关于PluginProviderClient2的分析见下一节。

2. PluginProviderClient和PluginProviderClient2

上一节中讲到插件中所有ContentResolver的调用都会替换为PluginProviderClient的同名方法调用,而所有ContentProviderClient的调用都会替换为PluginProviderClient2的同名方法调用

不过,首先需要注意的是,上面说到的PluginProviderClient和PluginProviderClient2都是replugin-plugin-library中的类,而不是replugin-host-library中的类。而这两个类都是采用反射的方式调用replugin-host-library中PluginProviderClient和PluginProviderClient2中的代码。

关于它们是如何包装r反射的就不说了,感兴趣的童鞋自己去看吧,下面要讲的都是replugin-host-library中的PluginProviderClient和PluginProviderClient2.

2.1 PluginProviderClient

PluginProviderClient实现了很多方法,我们以query()为例:

1
2
3
4
public static Cursor query(Context c, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Uri turi = toCalledUri(c, uri);
return c.getContentResolver().query(turi, projection, selection, selectionArgs, sortOrder);
}

其中的重点是toCalledUri()这个方法,这个方法在CRUD中都要调用,它的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 将从【当前】插件或主程序里的URI转化成系统传过来的URI,且由插件Manifest来指定进程。例如:
* Before: content://com.qihoo360.contacts.abc/people (Contacts插件,UI)
* After: content://com.qihoo360.mobilesafe.PluginUIP/contacts/com.qihoo360.mobilesafe.contacts.abc/people
*
* @param c 当前的Context对象。若传递主程序的Context,则直接返回Uri,不作处理。否则就做Uri转换
* @param uri URI对象
* @return 转换后可直接在ContentResolver使用的URI
*/
public static Uri toCalledUri(Context c, Uri uri) {
String pn = fetchPluginByContext(c, uri);
if (pn == null) {
return uri;
}
return toCalledUri(c, pn, uri, IPluginManager.PROCESS_AUTO);
}

这里只是通过context获取了plugin的名称,然后调用了另外一个toCalledUri()方法。

继续看toCallUri(Context,String,Uri,int)方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 将从插件里的URI转化成系统传过来的URI。可自由指定在哪个进程启动。例如:
* Before: content://com.qihoo360.contacts.abc/people (Contacts插件,UI)
* After: content://com.qihoo360.mobilesafe.PluginUIP/contacts/com.qihoo360.mobilesafe.contacts.abc/people
*
* @param context 当前的Context对象,目前暂无用
* @param plugin 要使用的插件
* @param uri URI对象
* @param process 进程信息,若为PROCESS_AUTO,则根据插件Manifest来指定进程
* @return 转换后可直接在ContentResolver使用的URI
*/
public static Uri toCalledUri(Context context, String plugin, Uri uri, int process) {
if (TextUtils.isEmpty(plugin)) {
throw new IllegalArgumentException();
}
if (uri == null) {
throw new IllegalArgumentException();
}
if (uri.getAuthority().startsWith(PluginPitProviderBase.AUTHORITY_PREFIX)) {
// 自己已填好了要使用的插件名(以PluginUIProvider及其它为开头),这里不做处理
return uri;
}
// content://com.qihoo360.mobilesafe.PluginUIP
if (process == IPluginManager.PROCESS_AUTO) {
// 直接从插件的Manifest中获取
process = getProcessByAuthority(plugin, uri.getAuthority());
if (process == PROCESS_UNKNOWN) {
// 可能不是插件里的,而是主程序的,直接返回Uri即可
return uri;
}
}
String au;
if (process == IPluginManager.PROCESS_PERSIST) {
au = PluginPitProviderPersist.AUTHORITY;
} else if (PluginProcessHost.isCustomPluginProcess(process)) {
au = PluginProcessHost.PROCESS_AUTHORITY_MAP.get(process);
} else {
au = PluginPitProviderUI.AUTHORITY;
}
// from => content:// com.qihoo360.contacts.abc/people?id=9
// to => content://com.qihoo360.mobilesafe.Plugin.NP.UIP/plugin_name/com.qihoo360.contacts.abc/people?id=9
String newUri = String.format("content://%s/%s/%s", au, plugin, uri.toString().replace("content://", ""));
return Uri.parse(newUri);
}

这个方法其实就做了两件时,第一件事是从plugin名称称uri的authority获取到其process, 然后根据process获取坑位ContentProvider的authority,最后进行封装成可以调用坑位ContentProvider的uri.

显然,总结起来,这个方法的作用就是把插件的uri改造成hostpkg+pluguri的方式

那么经过改造的uri被谁接收了呢?

实际上是坑位ContentProvider, 每个插件中的ContentProvider,即使是有自定义进程的,也会有一个转换关系,让它对应到某个坑位ContentProvider.

那么我们怎么知道是调用坑位ContentProvider的呢?

其实关键就是看authority的赋值,看如下代码片段:

1
2
3
4
5
6
7
8
String au;
if (process == IPluginManager.PROCESS_PERSIST) {
au = PluginPitProviderPersist.AUTHORITY;
} else if (PluginProcessHost.isCustomPluginProcess(process)) {
au = PluginProcessHost.PROCESS_AUTHORITY_MAP.get(process);
} else {
au = PluginPitProviderUI.AUTHORITY;
}

注意其中的PluginProcessHost.PROCESS_AUTHORITY_MAP是怎么来的:

1
2
3
4
5
6
7
static {
for (int i = 0; i < PROCESS_COUNT; i++) {
PROCESS_INT_MAP.put(PROCESS_PLUGIN_SUFFIX2 + i, PROCESS_INIT + i);
PROCESS_ADJUST_MAP.put("$" + PROCESS_PLUGIN_SUFFIX + i, IPC.getPackageName() + ":" + PROCESS_PLUGIN_SUFFIX + i); //IPC.getPak
PROCESS_AUTHORITY_MAP.put(PROCESS_INIT + i, PluginPitProviderBase.AUTHORITY_PREFIX + i);
}
}

注意这里的赋值就用到了PluginPitProviderBase.AUTHORITY_PREFIX, 而PluginPitProviderBase就是所有坑位ContentProvider的基类。

题外话:再次吐槽一下RePlugin糟糕的命名,这里如果改成encodeUri()会好很多,之后对于uri进行还原时可以对应地命名为decodeUri()方法

2.2 PluginProviderClient2

相比之下,PluignProviderClient2的代码要简单一些,因为它只需要替换插件中ContentProviderClient的query()和update()方法,以query()为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Cursor query(Context c, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
ContentProviderClient client = PluginProviderClient.acquireContentProviderClient(c, "");
if (client != null) {
try {
Uri toUri = toCalledUri(c, uri);
return client.query(toUri, projection, selection, selectionArgs, sortOrder);
} catch (RemoteException e) {
...
}
}
...
return null;
}

显然,核心也是toCalledUri()这个方法的调用,在上一节分析过,这里就不再啰嗦了。

3.坑位ContentProvider: PluginPitProviderBase

PluginPitProviderBase作为各坑位ContentProvider的基类,实现了CRUD在内的各种方法,仍然以query()为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
PluginProviderHelper.PluginUri pu = mHelper.toPluginUri(uri);
if (pu == null) {
return null;
}
ContentProvider cp = mHelper.getProvider(pu);
if (cp == null) {
return null;
}
return cp.query(pu.transferredUri, projection, selection, selectionArgs, sortOrder);
}

首先看PluginProviderHelper中的toPluginUri()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 将从系统传过来的URI转化成插件里的URI。例如:
// Before: content://com.qihoo360.mobilesafe.PluginTransferP/contacts/com.qihoo360.contacts.abc/people
// After : content:// com.qihoo360.contacts.abc/people (从contacts插件中解析并寻找)
public PluginUri toPluginUri(Uri uri) {
if (LogDebug.LOG) {
Log.i(TAG, "toPluginUri(): Start... Uri=" + uri);
}
// Authority正确
if (!TextUtils.equals(uri.getAuthority(), mAuthority)) {
if (LogDebug.LOG) {
Log.e(TAG, "toPluginUri(): Authority error! auth=" + uri.getAuthority());
}
return null;
}
// 至少两个元素(排除掉authority)
List<String> fs = uri.getPathSegments();
if (fs.size() < 2) {
if (LogDebug.LOG) {
Log.e(TAG, "toPluginUri(): Less than 2 fragments, size=" + fs.size());
}
return null;
}
// 获取插件名(第一个元素)
String pn = fs.get(0);
// 看这个Plugin是否可以被打开
if (!RePlugin.isPluginInstalled(pn)) {
if (LogDebug.LOG) {
Log.e(TAG, "toPluginUri(): Plugin not exists! pn=" + pn);
}
return null;
}
// 剔除Uri中开头的内容
String ut = uri.toString();
String tut = removeHostAuthorityAndInfo(ut, pn);
PluginUri pu = new PluginUri();
pu.plugin = pn;
pu.transferredUri = Uri.parse(tut);
if (LogDebug.LOG) {
Log.i(TAG, "toPluginUri(): End! t-uri=" + pu);
}
return pu;
}

这个方法就是字符串进行处理,将之前包装过的uri还原为插件原本的uri,之后将插件名称和原本的uri保存在PluginUri对象中。

再回到PluginPitProviderBase中的query()方法,获取到PluginUri之后,调用PluginProviderHelper.getProvider()方法找到对应的ContentProvider:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public ContentProvider getProvider(PluginUri pu) {
...
String auth = pu.transferredUri.getAuthority();
// 已有缓存?直接返回!
ContentProvider cp = mProviderAuthorityMap.get(auth);
if (cp != null) {
...
return cp;
}
// 开始构建插件里的ContentProvider对象
cp = installProvider(pu, auth);
if (cp == null) {
...
return null;
}
// 加入列表。下次直接读缓存
mProviderAuthorityMap.put(auth, cp);
...
return cp;
}

这个方法很简单,首先根据 authority从缓存中找,如果找到则直接返回;如果没找到则调用installProvider()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
private ContentProvider installProvider(PluginUri pu, String auth) {
// 开始尝试获取插件的ProviderInfo
ComponentList col = Factory.queryPluginComponentList(pu.plugin);
if (col == null) {
if (LogDebug.LOG) {
Log.e(TAG, "installProvider(): Fetch Component List Error! auth=" + auth);
}
return null;
}
ProviderInfo pi = col.getProviderByAuthority(auth);
if (pi == null) {
if (LogDebug.LOG) {
Log.e(TAG, "installProvider(): Not register! auth=" + auth);
}
return null;
}
// 通过ProviderInfo创建ContentProvider对象
Context plgc = Factory.queryPluginContext(pu.plugin);
if (plgc == null) {
if (LogDebug.LOG) {
Log.e(TAG, "installProvider(): Fetch Context Error! auth=" + auth);
}
return null;
}
ClassLoader cl = plgc.getClassLoader();
if (cl == null) {
if (LogDebug.LOG) {
Log.e(TAG, "installProvider(): ClassLoader is Null!");
}
return null;
}
ContentProvider cp;
try {
cp = (ContentProvider) cl.loadClass(pi.name).newInstance();
} catch (Throwable e) {
if (LogDebug.LOG) {
Log.e(TAG, "installProvider(): New instance fail!", e);
}
return null;
}
// 调用attachInfo方法(内部会调用onCreate)
try {
cp.attachInfo(plgc, pi);
} catch (Throwable e) {
// 有两种可能:
// 1、第三方ROM修改了ContentProvider.attachInfo的实现
// 2、开发者自己覆写了attachInfo方法,其中有Bug
// 故暂时先Try-Catch,这样若插件的Provider没有使用Context对象,则也不会出现问题
if (LogDebug.LOG) {
Log.e(TAG, "installProvider(): Attach info fail!", e);
}
return null;
}
return cp;
}

如果看过Android Framework代码的话,对这个方法不会陌生,它们其实非常像,唯一的不同就是这里是从ComponentList中获取ProviderInfo.

获取到ProviderInfo之后,调用插件中的PluignDexClassLoader加载相应的类并生成实例,之后调用相应的回调方法即可。