android简易轻量插件方案:嵌入插件的页面

需求:开发一个宿主app,用来嵌入第三方app的某些页面。例如在一个宿主是Fragment的容器中,嵌入一个插件的View。宿主跟插件由不同的团队开发完成。
面对这样一个需求,既然是第三方开发的插件。势必需要尽量满足以下条件。
  1. 宿主和插件的耦合性极低。因为与其他团队沟通是成本很高的一件事,尽量做到耦合低,以免出现众多适配问题。

  2. 保证插件的完整性和独立性。第三方app插件很可能已经是一个成熟的项目,若要变成我们的插件,需要尽可能较少修改代码。

可能的解决方案。
可能的方案之1:网上众多流行的免安装apk插件法。

例如:smallAndroid-Plugin-FrameworkDynamicAPKDroidPlugin等。这些框架又分为支持独立插件和非独立插件。

独立插件:即为独立的apk, 插件与app无异,插件框架更像一个沙盒容器,比如类似Lbe的平行空间, 360手机助手加载的均为此类插件。但是这种方式都是独立拉起一个新的页面,宿主并不能知道插件里面是什么,不能支持我们的嵌入第三方app某些页面的需求。

非独立插件:宿主与插件间有一定的约定规范,开发插件需要遵循制定的规则来进行开发,具有一定的弱侵入性,这种方式主要用于产品内部的业务模块插件化解耦。这种方式不能支持第三方团队开发app的需求。

另外,到目前为止,没有一个非常稳定的插件方案,本人也尝试过几个插件方案,遇坑无数,例如,对第三方lib兼容性不好,四大组件支持不好,文档缺失需要大量时间研究源码等,遂无奈放弃。

可能的方案之2: Widget

Widget是google官方支持的加载第三方插件的方法。但是由于需要跨进程,所以对于支持的View有限。页面中仅仅支持头部包含@RemoteView的View,例如TextViewImageView等基础View。

Widget方案要求第三方App团队提供Widget接口以供宿主使用。但是本人在尝试Widget方案时,有一个App团队需要提供的页面非常复杂,包含众多类似fresco中的View。并且此项目已经开发成熟,重构起来代价很大。

......

既然市面上的插件方案无法解决需求,那么我们就仔仔细细回归到需求上面来,我们要求的插件并没有要求不安装,只需要加载出这个插件apk的一个界面出来就可以了......emm......那么方案就自己想好了!于是...第三种自研方案出炉了!

可能的方案之3: 自研加载第三方apk的View

偶然之间看源码api的时候,发现了Context.java中这样一个api:

public abstract Context createPackageContext(String packageName,
            @CreatePackageOptions int flags) throws PackageManager.NameNotFoundException;

看这个方法的官方文档:

Return a new Context object for the given application name. This Context is the same as what the named application gets when it is launched, containing the same resources and class loader. Each call to this method returns a new instance of a Context object; Context objects are not shared, however they share common state (Resources, ClassLoader, etc) so the Context instance itself is fairly lightweight.

提炼一下这一段英文

  • 根据包名创建一个新的Context出来。
  • 这个Context拥有该包名指定的资源文件(resources )和类加载器(class loader)
  • 这个Context是轻量级的,只是拥有上面一条说的这些状态,不是共享的,可以重复创建。

简短来说就是可以通过包名得到这个app对应的资源,这不是正是我们这个需求所需要的东西吗!第三方app需要提供一个包名,让宿主通过这个包名创建一个Context,通过Context对象得到Resources对象以后,通过Resources.java中的
public int getIdentifier(String name, String defType, String defPackage)方法得到资源文件的id,有了id可以获取View了。当然还需要提供一个布局文件Layout的名称。下面直接贴代码:

/**
 * 通过插件的包名和布局名称获取这个布局的View对象。
 *
 * @param parentContext 宿主的Context对象,用来创建插件的Context。
 * @param containerView 父布局,映射插件View的时候作为参数传递进去,可以准确测量子布局的大小和位置。
 * @param packageName   插件的包名
 * @param layoutName    插件的布局名称。
 * @return 插件的view对象
 */
private View loadPlugin(Context parentContext, ViewGroup containerView, String packageName, String layoutName) {
    View pluginView = null;
    try {
        // 创建插件的Context
        Context pluginContext = parentContext.createPackageContext(packageName,
                Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);

        // 获取插件的布局文件id
        Resources r = pluginContext.getResources();
        int id = r.getIdentifier(layoutName, "layout", packageName);
        // 根据id获取View
        LayoutInflater layoutInflater = LayoutInflater.from(pluginContext);
        pluginView = layoutInflater.inflate(id, containerView, false);

    } catch (Exception e) {
        e.printStackTrace();
    }
    return pluginView;
}

既然代码出来了,咱们就来追溯一下看看这个插件的资源是如何被宿主加载出来,要知其然更知其所以然,知道原理对以后可能遇到的坑会提供很多解决思路。

先看看Context.createPackageContext方法,Context大部分方法都是由一个ContextImpl.java来代理实现的。要看实现方法,就直接到ContextImpl.java中去搜索。搜了一下,createPackageContext,实际上是调用了一个createPackageContextAsUser的方法:

ContextImpl.java

@Override
public Context createPackageContextAsUser(String packageName, int flags, UserHandle user)
        throws NameNotFoundException {
    if (packageName.equals("system") || packageName.equals("android")) {
        return new ContextImpl(this, mMainThread, mPackageInfo, mActivityToken,
                user, flags, mDisplay, null, Display.INVALID_DISPLAY);
    }

    LoadedApk pi = mMainThread.getPackageInfo(packageName, mResources.getCompatibilityInfo(),
            flags | CONTEXT_REGISTER_PACKAGE, user.getIdentifier());
    if (pi != null) {
        ContextImpl c = new ContextImpl(this, mMainThread, pi, mActivityToken,
                user, flags, mDisplay, null, Display.INVALID_DISPLAY);
        if (c.mResources != null) {
            return c;
        }
    }

    // Should be a better exception.
    throw new PackageManager.NameNotFoundException(
            "Application package " + packageName + " not found");
}

关键来了,这个方法实际上是用传递进去的包名创建了一个LoadedApk对象,然后用这个LoadedApk对象new出来一个新的ContextImpl 。

LoadedApk pi = mMainThread.getPackageInfo(packageName, mResources.getCompatibilityInfo(),
            flags | CONTEXT_REGISTER_PACKAGE, user.getIdentifier());
ContextImpl c = new ContextImpl(this, mMainThread, pi, mActivityToken,
                user, flags, mDisplay, null, Display.INVALID_DISPLAY);

再看看LoadedApk,这个LoadedApk对象到底是何方神圣?它又是怎么被创建出来的?
我们先看看他的创建,代码太多我省去了一些:

ActivityThread.java

public final LoadedApk getPackageInfo(String packageName, CompatibilityInfo compatInfo,
        int flags, int userId) {
    // 省略一系列读取缓存的代码
    // ...
    // 获取已经安装的ApplicationInfo信息  
    ApplicationInfo ai = null;
    try {
        ai = getPackageManager().getApplicationInfo(packageName,
                PackageManager.GET_SHARED_LIBRARY_FILES
                        | PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
                userId);
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
    // 如果这个app安装过了,就创建LoadedApk对象,否则返回null。
    if (ai != null) {
        return getPackageInfo(ai, compatInfo, flags);
    }
    return null;
}

看上面添加注释的地方,又多出来个ApplicationInfo,它是什么呢?官方注释可以说是很简单粗暴了。ApplicationInfo其实就是对应AndroidManifest.xml的application标签。这里只能加载已经安装过的ApplicationInfo。由于我们的需求中,插件是已经安装过的,所以这里可以直接得到ApplicationInfo。如果插件是未安装的,则可以利用PackageParser.javaApplicationInfo generateApplicationInfo(Package p, int flags, PackageUserState state)方法来构建,但是会复杂很多,本文就不深究了先。

还是回到LoadedApk,上面的代码中,创建LoadedApk对象是指向了另一个方法:

ActivityThread.java

public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo,
        int flags) {
    // 省略一系列对于进程的检查的code。
    // ...
    return getPackageInfo(ai, compatInfo, null, securityViolation, includeCode,
            registerPackage);
}

再往下跟:

ActivitThread.java

private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
        ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
        boolean registerPackage) {
        // 省略了读取缓存的代码
        // ...

        LoadedApk packageInfo = ref != null ? ref.get() : null;
        if (packageInfo == null || (packageInfo.mResources != null
                && !packageInfo.mResources.getAssets().isUpToDate())) {
            // LoadedApk的创建 
            packageInfo =
                new LoadedApk(this, aInfo, compatInfo, baseLoader,
                        securityViolation, includeCode &&
                        (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);

            // 如果是系统的LoadedApk的额外处理
            // ...
            
            // 保存缓存
            // ...
        }
        return packageInfo;
    }
}

再看调用的这个LoadedApk的构造方法:

LoadedApk.java

LoadedApk(ActivityThread activityThread, ApplicationInfo aInfo,
            CompatibilityInfo compatInfo, ClassLoader baseLoader,
            boolean securityViolation, boolean includeCode, boolean registerPackage)

LoadedApk的跟踪结论:跟了这么多代码,现在可以得出结论了,其实目的就只是为了看看LoadedApk对象在被创建的时候,到底接收了哪些参数进来了。很明显,这个对象接收了:

  • 来自宿主的ActivityThread
  • 来自宿主的CompatibilityInfo
  • 插件自己的ApplicationInfo,但是mApplication是null
  • 一个空的ClassLoader

再看ContextImpl的创建:

private ContextImpl(ContextImpl container, ActivityThread mainThread,
        LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
        Display display, Configuration overrideConfiguration, int createDisplayWithId) {
    mOuterContext = this;

    // If creator didn't specify which storage to use, use the default
    // location for application.
    // ...

    mMainThread = mainThread;
    mActivityToken = activityToken;
    mFlags = flags;

    if (user == null) {
        user = Process.myUserHandle();
    }
    mUser = user;

    mPackageInfo = packageInfo;
    mResourcesManager = ResourcesManager.getInstance();

    final int displayId = (createDisplayWithId != Display.INVALID_DISPLAY)
            ? createDisplayWithId
            : (display != null) ? display.getDisplayId() : Display.DEFAULT_DISPLAY;

    CompatibilityInfo compatInfo = null;
    if (container != null) {
        compatInfo = container.getDisplayAdjustments(displayId).getCompatibilityInfo();
    }
    if (compatInfo == null) {
        compatInfo = (displayId == Display.DEFAULT_DISPLAY)
                ? packageInfo.getCompatibilityInfo()
                : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
    }

    Resources resources = packageInfo.getResources(mainThread);
    if (resources != null) {
        if (displayId != Display.DEFAULT_DISPLAY
                || overrideConfiguration != null
                || (compatInfo != null && compatInfo.applicationScale
                        != resources.getCompatibilityInfo().applicationScale)) {

            if (container != null) {
                // This is a nested Context, so it can't be a base Activity context.
                // Just create a regular Resources object associated with the Activity.
                resources = mResourcesManager.getResources(
                        activityToken,
                        packageInfo.getResDir(),
                        packageInfo.getSplitResDirs(),
                        packageInfo.getOverlayDirs(),
                        packageInfo.getApplicationInfo().sharedLibraryFiles,
                        displayId,
                        overrideConfiguration,
                        compatInfo,
                        packageInfo.getClassLoader());
            } else {
                // This is not a nested Context, so it must be the root Activity context.
                // All other nested Contexts will inherit the configuration set here.
                resources = mResourcesManager.createBaseActivityResources(
                        activityToken,
                        packageInfo.getResDir(),
                        packageInfo.getSplitResDirs(),
                        packageInfo.getOverlayDirs(),
                        packageInfo.getApplicationInfo().sharedLibraryFiles,
                        displayId,
                        overrideConfiguration,
                        compatInfo,
                        packageInfo.getClassLoader());
            }
        }
    }
    mResources = resources;

    mDisplay = (createDisplayWithId == Display.INVALID_DISPLAY) ? display
            : mResourcesManager.getAdjustedDisplay(displayId, mResources.getDisplayAdjustments());

    if (container != null) {
        mBasePackageName = container.mBasePackageName;
        mOpPackageName = container.mOpPackageName;
    } else {
        mBasePackageName = packageInfo.mPackageName;
        ApplicationInfo ainfo = packageInfo.getApplicationInfo();
        if (ainfo.uid == Process.SYSTEM_UID && ainfo.uid != Process.myUid()) {
            // Special case: system components allow themselves to be loaded in to other
            // processes.  For purposes of app ops, we must then consider the context as
            // belonging to the package of this process, not the system itself, otherwise
            // the package+uid verifications in app ops will fail.
            mOpPackageName = ActivityThread.currentPackageName();
        } else {
            mOpPackageName = mBasePackageName;
        }
    }

    mContentResolver = new ApplicationContentResolver(this, mainThread, user);
}

