Android插件实例——360 DroidPlugin详解

在中国找到钱不难,但你的一个点子不意味着是一个创业。你谈一个再好的想法,比如我今天谈一个创意说,新浪为什么不收购GOOGLE呢?这个创意很好。新浪一收购GOOGLE,是不是新浪就变成老大了?你从哪儿弄来钱?怎么去整合GOOGLE呢;

之前写过有关于Android 插件方向的文章,解析了一下Android的插件原理与运行方式。很多小伙伴都问我,为什么不把我制作的插件放到Github上,让大家共享一下。

我只能说,大哥啊,这个插件是我在公司研发的时候制作的,商业机密,不能开源啊。

刚好,最近逛github的时候,看到了奇虎360手机助手团队的一个Android插件开源项目。今天,我们就具体的分析一下它的原理与实现逻辑。让大家更清楚的了解,一个Android插件的构造。


360 Android 插件项目 DroidPlugin

这个框架是奇虎360手机助手团队,最近在github上开源出来的Android插件框架。这种精神是很值得鼓励的。

github地址为: https://github.com/Qihoo360/DroidPlugin

好,一下纯属引入了官方的说明:

说明

DroidPlugin是360手机助手在Android系统上实现了一种新的插件机制:

它可以在无需安装、修改的情况下运行APK文件,此机制对改进大型APP的架构,实现多团队协作开发具有一定的好处。

特点

  1. 支持Androd 2.3以上系统
  2. 插件APK完全不需做任何修改,可以独立安装运行、也可以做插件运行。要以插件模式运行某个APK,你无需重新编译、无需知道其源码。
  3. 插件的四大组件完全不需要在Host程序中注册,支持Service、Activity、BroadcastReceiver、ContentProvider四大组件
  4. 插件之间、Host程序与插件之间会互相认为对方已经”安装”在系统上了。
  5. API低侵入性:极少的API。HOST程序只是需要一行代码即可集成Droid Plugin
  6. 超强隔离:插件之间、插件与Host之间完全的代码级别的隔离:不能互相调用对方的代码。通讯只能使用Android系统级别的通讯方法。
  7. 支持所有系统API 资源完全隔离:插件之间、与Host之间实现了资源完全隔离,不会出现资源窜用的情况。
  8. 实现了进程管理,插件的空进程会被及时回收,占用内存低。
  9. 插件的静态广播会被当作动态处理,如果插件没有运行(即没有插件进程运行),其静态广播也永远不回被触发。

那么这个插件框架,和我们之前所说的插件框架,有什么特点?它的实现方式是什么样的?这里我们从源码来分析一下。


插件原理——DexClassLoader

之前,我们也讨论过,一个Android的apk插件是个什么形式。主要是三点,我总结如下:

- DexLoader动态的加载dex文件进入vm

- AndroidManifest.xml中预注册一些将要使用的四大组件(方法很low,其实有更好的)

- 针对四大组件,分别进行抽象代理,proxy

- Apk安装还需要单独处理native的,so文件。

- 宿主需要处理好Resource,保证R文件的正确使用。

