ContentProvider插件化

ContentProvider插件化

ContentProvider基础知识

在讲解ContentProvider插件化之前,我们先来简单回顾一下ContentProvider的基础知识。

ContentProvider是安卓四大组件之一,主要用于不同应用之间的信息共享,比如说应用读取手机通讯录的联系人列表。

ContentProvider使用方法也比较简单,获取contentResolver就可以调用其下列方法。

  • query()。

    该方法的源码如下所示。

    public final @Nullable Cursor query(@RequiresPermission.Read @NonNull Uri uri,
            @Nullable String[] projection, @Nullable String selection,
            @Nullable String[] selectionArgs, @Nullable String sortOrder,
            @Nullable CancellationSignal cancellationSignal) {
        Bundle queryArgs = createSqlQueryBundle(selection, selectionArgs, sortOrder);
        return query(uri, projection, queryArgs, cancellationSignal);
    }
    • Uri。

      Uri即为我们常说的统一资源标识符,只不过在安卓系统里,它被设计成了一个类,系统通过Uri来确定应用想要读取的信息,该类的实例可通过该类自带的Parse()方法转换获取,查看Parse()源码。

      public static Uri parse(String uriString) {
             return new StringUri(uriString);
         }

      需要传入一个uriString的字符串,该uriString可由下面三个部分组成。

      • protocol。协议,在ContentProvier中该部分固定为content。
      • auth。全称为authority,系统通过该部分确定是读取哪个应用的数据。
      • path。系统通过该部分确定是读取应用数据中的哪个表。
    • projection。该参数的作用是指明要读取的列,相当于mysql的select部分。

    • selection。该参数的作用是对数据进行筛选,相当于mysql的"where ?=?"中等于号前面部分。

    • selectionArgs。该参数相当于mysql的"where ?=?"中等于号后面部分。

    • sortOrder。该参数标明查询到的数据该以何种顺序呈现。

  • insert()方法。

    该方法源码如下所示。

    public final @Nullable Uri insert(@RequiresPermission.Write @NonNull Uri url,
                    @Nullable ContentValues values) {
            return insert(url, values, null);
       }
    • Uri。该参数详情可见上文

    • ContentValues。

      该参数为待插入的值,可以通过contentValuesOf()方法进行获取到ContentValues的实例,查看contentValuesOf的源码,就会发现其实插入的值就是不限数量的键值对,键值对中key就是要插入的列,value就是要插入的值。

      public fun contentValuesOf(
          vararg pairs: Pair<String, Any?>
      ): ContentValues = ContentValues(pairs.size).apply {
          for ((key, value) in pairs) {
              when (value) {
                  null -> putNull(key)
                  is String -> put(key, value)
                  is Int -> put(key, value)
                  is Long -> put(key, value)
                  is Boolean -> put(key, value)
                  is Float -> put(key, value)
                  is Double -> put(key, value)
                  is ByteArray -> put(key, value)
                  is Byte -> put(key, value)
                  is Short -> put(key, value)
                  else -> {
                      val valueType = value.javaClass.canonicalName
                      throw IllegalArgumentException("Illegal value type $valueType for key "$key"")
                 }
             }
         }
      }
  • update()。

    该方法的源码如下所示。

    public final int update(@RequiresPermission.Write @NonNull Uri uri,
                @Nullable ContentValues values, @Nullable String where,
                @Nullable String[] selectionArgs) {
            return update(uri, values, createSqlQueryBundle(where, selectionArgs));
       }

    该方法所需参数在前面方法的参数讲解已经讲过,这里就不再赘述。

  • delete()

    该方法的源码如下所示。

    public final int delete(@RequiresPermission.Write @NonNull Uri url, @Nullable String where,
                @Nullable String[] selectionArgs) {
            return delete(url, createSqlQueryBundle(where, selectionArgs));
       }

    同样的,该方法所需参数在前面方法的参数讲解已经讲过,这里就不再赘述。

VirtualApk第三方库分析

了解完ContentProvider的基础知识,我们再来看看ContentProvider的具体实现方案,与之前分析Activity和Service插件化时需要我们自己编写插件化代码不同,我们这次通过分析第三方库VirtualApk来学习ContentProvider的插件化。

在VirtualApk示例代码的MainActivity::onCreate()中有下面一行代码

this.loadPlugin(this);

该方法调用了MainActivity::loadPlugin()方法,该方法源码如下所示。

private void loadPlugin(Context base) {
        if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
            Toast.makeText(this, "sdcard was NOT MOUNTED!", Toast.LENGTH_SHORT).show();
       }
 //1
        PluginManager pluginManager = PluginManager.getInstance(base);
 //后续代码的逻辑都是加载Test.apk文件
        File apk = new File(Environment.getExternalStorageDirectory(), "Test.apk");
        if (apk.exists()) {
            try {
             //2
                pluginManager.loadPlugin(apk);
                Log.i(TAG, "Loaded plugin from apk: " + apk);
           } catch (Exception e) {
                e.printStackTrace();
           }
       } else {
            try {
                File file = new File(base.getFilesDir(), "Test.apk");
                java.io.InputStream inputStream = base.getAssets().open("Test.apk", 2);
                java.io.FileOutputStream outputStream = new java.io.FileOutputStream(file);
                byte[] buf = new byte[1024];
                int len;
    
                while ((len = inputStream.read(buf)) > 0) {
                    outputStream.write(buf, 0, len);
               }
    
                outputStream.close();
                inputStream.close();
             //3
                pluginManager.loadPlugin(file);
                Log.i(TAG, "Loaded plugin from assets: " + file);
           } catch (Exception e) {
                e.printStackTrace();
           }
       }
   }
  • 1处的getInstance()源码如下所示。
public static PluginManager getInstance(Context base) {
        if (sInstance == null) {
            synchronized (PluginManager.class) {
                if (sInstance == null) {
                    sInstance = createInstance(base);
               }
           }
       }
        return sInstance;
}

方法如其名,该方法主要是获取PluginManager类的全局单例,createInstance()方法最终会调用到PluginManager的构造方法。

    protected PluginManager(Context context) {
        if (context instanceof Application) {
            this.mApplication = (Application) context;
            this.mContext = mApplication.getBaseContext();
       } else {
            final Context app = context.getApplicationContext();
            if (app == null) {
                this.mContext = context;
                this.mApplication = ActivityThread.currentApplication();
           } else {
                this.mApplication = (Application) app;
                this.mContext = mApplication.getBaseContext();
           }
       }
        
        mComponentsHandler = createComponentsHandler();
        hookCurrentProcess();
   }

可以看到PluginManager的构造方法主要是设置mContext和mApplication变量,mContext变量尤其需要注意一下,后文经常有mContext的身影。

此外,构造方法还调用了hookCurrentProcess()方法,主要是。

    protected void hookCurrentProcess() {
        hookInstrumentationAndHandler();
        hookSystemServices();
        hookDataBindingUtil();
   }

该方法与ContentProvider插件化关联性较强的主要是hookInstrumentationAndHandler()方法。

    protected void hookInstrumentationAndHandler() {
        try {
          //获取activityThread实例
            ActivityThread activityThread = ActivityThread.currentActivityThread();
          //获取activityThread的instrumentation实例
            Instrumentation baseInstrumentation = activityThread.getInstrumentation();    
          //创建Hook代理
            final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);  
          //用Hook代理替代原先的instrumentation实例
            Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);
          //获取activityThread实例的handler并用hook代理替换其mCallback变量
            Handler mainHandler = Reflector.with(activityThread).method("getHandler").call();
            Reflector.with(mainHandler).field("mCallback").set(instrumentation);
            this.mInstrumentation = instrumentation;
            Log.d(TAG, "hookInstrumentationAndHandler succeed : " + mInstrumentation);
       } catch (Exception e) {
            Log.w(TAG, e);
       }
   }

有的小伙伴可能会好奇,哎,ActivityThread不是系统隐藏类吗,上面代码怎么能直接调用呢?还有更奇怪的呢,走,去看看源码。

package android.app;
​
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
​
/**
 * @author johnsonlee
 */
public final class ActivityThread {
​
    public static ActivityThread currentActivityThread() {
        throw new RuntimeException("Stub!");
   }
​
    public static boolean isSystem() {
        throw new RuntimeException("Stub!");
   }
​
    public static String currentOpPackageName() {
        throw new RuntimeException("Stub!");
   }
​
    public static String currentPackageName() {
        throw new RuntimeException("Stub!");
   }
​
    public static String currentProcessName() {
        throw new RuntimeException("Stub!");
   }
​
    public static Application currentApplication() {
        throw new RuntimeException("Stub!");
   }
​
    public ApplicationThread getApplicationThread() {
        throw new RuntimeException("Stub!");
   }
​
    public Instrumentation getInstrumentation() {
        throw new RuntimeException("Stub!");
   }
​
    public Looper getLooper() {
        throw new RuntimeException("Stub!");
   }
​
    public Application getApplication() {
        throw new RuntimeException("Stub!");
   }
​
    public String getProcessName() {
        throw new RuntimeException("Stub!");
   }
​
    public final ActivityInfo resolveActivityInfo(final Intent intent) {
        throw new RuntimeException("Stub!");
   }
​
    public final Activity getActivity(final IBinder token) {
        throw new RuntimeException("Stub!");
   }
​
    final Handler getHandler() {
        throw new RuntimeException("Stub!");
   }
​
    private class ApplicationThread extends ApplicationThreadNative {
        
   }
}

可以看到的是,此ActivityThread非彼ActivityThread,而是VirtualApk编写的Stub类,该类的全部方法都抛了RuntimeException异常。

既然该类全部方法都抛了异常,那这么做有什么用呢?

VirtualApk对此做了解释。

翻译成人话就是:(为了避免混淆,下文用真ActivityThread来指代系统类ActivityThread,假ActivityThread来指代自定义的ActivityThread)

  • 欺骗编译器。由于像真ActivityThread等系统类都是隐藏的,在IDE直接调用会报类不存在的错,因此可以通过自定义Stub类来欺骗IDE通过编译。
  • 提高性能。我们之前是通过反射来获取真ActivityThread实例,而这种方法不需要通过反射,仔细看,这假ActivityThread类是定义在android.app包里的,真ActivityThread也是定义在这个包里的,在运行过程中,真ActivityThread作为系统类,会先被加载进虚拟机,得益于Java独特的双亲委托类加载模式,假ActivityThread就不会再被加载了,所以那些抛异常的方法都不会被调用到。

此外,还需要注意的是,上述自定义ActivityThread所有方法在真正的系统类ActivityThread都是Public的,可以直接调用的,而那些私有或者包级可见的方法或者变量仍需要通过反射进行获取或者调用。

这里需要注意一下的是,上述代码中之所以VAInstrumentation类能替换Handler的mCallBack实例,是因为其实现了Handler.callback的接口。

public class VAInstrumentation extends Instrumentation implements Handler.Callback {
}

该VAInstrumentation类非常关键,该类有一个方法callActivityOnCreate(),

    @Override
    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        injectActivity(activity);
        mBase.callActivityOnCreate(activity, icicle);
   }
    
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public void callActivityOnCreate(Activity activity, Bundle icicle, PersistableBundle persistentState) {
        injectActivity(activity);
        mBase.callActivityOnCreate(activity, icicle, persistentState);
   }

该callActivityOnCreate()会在系统启动Activity的时候就调用。

injectActivity()方法在这里就比较关键

    protected void injectActivity(Activity activity) {
        final Intent intent = activity.getIntent();
        if (PluginUtil.isIntentFromPlugin(intent)) {
            Context base = activity.getBaseContext();
            try {
               ...
                Reflector reflector = Reflector.with(activity);
                reflector.field("mBase").set(plugin.createPluginContext(activity.getBaseContext()));
 ...
           } catch (Exception e) {
                Log.w(TAG, e);
           }
       }
   }

mBase其类型为context,为ContextWrapper类的成员变量,上述代码替换了mBase为PlugInContext实例。

VirtualApk的初始化就讲到这里。

ContentProvider插件化原理

ContentProvider插件化原理与Service插件化原理差不多,都是启动一个代理ContentProvider,然后由代理ContentProvider去启动插件ContentProvider。

下图所示即为ContentResolver的调用时序图(下图ContentResolver的拼写有误)。

启动代理ContentProvider

VirtualApk有ContentProvider插件化的示例代码。

    public void onButtonClick(View v) {
        if (v.getId() == R.id.button) {
 ...
            // test ContentProvider
            Uri bookUri = Uri.parse("content://com.didi.virtualapk.demo.book.provider/book");
            LoadedPlugin plugin = PluginManager.getInstance(this).getLoadedPlugin(pkg);
            bookUri = PluginContentResolver.wrapperUri(plugin, bookUri);
         //1
            Cursor bookCursor = getContentResolver().query(bookUri, new String[]{"_id", "name"}, null, null, null);
       } else if (v.getId() == R.id.about) {
       } else if (v.getId() == R.id.webview) {
       }
   }