以上代码代码关键点:Resources对象是由传进来的LoadedApk来创建的,其中包含了各种路径和控件,而这些又恰恰是LoadedApk的ApplicationInfo决定的,ApplicationInfo又是关于插件的对象。另外mOutContext对象是this,也就意味着它没有外壳,它代理了它自己。

这里Resources对象就跟插件关联上了!虽然感觉理所当然是这样的,但是理清楚这些源码的过程中,还是观察到了很多其他细节。

下面就来整理一下所有的结论吧:

  • 最终创建Context对象是一个ContextImpl对象,它没有一个外壳,我们可以帮它设置一个宿主(Activity)外壳进去。
  • 被插件Context加载出来的View的代码将会运行在宿主进程中。插件运行在宿主中的代码跟插件本体app将不会在不同进程,那么考虑到Android的沙盒机制,访问插件访问数据库的方式将会值得考虑。
  • 插件Context的LoadedApk对象中不包含Application对象,也就是Context.getApplicationContext的时候会返回null。毕竟不能保证插件不使用这个方法,所以我们可以利用LoadedApk的makeApplication方法制造一个Application。考虑到这会走Application的生命周期,所以性能损耗也是需要值得验证的。
  • 插件可以利用资源文件可以被宿主访问的机制,将一个Layout布局文件的根布局设置成自定义View,这样宿主在加载这个自定义View的时候,可以出发自定义View的构造方法。插件的初始化代码就可以在构造方法中来写。

对于宿主

宿主的工作量很轻,只需要知道插件的包名跟布局名称,不需要知道插件里面有什么内容,完全解耦就可以把插件的某一页加载进来。当然需要做的工作是给插件构建好上下文环境:ContextImpl,Activity,Application。

对于插件

如果是改造既有项目,将原本的页面改造成一个自定义View即可。如果是新项目,写一个自定义View即可,像Activity那样。

插件可以正常得到Activity和Application,也可以方便得到资源文件。但需要注意的点有自定义View中如果要访问data/data下的数据时访问最好使用ContentProvider。

在使用Context的时候需要注意,得到的Activity跟ContextImpl不同,Activity是宿主,而ContextImpl是一个代理。意味着,Activity没法访问资源,只能使用它的mWindow来弹Dialog和PopWindow等。而ContextImpl刚好跟Activity互补,它没有Activity的mWindow对象,但是他有访问资源文件的Resouces对象。

部分可参考代码以及注意事项

1. 宿主方
1.1 添加适当位置如下代码
/**
 * 加载插件
 *
 * @param container 将要添加插件的容器
 * @param packageName 第三方app的包名
 * @param layoutName 第三方app首页的布局名称。
 */