360这个Android插件系统是否和我们之前讨论的类似呢?这里我们从IPluginManagerImpl.java文件中,我们能够看到其真正的安装apk插件的方法——installPackage。具体方法详解如下所示:

    /**
      * @param filepath 插件Apk的路径
      * @param flags Flag
      *
      */
    @Override
    public int installPackage(String filepath, int flags) throws RemoteException {
        //install plugin
        String apkfile = null;
        try {
            // 验证合法性
            PackageManager pm = mContext.getPackageManager();
            PackageInfo info = pm.getPackageArchiveInfo(filepath, 0);
            if (info == null) {
                return PackageManagerCompat.INSTALL_FAILED_INVALID_APK;
            }

            apkfile = PluginDirHelper.getPluginApkFile(mContext, info.packageName);

            if ((flags & PackageManagerCompat.INSTALL_REPLACE_EXISTING) != 0) {
                // 新安装的插件

                // 停止,删除插件的缓存
                forceStopPackage(info.packageName);
                if (mPluginCache.containsKey(info.packageName)) {
                    deleteApplicationCacheFiles(info.packageName, null);
                }
                new File(apkfile).delete();
                Utils.copyFile(filepath, apkfile);
                PluginPackageParser parser = new PluginPackageParser(mContext, new File(apkfile));
                parser.collectCertificates(0);
                PackageInfo pkgInfo = parser.getPackageInfo(PackageManager.GET_PERMISSIONS | PackageManager.GET_SIGNATURES);

                // 验证申请的permission,宿主中是否存在,不存在不让安装。
                if (pkgInfo != null && pkgInfo.requestedPermissions != null && pkgInfo.requestedPermissions.length > 0) {
                    for (String requestedPermission : pkgInfo.requestedPermissions) {
                        boolean b = false;
                        try {
                            b = pm.getPermissionInfo(requestedPermission, 0) != null;
                        } catch (NameNotFoundException e) {
                        }
                        if (!mHostRequestedPermission.contains(requestedPermission) && b) {
                            Log.e(TAG, "No Permission %s", requestedPermission);
                            new File(apkfile).delete();
                            return PluginManager.INSTALL_FAILED_NO_REQUESTEDPERMISSION;
                        }
                    }
                }

                // 保存签名
                saveSignatures(pkgInfo);
//                if (pkgInfo.reqFeatures != null && pkgInfo.reqFeatures.length > 0) {
//                    for (FeatureInfo reqFeature : pkgInfo.reqFeatures) {
//                        Log.e(TAG, "reqFeature name=%s,flags=%s,glesVersion=%s", reqFeature.name, reqFeature.flags, reqFeature.getGlEsVersion());
//                    }
//                }

                // 拷贝插件中的native库文件。
                copyNativeLibs(mContext, apkfile, parser.getApplicationInfo(0));
                // opt Dex文件,并使用DexClassLoader动态的载入类代码。
                dexOpt(mContext, apkfile, parser);
                mPluginCache.put(parser.getPackageName(), parser);

                // 回调函数,通知插件安装
                mActivityManagerService.onPkgInstalled(mPluginCache, parser, parser.getPackageName());
                sendInstalledBroadcast(info.packageName);
                return PackageManagerCompat.INSTALL_SUCCEEDED;
            } else {
                // 以下是插件,覆盖安装的过程。即更新的过程。与新安装类似。不做解释了。

                if (mPluginCache.containsKey(info.packageName)) {
                    return PackageManagerCompat.INSTALL_FAILED_ALREADY_EXISTS;
                } else {
                    forceStopPackage(info.packageName);
                    new File(apkfile).delete();
                    Utils.copyFile(filepath, apkfile);
                    PluginPackageParser parser = new PluginPackageParser(mContext, new File(apkfile));
                    parser.collectCertificates(0);
                    PackageInfo pkgInfo = parser.getPackageInfo(PackageManager.GET_PERMISSIONS | PackageManager.GET_SIGNATURES);
                    if (pkgInfo != null && pkgInfo.requestedPermissions != null && pkgInfo.requestedPermissions.length > 0) {
                        for (String requestedPermission : pkgInfo.requestedPermissions) {
                            boolean b = false;
                            try {
                                b = pm.getPermissionInfo(requestedPermission, 0) != null;
                            } catch (NameNotFoundException e) {
                            }
                            if (!mHostRequestedPermission.contains(requestedPermission) && b) {
                                Log.e(TAG, "No Permission %s", requestedPermission);
                                new File(apkfile).delete();
                                return PluginManager.INSTALL_FAILED_NO_REQUESTEDPERMISSION;
                            }
                        }
                    }
                    saveSignatures(pkgInfo);
//                    if (pkgInfo.reqFeatures != null && pkgInfo.reqFeatures.length > 0) {
//                        for (FeatureInfo reqFeature : pkgInfo.reqFeatures) {
//                            Log.e(TAG, "reqFeature name=%s,flags=%s,glesVersion=%s", reqFeature.name, reqFeature.flags, reqFeature.getGlEsVersion());
//                        }
//                    }

                    copyNativeLibs(mContext, apkfile, parser.getApplicationInfo(0));
                    dexOpt(mContext, apkfile, parser);
                    mPluginCache.put(parser.getPackageName(), parser);
                    mActivityManagerService.onPkgInstalled(mPluginCache, parser, parser.getPackageName());
                    sendInstalledBroadcast(info.packageName);
                    return PackageManagerCompat.INSTALL_SUCCEEDED;
                }
            }
        } catch (Exception e) {
            if (apkfile != null) {
                new File(apkfile).delete();
            }
            handleException(e);
            return PackageManagerCompat.INSTALL_FAILED_INTERNAL_ERROR;
        }
    }

