需求:开发一个宿主app,用来嵌入第三方app的某些页面。例如在一个宿主是Fragment的容器中,嵌入一个插件的View。宿主跟插件由不同的团队开发完成。
面对这样一个需求,既然是第三方开发的插件。势必需要尽量满足以下条件。
宿主和插件的耦合性极低。因为与其他团队沟通是成本很高的一件事,尽量做到耦合低,以免出现众多适配问题。
保证插件的完整性和独立性。第三方app插件很可能已经是一个成熟的项目,若要变成我们的插件,需要尽可能较少修改代码。
可能的解决方案。
可能的方案之1:网上众多流行的免安装apk插件法。
例如:small、Android-Plugin-Framework、DynamicAPK、DroidPlugin等。这些框架又分为支持独立插件和非独立插件。
独立插件:即为独立的apk, 插件与app无异,插件框架更像一个沙盒容器,比如类似Lbe的平行空间, 360手机助手加载的均为此类插件。但是这种方式都是独立拉起一个新的页面,宿主并不能知道插件里面是什么,不能支持我们的嵌入第三方app某些页面的需求。
非独立插件:宿主与插件间有一定的约定规范,开发插件需要遵循制定的规则来进行开发,具有一定的弱侵入性,这种方式主要用于产品内部的业务模块插件化解耦。这种方式不能支持第三方团队开发app的需求。
另外,到目前为止,没有一个非常稳定的插件方案,本人也尝试过几个插件方案,遇坑无数,例如,对第三方lib兼容性不好,四大组件支持不好,文档缺失需要大量时间研究源码等,遂无奈放弃。
可能的方案之2: Widget
Widget是google官方支持的加载第三方插件的方法。但是由于需要跨进程,所以对于支持的View有限。页面中仅仅支持头部包含@RemoteView的View,例如TextView、ImageView等基础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.java
的ApplicationInfo 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();
}
}
结束