private View loadPlugin(ViewGroup container, String packageName, String layoutName) {

    Context parentContext = getContext();
    View v = null;
    try {
        // 创建一个ContextImpl对象,并注入plugin的LoadedApk对象。
        Context context = parentContext.createPackageContext(packageName,
                Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);

        // 为Context创建一个Application并注入,然后再手动调用Application的生命周期。
        makePluginApplication(context);

        // 使用plugin的Context来寻找资源id
        int id = context.getResources().getIdentifier(layoutName, "layout", packageName);
        // 加载plugin的资源
        v = LayoutInflater.from(context).inflate(id, container, false);

        // 注入一个Activity
        setActivity(context, this);
    } catch (Exception e) {
        e.printStackTrace();
    }
    if (v != null) {
        try {
            container.addView(v);
            return;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    Toast.makeText(parentContext, "加载插件失败", Toast.LENGTH_SHORT).show();
}

/**
 * 创建一个Application,包名已经包含在Context中了。然后通过各种反射将application设置到Context中。
 *
 * @param context 包含指定包名的Context
 */
private void makePluginApplication(Context context) throws ClassNotFoundException, NoSuchFieldException,
        IllegalArgumentException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    Class contextImplClass = Class.forName("android.app.ContextImpl");
    Field packageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
    packageInfoField.setAccessible(true);

    Object loadedWidgetApk = packageInfoField.get(context);
    Class loadedApkClass = Class.forName("android.app.LoadedApk");
    Method makeApplicationMethod = loadedApkClass.getMethod("makeApplication", boolean.class, Instrumentation.class);

    Instrumentation instrumentation = getInstrumentation();
    Application application = (Application) makeApplicationMethod.invoke(loadedWidgetApk, false, instrumentation);

    Class contextWrapperClass = Class.forName("android.content.ContextWrapper");
    Field mBaseField = contextWrapperClass.getDeclaredField("mBase");
    mBaseField.setAccessible(true);
    mBaseField.set(application, context);
}

/**
 * 反射得到ActivityThread的Instrument对象。(ActivityThread类不可访问)
 *
 * @return 得到的Instrument
 * @throws ClassNotFoundException
 * @throws NoSuchFieldException
 * @throws IllegalAccessException
 */
private Instrumentation getInstrumentation() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
    Class activityThreadClass = Class.forName("android.app.ActivityThread");
    Field activityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
    activityThreadField.setAccessible(true);
    Object activityThread = activityThreadField.get(activityThreadClass);

    Field instrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
    instrumentationField.setAccessible(true);
    return (Instrumentation) instrumentationField.get(activityThread);
}

 /**
 * 反射将Activity加入到context的mOuterContext中。
 * 注意:
 * 1.如果在inflate插件之前调用此方法,可能将导致插件资源文件错乱。所以等inflate完成之后,加入到宿主之前,注入Activity。
 * 2.由于第一点,导致,在插件中,构造方法中的context不包含Activity。onAttachedToWindow回调中context就包含Activity了。
 * 3.目前测试没有什么问题,测试时间较短,不知道会不会导致其他问题发生。
 *
 * @param context  这是一个ContextImpl对象,待注入Activity
 * @param activity 这是待注入的Activity。
 */
private void setActivity(Context context, Activity activity) {
    try {
        Class contextImplClass = Class.forName("android.app.ContextImpl");
        Field outerContextField = contextImplClass.getDeclaredField("mOuterContext");
        outerContextField.setAccessible(true);
        outerContextField.set(context, activity);

    } catch (Exception e) {
        e.printStackTrace();
    }
}
1.2 关闭AndroidStudio的InstantRun功能。

InstantRun的增量更新apk可能会导致加载插件失败,找不到自定义的View。

1.3 注入一个Activity对象(可选)。

parentContext.createPackageContext中宿主为插件创建的Context对象实际上只是一个ContextImpl对象,既不是Activity也不是Application。
如果插件的首页需要使用到Activity中的方法,比如getWindow。通过getContext(Context)方法得到的Context不是Activity对象,所以并不能获取到Window。所以需要使用到上述代码中的setActivity(Context,Activity),其作用是将插件Context中的mOuterContext对象替换成宿主的Activity对象。

2. 插件方
2.1 添加一个layout布局文件,作为传递给宿主的参数。

添加一个布局文件,布局文件只包含一个自定义View
类似这样plugin_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<com.test.PluginView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/plugin_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
2.2 新建PluginView.java

在PluginView的初始化中添加首页的布局文件和初始化工作。

/**
 * 将会加载到宿主app中的页面
 * Created by wanchi on 2017/2/6.
 */
public class CustomView extends FrameLayout implements View.OnClickListener {

    public static final String TAG = "tag_plugin";
    private TextView mDescTv;
    private Activity mHostActivity;
    private Context mContext;

    public CustomView(Context context) {
        this(context, null);
    }

    public CustomView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;
        initView();
        Log.d(TAG, "context1:" + getContext().getClass()); // it's ContextImpl.class
        Log.d(TAG, "context2:" + mContext); // it's ContextImpl.class
        Activity activity = getActivity(mContext);