以下是dexOpt方法的具体内容,简单的贴一下,不做说明了。

    private void dexOpt(Context hostContext, String apkfile, PluginPackageParser parser) throws Exception {
        String packageName = parser.getPackageName();
        String optimizedDirectory = PluginDirHelper.getPluginDalvikCacheDir(hostContext, packageName);
        String libraryPath = PluginDirHelper.getPluginNativeLibraryDir(hostContext, packageName);
        ClassLoader classloader = new PluginClassLoader(apkfile, optimizedDirectory, libraryPath, ClassLoader.getSystemClassLoader());
//        DexFile dexFile = DexFile.loadDex(apkfile, PluginDirHelper.getPluginDalvikCacheFile(mContext, parser.getPackageName()), 0);
//        Log.e(TAG, "dexFile=%s,1=%s,2=%s", dexFile, DexFile.isDexOptNeeded(apkfile), DexFile.isDexOptNeeded(PluginDirHelper.getPluginDalvikCacheFile(mContext, parser.getPackageName())));
    }

占坑——预注册四大组件

360 Android插件DroidPlugin中,主要的四大组件代理方式还是占坑。这里所说的占坑就是指,在AndroidManifest.xml中预注册以下可能会用到的信息。

ps.这个方式不是特别好。如果大家对为什么要预注册这个问题不是很了解,或者不了解为什么不好。可以看一下我的博客,我会持续更新。 http://blog.csdn.net/yzzst/article/details/45582315

OK,下面是我们从DroidPlugin的Test项目中,截取出来的AndroidManifest.xml代码,我们看看什么叫占坑,怎么占坑。代码如下所示:

 <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MyActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".ServiceTest1"
            android:label="服务测试" />
        <activity
            android:name=".ContentProviderTest"
            android:label="ContentProvider测试" />
        <activity
            android:name=".BroadcastReceiverTest"
            android:label="广播测试" />
        <activity
            android:name=".NotificationTest"
            android:label="通知测试" />
        <activity
            android:name=".ServiceTest2"
            android:label="多进程服务测试" />
        <activity
            android:name=".ContentProviderTest2"
            android:label="多进程ContentProvider测试" />

        <!-- 预声明不同process的Service -->
        <service android:name=".Service1" />
        <service android:name=".Service2" />
        <service
            android:name=".Service3"
            android:process=":Process2" />
        <service
            android:name=".Service4"
            android:process=":Process2" />

        <!-- 预声明两种process的Provider -->
        <provider
            android:name=".MyContentProvider1"
            android:authorities="com.example.ApiTest.MyContentProvider1" />
        <provider
            android:name=".MyContentProvider2"
            android:authorities="com.example.ApiTest.MyContentProvider2"
            android:process=":Process2" />

        <receiver android:name=".StaticBroadcastReceiver" >
            <intent-filter>
                <action android:name="com.example.ApiTest.StaticBroadcastReceiver" />
            </intent-filter>
        </receiver>

        <!-- 预声明各种launchMode的Activity -->
        <activity
            android:name=".StandardActivity"
            android:label="@string/title_activity_standard" />
        <activity
            android:name=".SingleTopActivity"
            android:label="@string/title_activity_single_top"
            android:launchMode="singleTop" />
        <activity
            android:name=".SingleTaskActivity"
            android:label="@string/title_activity_single_task"
            android:launchMode="singleTask" />
        <activity
            android:name=".SingleInstanceActivity"
            android:label="@string/title_activity_single_instance"
            android:launchMode="singleInstance" />
        <activity
            android:name=".ActivityTestActivity"
            android:label="@string/title_activity_activity_test" >
        </activity>
</application>

大家都看的出来,虽然简单的插件已经完全够用了。但是这种方式确实不是很好。


Hook 系统API方法

