使用ContentProvider实现跨进程方法调用(RPC)

3,613 阅读3分钟
原文链接: yimu.me

作为 Android 四大组件之一的 ContentProvider 在应用开发的过程中存在感一直很弱,使用的场景也限于在应用间共享数据。官方推荐我们搭配ContentProvider和数据库进行数据的增删改查,本文提到的用法是把 ContentProvder 当成一个远程过程调用的中介,实现轻量级的进程间通信,这个方法在 WebViewActivity 独立进程的重构过程中被大量使用。

Manifest 注册

首先需要在AndroidManifest.xml文件中注册ContentProvider,由于内容提供器只是在应用内部使用不该对外暴露,所以设置属性android:exported="false"

<provider
  android:authorities="${applicationId}.provider"
  android:name=".interprocess.InterProcessDataProvider"
  android:exported="false" />

ContentProvider 定义

我们需要在定义时禁用掉数据库增删改查操作,只保留基础的call方法。

/**
 * 跨进程数据提供者,基于ContentProvider,运行于主进程
 */
public class InterProcessDataProvider extends ContentProvider {

    @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) {
        throw new UnsupportedOperationException();
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        throw new UnsupportedOperationException();
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        throw new UnsupportedOperationException();
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        throw new UnsupportedOperationException();
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        throw new UnsupportedOperationException();
    }

    @Nullable
    @Override
    public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
        Processor processor = ProcessorPool.get(method);
        return processor == null ? null : processor.process(arg, extras);
    }
}

管理跨进程方法

上面的代码中出现了一个ProcessorPool, 通过这个类去注册管理所有的跨进程调用方法,相关的类定义如下:
Processor.java 定义跨进程调用方法

public interface Processor {
    String getMethodName();
    Bundle process(@Nullable String arg, @Nullable Bundle extras);
}

ProcessorManger.java 统一管理某一类别的Processor

public interface ProcessorManager {
    List<Processor> getItems();
}

ProcessorPool.java 跨进程方法池

public class ProcessorPool {

    public static Map<String, Processor> sMethodMap = new HashMap<>();

    public static Processor get(String name) {
        return sMethodMap.get(name);
    }

    public static void register(Processor processor) {
        sMethodMap.put(processor.getMethodName(), processor);
    }

    public static void register(ProcessorManager processors) {
        for (Processor item : processors.getItems()) {
            register(item);
        }
    }
}

一个使用的栗子

下面演示跨进程获取token的方法,注意上面manifest定义时ContentProvider运行在主进程中,也就是获取到的是主进程的数据。

1.首先定义处理器:

/**
* 获取登录用户的token
*/
static Processor sGetAccessToken = new Processor() {

   @Override
   public String getMethodName() {
       return "get_user_token";
   }

   @Override
   public Bundle process(@Nullable String arg, @Nullable Bundle extras) {
       Bundle bundle = new Bundle();
       // AccountManger主进程的用户管理
       String accessToken = AccountManager.getInstance().getAccessToken();
       // 通过bundle包装数据
       bundle.putString(Constants.KEY_ACCESS_TOKEN, accessToken);
       return bundle;
   }
};

2.注册:在Application初始化时注册

public class MyApplication extends Application {
    public void onCreate() {
        ProcessorPool.register(sGetAccessToken);
    }
}

3.调用:

public class TokenLet {
     public static String getAccessToken() {
        // 如果当前进程是主进程,则使用进程内调用
        if (InterProcessUtils.isMainProcess) {
            return FrodoAccountManager.getInstance().getAccessToken();
        }
        Bundle result = null;
        // 与manifest中定义的authorities相对应
        Uri providerUri = Uri.parse("content://" + context.getPackageName() + ".provider");
        try {
            result = AppContext.getApp().getContentResolver().call(providerUri,
                    "get_user_token", null, null);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (result != null) {
            return result.getString(Constants.KEY_ACCESS_TOKEN);
        }
        return "";
    }
}

优缺点

过去实现类似跨进程获取token字符串一般是使用aidl接口,但是aidl接口的定义十分繁琐,而且这种方式需要启动一个service去管理这些aidl调用。使用ContentProvider的方法优点很明显,使用简单,可以传递任何支持序列化的数据。从上面的代码中我们可以发现,所有方法的调用是同步的,并不支持aidl中的异步callback回调的方式。综上而言,ContentProvider实现RPC适合数据量小且调用简单的同步调用。