1处的代码中getContentResolver()方法源码如下面所示。

public class ContextWrapper extends Context {   
 @Override
    public ContentResolver getContentResolver() {
        return mBase.getContentResolver();
   }
}  

记得前面ContextWrapper的mBase已经被替换成PluginContext了吗?

所以这里其实是调用了PluginContext的getContentResolver()方法。

    @Override
    public ContentResolver getContentResolver() {
        return new PluginContentResolver(getHostContext());
   }

该方法返回了PluginContentResolver实例,因此会调用到该实例的acquireUnstableProvider()方法。

    protected IContentProvider acquireUnstableProvider(Context context, String auth) {
        if (mPluginManager.resolveContentProvider(auth, 0) != null) {
            return mPluginManager.getIContentProvider();
       }
        return super.acquireUnstableProvider(context, auth);
   }

该方法会调用到PluginManager的getIContentProvider()方法。

    public synchronized IContentProvider getIContentProvider() {
        if (mIContentProvider == null) {
            hookIContentProviderAsNeeded();
       }
​
        return mIContentProvider;
   }

继续往下查。

protected void hookIContentProviderAsNeeded() {
        Uri uri = Uri.parse(RemoteContentProvider.getUri(mContext));
        mContext.getContentResolver().call(uri, "wakeup", null, null);
        try {
            Field authority = null;
            Field provider = null;
            ActivityThread activityThread = ActivityThread.currentActivityThread();
            Map providerMap = Reflector.with(activityThread).field("mProviderMap").get();
            Iterator iter = providerMap.entrySet().iterator();
            while (iter.hasNext()) {
                Map.Entry entry = (Map.Entry) iter.next();
                Object key = entry.getKey();
                Object val = entry.getValue();
                String auth;
                if (key instanceof String) {
                    auth = (String) key;
               } else {
                    if (authority == null) {
                        authority = key.getClass().getDeclaredField("authority");
                        authority.setAccessible(true);
                   }
                    auth = (String) authority.get(key);
               }
             //1
                if (auth.equals(RemoteContentProvider.getAuthority(mContext))) {
                    if (provider == null) {
                        provider = val.getClass().getDeclaredField("mProvider");
                        provider.setAccessible(true);
                   }
                    IContentProvider rawProvider = (IContentProvider) provider.get(val);
                    IContentProvider proxy = IContentProviderProxy.newInstance(mContext, rawProvider);
                    mIContentProvider = proxy;
                    Log.d(TAG, "hookIContentProvider succeed : " + mIContentProvider);
                    break;
               }
           }
       } catch (Exception e) {
            Log.w(TAG, e);
       }
   }

该方法的核心是遍历ActivityThread类的mProviderMap,用IContentProviderProxy实例来代替掉IContentProvider。

代码1需要注意的是,RemoteContentProvider即为代理ContentProvider,如果此刻遍历的IContentProvider的auth和代理ContentProvider的auth一致,前面提到过系统通过auth来区分app,auth一致,可说明该IContentProvider即为目标IContentProvider。

IContentProviderProxy中有一个方法wrapperUri()比较重要。

    private void wrapperUri(Method method, Object[] args) {
        Uri uri = null;
        int index = 0;
        if (args != null) {
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Uri) {
                    uri = (Uri) args[i];
                    index = i;
                    break;
               }
           }
       }
​
        Bundle bundleInCallMethod = null;
        if (method.getName().equals("call")) {
            bundleInCallMethod = getBundleParameter(args);
            if (bundleInCallMethod != null) {
                String uriString = bundleInCallMethod.getString(RemoteContentProvider.KEY_WRAPPER_URI);
                if (uriString != null) {
                    uri = Uri.parse(uriString);
               }
           }
       }
​
        if (uri == null) {
            return;
       }
​
        PluginManager pluginManager = PluginManager.getInstance(mContext);
        ProviderInfo info = pluginManager.resolveContentProvider(uri.getAuthority(), 0);
        if (info != null) {
            String pkg = info.packageName;
            LoadedPlugin plugin = pluginManager.getLoadedPlugin(pkg);
         //1
            Uri wrapperUri = PluginContentResolver.wrapperUri(plugin, uri);
            if (method.getName().equals("call")) {
                bundleInCallMethod.putString(RemoteContentProvider.KEY_WRAPPER_URI, wrapperUri.toString());
           } else {
                args[index] = wrapperUri;
           }
       }
   }