DroidPlugin中最重要的就是,为了让插件中的方法能够正确的调用。DroidPlugin把所有的,方法都在内部Hook并替换掉了。

等等,为什么不能够正确的调用?

因为,这个是一个插件系统,Activity是虚拟Proxy出来的。四大组件,并不是真正的,即Context拿到不一定是真的。系统也不会有此类的回调。所以,很多单例都需要Context才能使用。

比如:InputMethodManager.getInstance(Context context);

这时,插件中的代码将不能够直接运行。当然,还有其他原因。

具体的替换和操作方法,我们能够在 HookFactory.java文件中installHook方法中,看到整个DroidPlugin的具体操作如下所示:

public final void installHook(Context context, ClassLoader classLoader) throws Throwable {
        installHook(new IClipboardBinderHook(context), classLoader);
        //for INotificationManager
        installHook(new INotificationManagerBinderHook(context), classLoader);
        installHook(new IMountServiceBinder(context), classLoader);
        installHook(new IAudioServiceBinderHook(context), classLoader);
        installHook(new IContentServiceBinderHook(context), classLoader);
        installHook(new IWindowManagerBinderHook(context), classLoader);
        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP_MR1) {
            installHook(new IGraphicsStatsBinderHook(context), classLoader);
        }
        if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
            installHook(new IMediaRouterServiceBinderHook(context), classLoader);
        }
        if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
            installHook(new ISessionManagerBinderHook(context), classLoader);
        }
        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
            installHook(new IWifiManagerBinderHook(context), classLoader);
        }

        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2) {
            installHook(new IInputMethodManagerBinderHook(context), classLoader);
        }

        installHook(new IPackageManagerHook(context), classLoader);
        installHook(new IActivityManagerHook(context), classLoader);
        installHook(new PluginCallbackHook(context), classLoader);
        installHook(new InstrumentationHook(context), classLoader);
        installHook(new LibCoreHook(context), classLoader);

        installHook(new SQLiteDatabaseHook(context), classLoader);
    }

限制和缺陷

Ok,说到最后,还是得称赞一下360。国内大公司开源的使用框架原来越少了。他们的这一举动确实是造福了目前的Android开发者。

但是,对于DroidPlugin来说,目前还存在一下几种不足和缺点:

  1. 无法在插件中发送具有自定义资源的Notification,例如: a. 带自定义RemoteLayout的Notification b.

    图标通过R.drawable.XXX指定的通知(插件系统会自动将其转化为Bitmap)

  2. 无法在插件中注册一些具有特殊Intent

    Filter的Service、Activity、BroadcastReceiver、ContentProvider等组件以供Android系统、已经安装的其他APP调用。

  3. 对Activity的LaunchMode支持不够好,Activity

    Stack管理存在一定缺陷。Activity的onNewIntent函数可能不会被触发。 (此为BUG,未来会修复)

  4. 缺乏对Native层的Hook,对某些带native代码的apk支持不好,可能无法运行。比如一部分游戏(cocos2d、unity3d开发的游戏估计都用不了)无法当作插件运行。


当然,看我说的再多,也只是我的个人理解,有兴趣的同学,直接去github clone一份代码阅读阅读吧。

github地址为: https://github.com/Qihoo360/DroidPlugin