        Log.d(TAG, "activity1:" + activity); // it's null
    }

    private void initView() {
        LayoutInflater.from(getContext()).inflate(R.layout.custom_layout, this);

        mDescTv = (TextView) findViewById(R.id.custom_view_description_tv);

        View navigateToOtherBtn = findViewById(R.id.custom_navigate_to_other_page_btn);
        View navigateToAidl = findViewById(R.id.custom_navigate_to_aidl_btn);
        navigateToAidl.setOnClickListener(this);
        navigateToOtherBtn.setOnClickListener(this);
    }

    @Nullable
    private Activity getActivity(Context context) {
        try {
            Class contextImplClass = Class.forName("android.app.ContextImpl");
            Field outerContextField = contextImplClass.getDeclaredField("mOuterContext");
            outerContextField.setAccessible(true);
            Activity activity = (Activity) outerContextField.get(context);
            return activity;
        } catch (Exception e) {
            Log.d(TAG, "exception:" + e);
            e.printStackTrace();
            return null;
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        Log.d(TAG, "context3:" + getContext().getClass()); // ContextImpl.class

        Activity activity = getActivity(mContext);
        Log.d(TAG, "activity2:" + activity); //Yes! get the activity!
        mHostActivity = activity;
        if (activity != null) {
            activity.getWindowManager();
        }

        View v1 = LayoutInflater.from(getContext()).inflate(R.layout.activity_second, null, false);
        Log.d(TAG, "v1:" + v1); // get the right layout
        // do something
        // requestData etc..
    }

    @Override
    protected void onDetachedFromWindow() {
        // do some recycling work
        super.onDetachedFromWindow();
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        if (id == R.id.custom_navigate_to_other_page_btn) {
            SecondActivity.navigateTo(getContext());
        } else if (id == R.id.custom_navigate_to_aidl_btn) {
            AidlTestActivity.start(getContext());
        }
    }
}
2.3 进程问题

插件首页中的代码会被加载到宿主中运行(宿主通过LoadedApk对象,寻找并加载插件的dex文件)。
所以插件的首页的代码将会运行在宿主进程中,如果通过插件首页打开了插件的二级页面Activity。那么需要注意,开启的二级Activity,将会是另一个进程:插件进程。也就是说插件首页和其他页面将会是跨进程。由此带来了一些影响:

  • 首页启动其他页面的Activity(跨进程),可以直接启动(设置android:exported="true")也可以隐式启动(设置Action)。
  • 插件首页访问插件的数据库、 Assets资源等与插件文件目录有关的资源时,将不可访问,建议使用ContentProvider来实现文件资源的访问。
  • 插件首页与其他页面通信,也视作跨进程通信。可使用的通信方式:广播,AIDL,Messenger等。
2.4 Context的使用

插件首页的Context使用,需要有一些注意事项。
首先Context有两种。

  • 宿主手动产生的Context:本质是替换过LoadedApk的一个ContextImpl对象,用来加载插件资源。在插件中getContext()即可获得。
  • 宿主的Activity:就是宿主本身,不可加载资源文件。但是可以获得当前Window对象,所以可以用来显示Toast,创建对话框等。由宿主注入到ContextImpl对象中,在插件首页中调用上一段代码中的方法getActivity(Context)即可得到。

在插件中显示Dialog的实例。

    private void showDialog() {
        if (mDialog == null) {
            // 用宿主中的Context来创建dialog。
            mDialog = new Dialog(mHostActivity);
            // 用插件中的Context来获得布局。
            LayoutInflater inflater = LayoutInflater.from(mContext);
            
            // 注意这里,由于没有传递Parent参数,映射的布局中,最外层的布局大小将会失效。
            View dialogView = inflater.inflate(R.layout.dialog_test, null);

            mDialog.setContentView(dialogView);
        }
        if (!mDialog.isShowing()) {
            mDialog.show();
        }
    }

结束

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,165评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,503评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,295评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,589评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,439评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,342评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,749评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,397评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,700评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,740评论 2 313
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,523评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,364评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,755评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,024评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,297评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,721评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,918评论 2 336

推荐阅读更多精彩内容