代码1处调用了PluginContentResolver的wrapperUri()方法。

    public static Uri wrapperUri(LoadedPlugin loadedPlugin, Uri pluginUri) {
        String pkg = loadedPlugin.getPackageName();
        String pluginUriString = Uri.encode(pluginUri.toString());
      //2
        StringBuilder builder = new StringBuilder(RemoteContentProvider.getUri(loadedPlugin.getHostContext()));
        builder.append("/?plugin=" + loadedPlugin.getLocation());
        builder.append("&pkg=" + pkg);
        builder.append("&uri=" + pluginUriString);
        Uri wrapperUri = Uri.parse(builder.toString());
        return wrapperUri;
   }

代码2处调用了RemoteContentProvider的getUri()方法。

    public static String getAuthority(Context context) {
        return context.getPackageName() + ".VirtualAPK.Provider";
   }
    
    public static String getUri(Context context) {
        return "content://" + getAuthority(context);
   }

RemoteContentProvider作为代理ContentProvider,需要在AndroidManifest进行声明。

<provider
            android:exported="false"
            android:name="com.didi.virtualapk.delegate.RemoteContentProvider"
            android:authorities="${applicationId}.VirtualAPK.Provider"
            android:process=":daemon" />

这是其声明内容,可以看见其auth与RemoteContentProvider的getUri()是一致的。

因此通过IContentProviderProxy启动插件ContentProvider的时候都会首先启动代理ContentProvider。

启动插件ContentProvider

从上述分析我们可以知道,每次我们获取到的ContentProvider都是代理ContentProvider,因此调用的都是RemoteContentProvider的query()方法,该方法源码如下所示.

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        ContentProvider provider = getContentProvider(uri);
        Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI));
        if (provider != null) {
            return provider.query(pluginUri, projection, selection, selectionArgs, sortOrder);
       }
        return null;
   }

由于该方法的目的是进行代理分发,启动插件ContentProvider,因此首先通过getContentProvider()获取插件ContentProvider,然后再调用插件ContentProvider的query()方法。

private static Map<String, ContentProvider> sCachedProviders = new HashMap<>();
private ContentProvider getContentProvider(final Uri uri) {
        final PluginManager pluginManager = PluginManager.getInstance(getContext());
        Uri pluginUri = Uri.parse(uri.getQueryParameter(KEY_URI));
 //1
        final String auth = pluginUri.getAuthority();
 //2
        ContentProvider cachedProvider = sCachedProviders.get(auth);
        if (cachedProvider != null) {
            return cachedProvider;
       }
​
        synchronized (sCachedProviders) {
            LoadedPlugin plugin = pluginManager.getLoadedPlugin(uri.getQueryParameter(KEY_PKG));
            if (plugin == null) {
                try {
                    pluginManager.loadPlugin(new File(uri.getQueryParameter(KEY_PLUGIN)));
               } catch (Exception e) {
                    Log.w(TAG, e);
               }
           }
 //3
            final ProviderInfo providerInfo = pluginManager.resolveContentProvider(auth, 0);
            if (providerInfo != null) {
                RunUtil.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                          
                            LoadedPlugin loadedPlugin = pluginManager.getLoadedPlugin(uri.getQueryParameter(KEY_PKG));
                            ContentProvider contentProvider = (ContentProvider) Class.forName(providerInfo.name).newInstance();
                            contentProvider.attachInfo(loadedPlugin.getPluginContext(), providerInfo);
                            sCachedProviders.put(auth, contentProvider);
                       } catch (Exception e) {
                            Log.w(TAG, e);
                       }
                   }
               }, true);
                return sCachedProviders.get(auth);
           }
       }
​
        return null;
   }
  • 1处代码从uri获取auth。
  • 2处代码查询缓存中是否有auth相对应的ContentProvider,有就立马返回。
  • 3处代码判断插件ContentProvider是否存在,如果存在则直接实例化。

到这里,插件ContentProvider就启动完成了。

参考资料

1.《Android进阶解密》刘望舒

2.《第一行代码》郭霖

作者:安卓学习从入门到入土

%s 个评论

要回复文章请先登录注册