/*

* @author zhoushengtao(周圣韬)

* @since 2015年8月31日 凌晨 00:46:42

* @weixin stchou_zst

* @blog http://blog.csdn.net/yzzst

* @交流学习QQ群:341989536

* @私人QQ:445914891

/

版权声明:转载请标注:http://blog.csdn.net/yzzst 。 本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-03 22:54:23

Android插件实例——360 DroidPlugin详解的相关文章

Android插件实例——360 DroidPlugin具体解释

在中国找到钱不难,但你的一个点子不意味着是一个创业.你谈一个再好的想法,比方我今天谈一个创意说,新浪为什么不收购GOOGLE呢?这个创意非常好.新浪一收购GOOGLE.是不是新浪就变成老大了?你从哪儿弄来钱?怎么去整合GOOGLE呢: 之前写过有关于Android 插件方向的文章,解析了一下Android的插件原理与执行方式.非常多小伙伴都问我.为什么不把我制作的插件放到Github上,让大家共享一下. 我仅仅能说.大哥啊,这个插件是我在公司研发的时候制作的,商业机密.不能开源啊. 刚好.近期逛

[Android]Message,MessageQueue,Looper,Handler详解+实例

转http://www.eoeandroid.com/forum-viewthread-tid-49595-highlight-looper.html 一.几个关键概念 1.MessageQueue:是一种数据结构,见名知义,就是一个消息队列,存放消息的地方.每一个线程最多只可以拥有一个MessageQueue数据结构. 创建一个线程的时候,并不会自动创建其MessageQueue.通常使用一个Looper对象对该线程的MessageQueue进行管理.主线程创建时,会创建一 个默认的Loope

Android Animations 视图动画使用详解!!!

转自:http://www.open-open.com/lib/view/open1335777066015.html Android Animations 视图动画使用详解 一.动画类型 Android的animation由四种类型组成:alpha.scale.translate.rotate XML配置文件中 alpha 渐变透明度动画效果 scale 渐变尺寸伸缩动画效果 translate 画面转换位置移动动画效果 rotate 画面转移旋转动画效果 Java Code代码中 Alpha

Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)

1 背景 还记得前一篇<Android触摸屏事件派发机制详解与源码分析一(View篇)>中关于透过源码继续进阶实例验证模块中存在的点击Button却触发了LinearLayout的事件疑惑吗?当时说了,在那一篇咱们只讨论View的触摸事件派发机制,这个疑惑留在了这一篇解释,也就是ViewGroup的事件派发机制. PS:阅读本篇前建议先查看前一篇<Android触摸屏事件派发机制详解与源码分析一(View篇)>,这一篇承接上一篇. 关于View与ViewGroup的区别在前一篇的A

Android开发之通知栏Notification详解

Notification的用法  --- 状态栏通知 发送一个状态栏通知必须的两个类: 1. NotificationManager   --- 状态栏通知的管理类,负责发通知,清除通知等 NotificationManager : 是一个系统Service,必须通过 context.getSystemService(NOTIFICATION_SERVICE)方法获取 NotificationManager notificationManager = (NotificationManager)

Android开发之SQLite数据库详解

Android开发之SQLite数据库详解 请尊重他人的劳动成果,转载请注明出处:Android开发之SQLite数据库详解 http://blog.csdn.net/fengyuzhengfan/article/details/40194393 Android系统集成了一个轻量级的数据库:SQLite, SQLite并不想成为像Oracle.MySQL那样的专业数据库.SQLite只是一个嵌入式的数据库引擎,专门适用于资源有限的设备上(如手机.PDA等)适量数据存取. 虽然SQLite支持绝大

[Android新手区] SQLite 操作详解--SQL语法

该文章完全摘自转自:北大青鸟[Android新手区] SQLite 操作详解--SQL语法  :http://home.bdqn.cn/thread-49363-1-1.html SQLite库可以解析大部分标准SQL语言.但它也省去了一些特性并且加入了一些自己的新特性.这篇文档就是试图描述那些SQLite支持/不支持的SQL语法的.查看关键字列表. 如下语法表格中,纯文本用蓝色粗体显示.非终极符号为斜体红色.作为语法一部分的运算符用黑色Roman字体表示. 这篇文档只是对SQLite实现的SQ

Android Design Support Library使用详解

Android Design Support Library使用详解 Google在2015的IO大会上,给我们带来了更加详细的Material Design设计规范,同时,也给我们带来了全新的Android Design Support Library,在这个support库里面,Google给我们提供了更加规范的MD设计风格的控件.最重要的是,Android Design Support Library的兼容性更广,直接可以向下兼容到Android 2.2.这不得不说是一个良心之作. 使用S

Android 四大组件之Service详解

                   Android四大组件之Service详解    来这实习已经10多天了,今天整理整理学习时的Android笔记.正所谓好记性不如烂笔头,今天来说说service组件. service可以在和多场合的应用中使用,比如播放多媒体的时候用户启动了其他Activity这个时候程序要在后台继续播放,比如检测SD卡上文件的变化,再或者在后台记录你地理信息位置的改变等等,总之服务嘛,总是藏在后头的. Service是在一段不定的时间运行在后台,不和用户交互应用组件.每个