这里先简单介绍一下ContentProvider,内容提供者(contentprovider)主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另外一个程序中的数据,同时还能保证被访问的数据的安全性。当一个应用程序通过ContentProvider对其数据提供了外部访问的接口,任何其他应用就都可以对这部分数据进行访问。我们通常通过如下方法使用ContentProvider。
1.通过继承ContentProvider,然后重写其中的如下方法
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
2.然后在其他进程中,通过如下方式调用
Uri uri = Uri.parse("content://com.example.test");
getContentResolver().query(uri, ...);
上面的query方法中我们通常是通过sqlite提供一个cursor,或者也可以自己构造一个cursor,然后操作文件给其赋值。
但是这里我们要介绍的不是上面四个方法,而是ContentProvider提供的另外一个很有意思的方法call。通过该方法我们可以调用到ContentProvider自定义的方法。这个方法的签名如下:
@Nullable
@Override
public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
return null;
}
这个方法可以传递一个方法名, 方法的参数, 以及一个Bundle。该方法的返回值也是一个Bundle。因此我们可以很方便的借助该方法,在一个进程中调用另外一个进程的方法。
我们项目中有个壁纸Service,如果让该壁纸Service运行在单独的进程中,那么我们就需要在一个进程中更新WallpaperService中的视频地址。设计的大致过程如下所示:
首先我们完成中间的BaseContentProvider,这个ContentProvider主要完成中转工作,代码如下
public abstract class IpcServer extends ContentProvider {
private Dispatcher dispatcher;
private Bundle returnBundle;
public IpcServer() {
dispatcher = new Dispatcher(this);
returnBundle = new Bundle();
}
@Nullable
@Override
public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
String ret = dispatcher.dispatch(method, extras);
returnBundle.putString(IpcConstant.RETURN_VALUE, ret);
return returnBundle;
}
@Override
public boolean onCreate() {
return true;
}
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
return null;
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
return null;
}
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
return 0;
}
}
可以看到这个ContentProvider主要是在call方法中做了一些处理,在call方法中,我们先通过dispatcher方法完成请求的转发,然后返回结果之后我们会对这个类介绍的。
假如我们现在需要在进程A中调用进程B中的更新视频地址的方法,以及更新设置。进程B作为服务端需要提供如下所示接口中的能力。
@ServerUri(RemoteIpcServer.URI)
public interface RemoteIpcServerStub {
String updateVideo(String path);
String updateSetting();
}
另外定义一个类继承自之前的ContentProvider,并实现上述接口,具体代码如下所示:
public class RemoteIpcServer extends IpcServer implements RemoteIpcServerStub{
public static final String URI = "content://com.huya.marksman.ipc.server.RemoteIpcServer";
private static MyService service = null;
public static void setMyService(MyService myService) {
service = myService;
}
@Override
public String updateVideo(String path) {
return path + service.switchVideo();
}
@Override
public String updateSetting() {
return service.updateSetting();
}
}
现在说一下上面的Diapatcher类,如果没有这个类也是可以的,那么我们需要在每个具体的ContentProvider之中重写call方法,然后各自调用。而这个dispatcher只是帮助我们做了这个调用的工作而已,看一下该类,我们只是通过反射从具体的ContentProvider之中取出了所有的方法,然后和传递进来的ipcMethod作对比,选出要执行的方法。然后从ipcExtras中取出要执行方法的参数信息。然后执行目标方法即可获取结果。具体代码如下:
public class Dispatcher {
private IpcServer ipcServer;
public Dispatcher(IpcServer ipcServer) {
this.ipcServer = ipcServer;
}
public String dispatch(String ipcMethod, Bundle ipcExtras) {
Method targetMethod = null;
Method[] methods = ipcServer.getClass().getDeclaredMethods();
if (TextUtils.isEmpty(ipcMethod) || (methods == null)) {
return null;
}
for (Method method : methods) {
if (ipcMethod.equals(method.getName())) {
targetMethod = method;
break;
}
}
String ret = null;
try {
int argCount = ipcExtras.getInt(IpcConstant.ARG_COUNT);
Object[] args = new Object[argCount];
Class[] paramTypes = targetMethod.getParameterTypes();
for (int i = 0; i < argCount; i++) {
if (paramTypes[i] == String.class) {
args[i] = ipcExtras.getString(IpcConstant.ARG_KEYS[i]);
}
else if (paramTypes[i] == Boolean.class || paramTypes[i] == boolean.class) {
args[i] = ipcExtras.getBoolean(IpcConstant.ARG_KEYS[i]);
}
else if (paramTypes[i] == Integer.class || paramTypes[i] == int.class) {
args[i] = ipcExtras.getInt(IpcConstant.ARG_KEYS[i]);
}
else if (paramTypes[i] == Long.class || paramTypes[i] == long.class) {
args[i] = ipcExtras.getLong(IpcConstant.ARG_KEYS[i]);
}
else {
args[i] = null;
}
}
Object invokeRet = targetMethod.invoke(ipcServer, (Object[]) args);
if (invokeRet != null) {
ret = invokeRet.toString();
}
} catch (Exception e) {
e.printStackTrace();
}
return ret;
}
}
好了,进程B作为服务端该做的工作就是这些了,接下来看下客户端需要做的工作。由于上面已经有接口定义了服务端的功能。我们客户端可以使用动态代理来完成对服务端的代理。android中有很多使用动态代理完成相似功能的。比如说retrofit就是定义了请求的接口,然后通过动态代理完成功能调用的,感兴趣的可以去看看。
动态代理请求的代码如下所示:
public class IpcServerStubProxy {
public static T create(Class stubClass) {
String serverUriString = "";
ServerUri serverUri = stubClass.getAnnotation(ServerUri.class);
if (serverUri != null) {
serverUriString = serverUri.value();
}
return (T) Proxy.newProxyInstance(stubClass.getClassLoader(),
new Class[] {stubClass}, new IpcInvocationHandler(serverUriString));
}
private static class IpcInvocationHandler implements InvocationHandler {
private Uri serverUri;
public IpcInvocationHandler(String serverUri) {
this.serverUri = Uri.parse(serverUri);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (proxy == null || method == null) {
return null;
}
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
String ipcMethodName = method.getName();
Bundle ipcExtras = new Bundle();
if (args != null) {
Class[] paramTypes = method.getParameterTypes();
for (int i = 0; i < args.length; i++) {
Object arg = args[i];
if (paramTypes[i] == String.class) {
ipcExtras.putString(IpcConstant.ARG_KEYS[i], arg instanceof String ? (String) arg : "");
}
else if (paramTypes[i] == Boolean.class || paramTypes[i] == boolean.class) {
ipcExtras.putBoolean(IpcConstant.ARG_KEYS[i], arg instanceof Boolean ? (Boolean) arg : false);
}
else if (paramTypes[i] == Integer.class || paramTypes[i] == int.class) {
ipcExtras.putInt(IpcConstant.ARG_KEYS[i], arg instanceof Integer ? (Integer) arg : 0);
}
else if (paramTypes[i] == Long.class || paramTypes[i] == long.class) {
ipcExtras.putLong(IpcConstant.ARG_KEYS[i], arg instanceof Long ? (Long) arg : 0);
}
else {
ipcExtras.putString(IpcConstant.ARG_KEYS[i], null);
}
}
ipcExtras.putInt(IpcConstant.ARG_COUNT, args.length);
}
Class returnType = method.getReturnType();
return ipcCall(returnType, ipcMethodName, ipcExtras);
}
private Object ipcCall(Class returnType, String methodName, Bundle extras) {
Bundle returnBundle = MarkApplication.getApplication().getContentResolver().call(serverUri, methodName, null, extras);
String result = returnBundle != null ? returnBundle.getString(IpcConstant.RETURN_VALUE) : null;
if (returnType == Boolean.class || returnType == boolean.class) {
return Boolean.parseBoolean(result);
}
if (returnType == Integer.class || returnType == int.class) {
return Integer.parseInt(result);
}
if (returnType == Long.class || returnType == long.class) {
return Long.parseLong(result);
}
return result;
}
}
}
之前提到过我们使用ContentProvider需要传递一个uri用于确定是哪个ContentProvider来提供服务。而我们之前定义的接口其实就代表一个一个具体的ContentProvider。因此我们可以定义一个注解,通过接口获取uri。这就是之前接口上有注解的原因。然后定义的注解如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ServerUri {
String value();
}
最后是提供使用动态代理之后的接口给客户端,方便客户端调用,代码如下,这里使用的是单例提供。我们使用枚举的方式实现了单例,这是Android源码设计模式一书中介绍的方式。当然你可以使用自己喜欢的方式来实现。
public enum RemoteIpcClient {
SINGLETON;
RemoteIpcServerStub stub;
RemoteIpcClient() {
stub = IpcServerStubProxy.create(RemoteIpcServerStub.class);
}
public static RemoteIpcServerStub getInstance() {
return SINGLETON.stub;
}
}
然后客户端调用时通过如下方式即可:
public void testContentProvider(View view) {
startService(intentProvider);
String result1 = RemoteIpcClient.getInstance().updateVideo("video");
String result2 = RemoteIpcClient.getInstance().updateSetting();
textView.setText("返回的值: " + result1 + result2);
}
结果如下图所示: