Android借助ContentProvider完成进程间通信

1.ContentProvider简单介绍

这里先简单介绍一下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。因此我们可以很方便的借助该方法,在一个进程中调用另外一个进程的方法。

2.通过ContentProvider设计进程间通信部分

我们项目中有个壁纸Service,如果让该壁纸Service运行在单独的进程中,那么我们就需要在一个进程中更新WallpaperService中的视频地址。设计的大致过程如下所示:

Android借助ContentProvider完成进程间通信_第1张图片

首先我们完成中间的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);
}

结果如下图所示:

                        Android借助ContentProvider完成进程间通信_第2张图片                                               Android借助ContentProvider完成进程间通信_第3张图片

总结:在上面我们利用ContentProvider的跨进程通信能力,以及call方法实现了进程间通信功能。通过这个功能,对ContentProvider的使用有了更深的理解,同时也熟悉了注解的使用,动态代理等。希望自己平时能多分析要实现什么功能,怎么实现该功能,不断提升自己,加油!

 

你可能感兴趣的:(Android借助ContentProvider完成进程间通信)