├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml └── runConfigurations.xml ├── LICENSE ├── README.md ├── ZeusPlugin ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ ├── com │ └── android │ │ └── internal │ │ └── util │ │ └── Predicate.java │ └── zeus │ └── plugin │ ├── PluginConstant.java │ ├── PluginManager.java │ ├── PluginManifest.java │ ├── PluginUtil.java │ ├── ZeusBaseActivity.java │ ├── ZeusBaseApplication.java │ ├── ZeusClassLoader.java │ ├── ZeusHelper.java │ ├── ZeusHotfixClassLoader.java │ ├── ZeusInstrumentation.java │ ├── ZeusPlugin.java │ └── ZeusPluginClassLoader.java ├── aapt ├── aapt(linux64位版) ├── aapt(mac版) └── aapt(windows版).exe ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── public-xml.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── zeushotfix_test.apk │ ├── zeusplugin_test.apk │ └── zeusplugin_test_version2.apk │ ├── java │ └── zeus │ │ └── test │ │ ├── MainActivity.java │ │ ├── MyApplication.java │ │ ├── StringConstant.java │ │ ├── hotfix │ │ ├── TestHotFix.java │ │ ├── TestHotFixActivity.java │ │ └── TestHotfixActivity1.java │ │ ├── hotfixTest │ │ └── MyInterface.java │ │ └── plugin │ │ ├── TestPluginActivity.java │ │ └── TestView.java │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── layout │ ├── activity_main.xml │ ├── activity_plugin.xml │ └── activity_testhotfix.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── public.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshot └── demo.gif ├── settings.gradle ├── testhotfix ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── sdk-jars │ └── hotfix_sdk.jar └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── zeusplugin.meta │ ├── java │ └── zeus │ │ └── test │ │ └── hotfix │ │ ├── TestHotFix.java │ │ └── TestHotFixActivity.java │ └── res │ ├── layout │ └── activity_main.xml │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml └── testplugin ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── sdk-jars └── plugin_sdk.jar └── src └── main ├── AndroidManifest.xml ├── assets └── zeusplugin.meta ├── java └── zeus │ └── testplugin │ └── MainActivity.java └── res ├── layout └── activity_main.xml └── values ├── colors.xml ├── dimens.xml ├── strings.xml └── styles.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.ap_ 3 | 4 | # files for the dex VM 5 | *.dex 6 | 7 | # Java class files 8 | *.class 9 | 10 | # generated filesgit rm --cached 11 | bin/ 12 | gen/ 13 | 14 | # Local configuration file (sdk path, etc) 15 | local.properties 16 | 17 | # Eclipse project files 18 | .classpath 19 | .project 20 | 21 | # Proguard folder generated by Eclipse 22 | proguard/ 23 | 24 | # Intellij project files 25 | .gradle 26 | *.iml 27 | *.ipr 28 | *.iws 29 | .idea/ 30 | build/ 31 | /*/build/ 32 | 33 | #Mac OS 34 | .DS_Store -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | ZeusPlugin -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 1.7 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 zhangyue 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZeusPlugin 2 | 3 | **最精简的插件补丁框架,日活千万级app验证稳定** 4 | 5 | ![demo演示](./screenshot/demo.gif) 6 | 7 | ## 支持特性 8 | 9 | 1. 支持插件的安装、升级、卸载、版本管理 10 | 2. 支持插件调用宿主的类与资源。要在插件中使用宿主的资源ID,需要使用public.xml将资源ID固定,public.xml如何使用请自行搜索,并将该ID添加到sdk-jar中,如果只是插件调用宿主中的某个类,然后这个使用了宿主资源则不需处理。 11 | 3. 支持运行时动态升级加载插件。调用PluginManager.getPlugin(pluginId).install()安装完成之后,只需要调用`PluginManager.loadLastVersionPlugin(pluginName)`如果没加载过插件会加载最新插件,如果已加过老版本的该插件则会替换为该插件的最新版本。 12 | 4. 插件与宿主的关系和apk与android系统的关系接近。 13 | **如果插件中有与宿主重名的类,这个插件中的类只能被插件使用,宿主是不会使用插件中的类的。宿主只能通过显式loadClass的方式才能访问插件。** 14 | 5. 当插件版本过多又怕新插件在早期apk中不支持,应编写一个类CTS测试(google强制厂商执行的兼容性测试)的小插件,该插件中会调用所有之前插件用到的宿主中的所有方法和成员等等。如果该小程序跑过了则说明新版本apk兼容所有插件。 15 | 6. 支持so以及so的动态实时升级。 16 | 7. 插件与补丁支持加固方案,单dex或者多个dex文件情况,已对android 1.5以上版本(已适配最新的android P)和厂商定制的android系统进行了适配,适配了各种机型和厂商自己的系统(包括yunOS等)。测试无资源加载找不到的问题,存在极个别的第一次加载后类找不到的情况,尝试几次就可以了。(概率极低,<0.0001%) 17 | 8. 对性能无明显影响。经过在android 2.2及以上进行高强度测试,对性能无明显影响。 18 | 9. 支持bug fix的补丁功能,补丁修复最小单位是java中的class,补丁中可以有资源,也可以使用宿主的资源,它其实跟插件是一样的,只不过补丁的class与宿主的class重名了,发现重名就替换,支持单dex、多dex(方法数超了的情况)。补丁对性能有微弱影响(个人认为可以忽略),android 4.4及以上完全无影响。 19 | 10. 如果你的apk没有进行代码混淆,补丁也可以产生与插件相同的作用来进行功能的更新。 20 | 21 | ## 不支持的特性 22 | 23 | 1. 不支持插件中使用activity动画。如果要使用activity动画请将activity动画用到的xml文件放到宿主中,否则卡死。 24 | 2. 不支持插件有自己的Application,插件获取的是宿主的application。 25 | 3. 不支持动态升级插件的AndroidManifest.xml文件,所有试图修改AndroidManifest.xml的功能都需要升级宿主。不过这种情况很少,目前我们还没遇到过。 26 | 4. 不支持补丁实时加载,下次启动才能加载,否则内存中的对象会乱掉,如之前保存了A类的实例,现在A类已经被实时替换为B类了,那么之前的A类实例就不能转为B类了。 27 | 5. 不支持插件在xml使用宿主的自定义属性。(支持这个性价比太低,请使用其他替代方法) 28 | 6. 其他还不清楚,还请大家进行测试。 29 | 30 | ## 文档 31 | 32 | 使用方法及代码原理见 [Zeus Wiki](https://github.com/iReaderAndroid/ZeusPlugin/wiki) 33 | 34 | ## 由使用问题和其他技术问题,欢迎加群交流讨论 35 | 36 | > QQ群:`558449447`,添加请注明来自`ZeusPlugin` 37 | > 38 | > Android技术交流分享 39 | > 40 | > 欢迎各位使用,该项目会持续维护。 41 | > 42 | > 43 | > 另:欢迎加入[掌阅](http://www.zhangyue.com/jobs)大家庭,一起研究Android新技术。简历请发送`huangjian@zhangyue.com`,注明应聘方向。 44 | > 45 | # LICENSE 46 | 47 | ``` 48 | MIT LICENSE 49 | Copyright (c) 2016 zhangyue 50 | 51 | Permission is hereby granted, free of charge, to any person obtaining 52 | a copy of this software and associated documentation files (the 53 | "Software"), to deal in the Software without restriction, including 54 | without limitation the rights to use, copy, modify, merge, publish, 55 | distribute, sublicense, and/or sell copies of the Software, and to 56 | permit persons to whom the Software is furnished to do so, subject to 57 | the following conditions: 58 | 59 | The above copyright notice and this permission notice shall be 60 | included in all copies or substantial portions of the Software. 61 | 62 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 63 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 64 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 65 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 66 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 67 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 68 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 69 | ``` 70 | -------------------------------------------------------------------------------- /ZeusPlugin/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | .idea/ -------------------------------------------------------------------------------- /ZeusPlugin/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.2" 6 | 7 | defaultConfig { 8 | minSdkVersion 8 9 | targetSdkVersion 23 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | } 24 | -------------------------------------------------------------------------------- /ZeusPlugin/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /ZeusPlugin/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ZeusPlugin/src/main/java/com/android/internal/util/Predicate.java: -------------------------------------------------------------------------------- 1 | package com.android.internal.util; 2 | 3 | /** 4 | * 该类从API8开始系统就存在,这个类只是为了让当前APK中存在一个类与系统的ClassLoader同名, 5 | * 在从dex文件优化为odex文件时,让每个类都标记为可以从其他ClassLoader加载 6 | * 如果你的项目是只支持android 4.4以上(默认为art虚拟机),则不需要这个类。 7 | */ 8 | public interface Predicate { 9 | 10 | boolean apply(T t); 11 | } -------------------------------------------------------------------------------- /ZeusPlugin/src/main/java/zeus/plugin/PluginConstant.java: -------------------------------------------------------------------------------- 1 | package zeus.plugin; 2 | 3 | /** 4 | * Created by huangjian on 2016/6/21. 5 | * 插件的一些配置项 6 | */ 7 | public class PluginConstant { 8 | /** 9 | * 插件配置文件存放的路径,没有放到manifest中的meta data是因为有些手机取不到这个值 10 | */ 11 | public static final String PLUGINWEB_MAINIFEST_FILE = "assets/zeusplugin.meta"; 12 | /** 13 | * 有些手机中DexFile只能接受.apk和.jar后缀,所以这里二选一,如果是sd卡上最好用jar,因为apk会被当成未安装apk被清理软件清理。 14 | */ 15 | public static final String PLUGIN_SUFF = ".apk"; 16 | /** 17 | * 真实的activity 18 | */ 19 | public static final String PLUGIN_REAL_ACTIVITY = "realActivity"; 20 | /** 21 | * 校验的标准的activity,仅为了通过android的activity校验,需要在AndroidManifest.xml中添加 22 | */ 23 | public static final String PLUGIN_ACTIVITY_FOR_STANDARD = "com.zeus.ZeusActivityForStandard"; 24 | 25 | /** 26 | * 如果插件路径是在SD卡上,则插件后缀应为jar 27 | */ 28 | public static final String PLUGIN_JAR_SUFF = ".jar"; 29 | public static final String PLUGIN_INSTALLED_INFO_PATH = "zeusplugin_installinfo"; //插件的安装路径信息,每次安装后的apk的文件名都是随机的,为了实现动态实时加载 30 | public static final String EXP_PLUG_PREFIX = "zeusplugin"; //如果插件id是以zeusplugin开头,则认为是插件 31 | public static final String EXP_PLUG_HOT_FIX_PREFIX = "zeushotfix"; //如果插件id是以zeushotfix开头,则认为是热修复补丁 32 | 33 | } 34 | -------------------------------------------------------------------------------- /ZeusPlugin/src/main/java/zeus/plugin/PluginManager.java: -------------------------------------------------------------------------------- 1 | package zeus.plugin; 2 | 3 | import android.app.Activity; 4 | import android.app.Application; 5 | import android.content.ComponentName; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.pm.ApplicationInfo; 9 | import android.content.pm.PackageInfo; 10 | import android.content.pm.PackageManager; 11 | import android.content.res.AssetManager; 12 | import android.content.res.Resources; 13 | import android.os.Build; 14 | import android.text.TextUtils; 15 | import android.util.SparseArray; 16 | import android.view.LayoutInflater; 17 | import android.webkit.WebView; 18 | 19 | import org.json.JSONArray; 20 | import org.json.JSONObject; 21 | 22 | import java.io.ByteArrayOutputStream; 23 | import java.io.File; 24 | import java.io.FileInputStream; 25 | import java.io.FileOutputStream; 26 | import java.io.InputStream; 27 | import java.lang.ref.WeakReference; 28 | import java.lang.reflect.Field; 29 | import java.lang.reflect.InvocationTargetException; 30 | import java.lang.reflect.Method; 31 | import java.util.ArrayList; 32 | import java.util.HashMap; 33 | import java.util.List; 34 | import java.util.Map; 35 | 36 | import dalvik.system.DexClassLoader; 37 | 38 | /** 39 | * 插件管理类,管理插件的初始化、安装、卸载、加载等等 40 | * 所有的方法和对象都为静态。 41 | *

42 | * Created by huangjian on 2016/6/21. 43 | */ 44 | public class PluginManager { 45 | 46 | private static final String PLUGIN_INSTALLED_LIST_FILE_NAME = "plugin.installedlist"; //已安装插件列表的文件地址 47 | 48 | /*start---这些对象的强引用系统也会一直持有,所以这里不会有内存泄漏---start*/ 49 | public static volatile ClassLoader mNowClassLoader = null; //正在使用的ClassLoader 50 | public static volatile ClassLoader mBaseClassLoader = null; //系统原始的ClassLoader 51 | public static volatile Resources mNowResources; //正在使用的Resources 52 | public static volatile Resources mBaseResources; //原始的resources 53 | public static volatile Context mBaseContext; //原始的application中的BaseContext,不能是其他的,否则会内存泄漏 54 | private static Object mPackageInfo = null; //ContextImpl中的LoadedAPK对象mPackageInfo 55 | /*end---这些对象的强引用系统也会一直持有,所以这里不会有内存泄漏---end*/ 56 | 57 | private static HashMap mInstalledPluginList = null; //已安装的插件列表 58 | private static HashMap mLoadedPluginList = null; //已加载的插件列表 59 | private static HashMap mLoadedDiffPluginPathinfoList = null; 60 | 61 | private static final Object mInstalledPluginListLock = new Object(); //插件安装的锁,支持多线程安装 62 | private static final Object mLoadedPluginListLock = new Object(); //修改已加载的插件对象的锁 63 | private static final Object mLoadLock = new Object(); //插件加载的锁,支持多线程加载 64 | 65 | private static boolean isIniteInstallPlugins = false; //插件是否已经初始化 66 | 67 | private static HashMap mDefaultList; 68 | /** 69 | * 缓存插件的对象 70 | */ 71 | private static HashMap mPluginMap = new HashMap<>(); 72 | 73 | /** 74 | * 得在插件相关的方法调用之前调用 75 | * 76 | * @param application application 77 | */ 78 | public static void init(Application application, HashMap defaultList) { 79 | if(defaultList == null){ 80 | mDefaultList = new HashMap<>(); 81 | }else{ 82 | mDefaultList = defaultList; 83 | } 84 | //初始化一些成员变量和加载已安装的插件 85 | mPackageInfo = PluginUtil.getField(application.getBaseContext(), "mPackageInfo"); 86 | mBaseContext = application.getBaseContext(); 87 | mNowClassLoader = mBaseContext.getClassLoader(); 88 | mBaseClassLoader = mBaseContext.getClassLoader(); 89 | mNowResources = mBaseContext.getResources(); 90 | mBaseResources = mNowResources; 91 | //更改系统的Instrumentation对象,以便创建插件的activity 92 | Object mMainThread = PluginUtil.getField(mBaseContext, "mMainThread"); 93 | PluginUtil.setField(mMainThread, "mInstrumentation", new ZeusInstrumentation()); 94 | //创建插件的相关文件夹目录 95 | createPath(); 96 | //加载已安装过的插件 97 | loadInstalledPlugins(); 98 | //清除老版本的插件,最好放到软件退出时调用,防止让启动速度变慢 99 | clearOldPlugin(); 100 | //安装内置插件 101 | Thread initPluginThread = new Thread(new Runnable() { 102 | @Override 103 | public void run() { 104 | installInitPlugins(); 105 | } 106 | }); 107 | initPluginThread.setName("initPluginThread"); 108 | initPluginThread.start(); 109 | } 110 | 111 | private static void createPath() { 112 | PluginUtil.createDir(PluginUtil.getInsidePluginPath()); 113 | } 114 | 115 | /** 116 | * 在android 5.0以上设置resource的share lib path,解决加载webView时package id not found的问题 117 | * 系统会在加载webView之后通过调用AssetManager中的addAssetPathAsSharedLibrary(7.0以上)或者addAssetPath(5.0-7.0之间) 118 | * 将webview使用的资源apk添加到AssetManager中,因此重新生成AssetManager时也需要对应的设置一下 119 | * @param resources 120 | * @param orgAssetManger 121 | */ 122 | private static void addShareLibPaths(Resources resources, AssetManager orgAssetManger) { 123 | if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 124 | try { 125 | PackageInfo packageInfo = null; 126 | Method getLoadedPackageInfoMethod = PluginUtil.getMethod(mBaseClassLoader.loadClass("android.webkit.WebViewFactory"), "getLoadedPackageInfo", null); 127 | if(getLoadedPackageInfoMethod != null){ 128 | packageInfo = (PackageInfo)getLoadedPackageInfoMethod.invoke(null); 129 | } 130 | String webviewApk; 131 | if (packageInfo != null && packageInfo.applicationInfo != null) { 132 | webviewApk = packageInfo.applicationInfo.sourceDir; 133 | AssetManager assetManager = resources.getAssets(); 134 | Method addAssetPathAsSharedLibrary = PluginUtil.getMethod(assetManager.getClass(), "addAssetPathAsSharedLibrary", String.class); 135 | if (addAssetPathAsSharedLibrary != null) { 136 | addAssetPathAsSharedLibrary.invoke(orgAssetManger, webviewApk); 137 | } else { 138 | Method addAssetPath = PluginUtil.getMethod(assetManager.getClass(), "addAssetPath", String.class); 139 | addAssetPath.invoke(orgAssetManger, webviewApk); 140 | } 141 | } 142 | } catch (Throwable throwable) { 143 | throwable.printStackTrace(); 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * 安装初始的内置插件 150 | */ 151 | private static void installInitPlugins() { 152 | HashMap installedList = getInstalledPlugin(); 153 | HashMap defaultList = getDefaultPlugin(); 154 | for (String key : defaultList.keySet()) { 155 | int installVersion = -1; 156 | int defaultVersion = defaultList.get(key); 157 | 158 | if (installedList.containsKey(key)) { 159 | installVersion = installedList.get(key).getVersion(); 160 | } 161 | 162 | ZeusPlugin plugin = getPlugin(key); 163 | if (defaultVersion > installVersion) { 164 | boolean ret = plugin.installAssetPlugin(); 165 | //提前将dex文件优化为odex或者opt文件 166 | if (ret) { 167 | try { 168 | new DexClassLoader(PluginUtil.getAPKPath(key), PluginUtil.getDexCacheParentDirectPath(key), null, mBaseClassLoader.getParent()); 169 | } catch (Throwable e) { 170 | e.printStackTrace(); 171 | } 172 | } 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * 获取已安装的插件列表 179 | * 180 | * @return 已安装的插件列表 181 | */ 182 | public static HashMap getInstalledPlugin() { 183 | if (mInstalledPluginList != null) { 184 | return mInstalledPluginList; 185 | } else { 186 | return mInstalledPluginList = readInstalledPlugin(); 187 | } 188 | } 189 | 190 | /** 191 | * 获取某插件是否安装 192 | * 193 | * @param pluginId 插件ID 194 | * @return 某插件是否安装 195 | */ 196 | public static boolean isInstall(String pluginId) { 197 | return isInstall(pluginId, -1); 198 | } 199 | 200 | /** 201 | * 获取某插件是否安装 202 | * 203 | * @param pluginId 插件ID 204 | * @return 某插件是否安装 205 | */ 206 | public static boolean isInstall(String pluginId, int version) { 207 | getInstalledPlugin(); 208 | return mInstalledPluginList != null && mInstalledPluginList.containsKey(pluginId) && mInstalledPluginList.get(pluginId).getVersion() >= version; 209 | } 210 | 211 | /** 212 | * 获取已经被加载到系统中的插件列表 213 | * 214 | * @return 已经被加载到系统中的插件列表 215 | */ 216 | public static HashMap getLoadedPlugin() { 217 | return mLoadedPluginList; 218 | } 219 | 220 | /** 221 | * 判断某个插件是否已经被加载 222 | * 223 | * @param pluginId 插件ID 224 | * @return 某个插件是否已经被加载 225 | */ 226 | public static boolean isLoaded(String pluginId) { 227 | return mLoadedPluginList != null && mLoadedPluginList.containsKey(pluginId); 228 | } 229 | 230 | private static void putLoadedPlugin(String pluginId, PluginManifest pluginManifest, String pathinfo) { 231 | synchronized (mLoadedPluginListLock) { 232 | if (mLoadedPluginList == null) { 233 | mLoadedPluginList = new HashMap<>(); 234 | } 235 | if(mLoadedDiffPluginPathinfoList == null){ 236 | mLoadedDiffPluginPathinfoList = new HashMap<>(); 237 | } 238 | mLoadedPluginList.put(pluginId, pluginManifest); 239 | mLoadedDiffPluginPathinfoList.put(pluginId, pathinfo); 240 | } 241 | } 242 | 243 | /** 244 | * 获取内置插件列表 245 | * 246 | * @return 内置插件列表 247 | */ 248 | public static HashMap getDefaultPlugin() { 249 | return mDefaultList; 250 | } 251 | 252 | 253 | private static HashMap readInstalledPlugin() { 254 | synchronized (mInstalledPluginListLock) { 255 | HashMap pluginList = new HashMap<>(); 256 | 257 | String path = getInstalledPluginListFilePath(); 258 | File file = new File(path); 259 | if (file.exists()) { 260 | try { 261 | InputStream inputStream = new FileInputStream(file); 262 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 263 | 264 | byte[] buffer = new byte[512]; 265 | int length; 266 | int count = 0; 267 | while ((length = inputStream.read(buffer, 0, 512)) > 0) { 268 | bos.write(buffer, 0, length); 269 | count += length; 270 | } 271 | if (count > 0) { 272 | String result = new String(bos.toByteArray(), "UTF-8"); 273 | PluginUtil.close(inputStream); 274 | PluginUtil.close(bos); 275 | JSONArray jObject = new JSONArray(result); 276 | int jLength = jObject.length(); 277 | for (int i = 0; i < jLength; i++) { 278 | JSONObject f = jObject.getJSONObject(i); 279 | String id = f.optString("id", ""); 280 | //兼容老版本,新版本中version字段已不存在 281 | int version = f.optInt("version", 1); 282 | String meta = f.optString("meta",""); 283 | PluginManifest pluginManifest; 284 | if(!TextUtils.isEmpty(meta)){ 285 | pluginManifest = new PluginManifest(meta); 286 | }else{ 287 | pluginManifest = new PluginManifest(); 288 | pluginManifest.version = String.valueOf(version); 289 | pluginManifest.name = id; 290 | } 291 | if (!TextUtils.isEmpty(id)) { 292 | pluginList.put(id, pluginManifest); 293 | } 294 | } 295 | } 296 | PluginUtil.close(inputStream); 297 | PluginUtil.close(bos); 298 | } catch (Exception e) { 299 | e.printStackTrace(); 300 | return pluginList; 301 | } 302 | } 303 | return pluginList; 304 | } 305 | } 306 | 307 | protected static boolean addInstalledPlugin(String pluginId, PluginManifest pluginManifest) { 308 | if (getInstalledPlugin() != null) { 309 | synchronized (mInstalledPluginListLock) { 310 | mInstalledPluginList.put(pluginId, pluginManifest); 311 | } 312 | save(); 313 | return true; 314 | } 315 | return false; 316 | } 317 | 318 | private static boolean save() { 319 | synchronized (mInstalledPluginListLock) { 320 | if (mInstalledPluginList == null) return true; 321 | FileOutputStream out = null; 322 | try { 323 | JSONArray array = new JSONArray(); 324 | for (String key : mInstalledPluginList.keySet()) { 325 | //新版本中不再使用此字段 326 | // int version = mInstalledPluginList.get(key).getVersion(); 327 | JSONObject obj = new JSONObject(); 328 | obj.put("id", key); 329 | // obj.put("version", version); 330 | obj.put("meta",mInstalledPluginList.get(key).toString()); 331 | array.put(obj); 332 | } 333 | String result = array.toString(); 334 | 335 | String path = getInstalledPluginListFilePath(); 336 | File file = new File(path); 337 | 338 | if (!file.exists()) { 339 | file.createNewFile(); 340 | } 341 | out = new FileOutputStream(file); 342 | out.write(result.getBytes()); 343 | } catch (Exception e) { 344 | e.printStackTrace(); 345 | return false; 346 | } finally { 347 | PluginUtil.close(out); 348 | } 349 | } 350 | return true; 351 | } 352 | 353 | private static String getInstalledPluginListFilePath() { 354 | return PluginUtil.getInsidePluginPath() + PLUGIN_INSTALLED_LIST_FILE_NAME; 355 | } 356 | 357 | /** 358 | * 加载某个插件,如果已经加载了直接返回 359 | * 如果已经加载了老版本插件,而新版本插件也安装了,此方法也不会加载最新版本的插件 360 | * 361 | * @param pluingId 插件的ID 362 | * @return 是否加载成功 363 | */ 364 | public static boolean loadPlugin(String pluingId) { 365 | return loadLastVersionPlugin(pluingId); 366 | } 367 | 368 | /** 369 | * 加载最新版本的插件,如果老版本的该插件已经被加载过,则清除之前的版本后再加载最新版本。 370 | * 此方法不可在插件的使用过程中调用,只能在插件运行之前,或插件退出之后。 371 | * 如果运行过程中可能会出现class不一致的情况,但也不是一定的,有的插件还是可以运行时调用的, 372 | * 但通常生效都得下次进入插件时才生效,但不必退出软件再进才生效 373 | * 374 | * @param pluingId 插件的ID 375 | * @return 是否加载成功 376 | */ 377 | public static boolean loadLastVersionPlugin(String pluingId) { 378 | ZeusPlugin plugin = getPlugin(pluingId); 379 | PluginManifest meta = plugin.getPluginMeta(); 380 | int version = -1; 381 | if (meta != null) { 382 | version = Integer.valueOf(meta.version); 383 | } 384 | return loadPlugin(pluingId, version); 385 | } 386 | 387 | /** 388 | * 加载指定版本的插件 389 | * 390 | * @param pluginId 插件ID 391 | * @param version 要加载插件的版本号 392 | * @return 是否加载成功 393 | */ 394 | public static boolean loadPlugin(String pluginId, int version) { 395 | synchronized (mLoadLock) { 396 | if (getLoadedPlugin() != null && getLoadedPlugin().get(pluginId) != null && getLoadedPlugin().get(pluginId).getVersion() >= version) { 397 | return true; 398 | } 399 | String pathInfo = PluginUtil.getInstalledPathInfo(pluginId); 400 | String pluginApkPath = PluginUtil.getAPKPath(pluginId, pathInfo); 401 | ZeusPlugin plugin = getPlugin(pluginId); 402 | if (!PluginUtil.exists(pluginApkPath)) { 403 | if (getDefaultPlugin().containsKey(pluginId)) { 404 | if (!plugin.installAssetPlugin()) { 405 | return false; 406 | } else { 407 | pathInfo = PluginUtil.getInstalledPathInfo(pluginId); 408 | pluginApkPath = PluginUtil.getAPKPath(pluginId); 409 | } 410 | } else { 411 | return false; 412 | } 413 | } 414 | 415 | PluginManifest meta = plugin.getPluginMeta(); 416 | if (meta == null || Integer.valueOf(meta.version) < version || !isSupport(pluginId)) return false; 417 | 418 | ClassLoader cl = mNowClassLoader; 419 | if (PluginUtil.isHotFix(pluginId)) { 420 | loadHotfixPluginClassLoader(pluginId); 421 | } else { 422 | //如果一个老版本的插件已经被加载了,则需要先移除 423 | if (getLoadedPlugin() != null && getLoadedPlugin().containsKey(pluginId)) { 424 | if (cl instanceof ZeusClassLoader) { 425 | ZeusClassLoader classLoader = (ZeusClassLoader) cl; 426 | //移除老版本的插件 427 | classLoader.removePlugin(pluginId); 428 | //添加新版本的插件 429 | classLoader.addAPKPath(pluginId, pluginApkPath, PluginUtil.getLibFileInside(pluginId)); 430 | } 431 | } else { 432 | if (cl instanceof ZeusClassLoader) { 433 | ZeusClassLoader classLoader = (ZeusClassLoader) cl; 434 | classLoader.addAPKPath(pluginId, pluginApkPath, PluginUtil.getLibFileInside(pluginId)); 435 | } else { 436 | String nativeLibraryDir = null; 437 | ApplicationInfo applicationInfo = mBaseContext.getApplicationInfo(); 438 | if (applicationInfo != null) { 439 | nativeLibraryDir = applicationInfo.nativeLibraryDir; 440 | } 441 | ZeusClassLoader classLoader = new ZeusClassLoader(mBaseContext.getPackageCodePath(), cl, nativeLibraryDir); 442 | classLoader.addAPKPath(pluginId, pluginApkPath, PluginUtil.getLibFileInside(pluginId)); 443 | PluginUtil.setField(mPackageInfo, "mClassLoader", classLoader); 444 | Thread.currentThread().setContextClassLoader(classLoader); 445 | mNowClassLoader = classLoader; 446 | } 447 | } 448 | putLoadedPlugin(pluginId, meta, pathInfo); 449 | } 450 | clearViewConstructorCache(); 451 | if ((meta.getFlag() & PluginManifest.FLAG_WITHOUT_RESOURCES) != PluginManifest.FLAG_WITHOUT_RESOURCES) { 452 | reloadInstalledPluginResources(); 453 | } 454 | } 455 | return true; 456 | } 457 | 458 | private static boolean isSupport(String pluginId){ 459 | int versionCode = -1; 460 | try { 461 | versionCode = mBaseContext.getPackageManager().getPackageInfo(mBaseContext.getPackageName(), 0).versionCode; 462 | } catch (PackageManager.NameNotFoundException e) { 463 | e.printStackTrace(); 464 | } 465 | 466 | PluginManifest manifest = getPlugin(pluginId).getPluginMeta(); 467 | int maxVersion = TextUtils.isEmpty(manifest.maxVersion) ? Integer.MAX_VALUE : Integer.valueOf(manifest.maxVersion); 468 | int minVersion = TextUtils.isEmpty(manifest.minVersion) ? -1 : Integer.valueOf(manifest.minVersion); 469 | //如果宿主不支持该插件或补丁则不加载 470 | return !(versionCode != -1 && 471 | (versionCode > maxVersion || 472 | versionCode < minVersion)); 473 | } 474 | /** 475 | * 系统会记录当前已经加载过的view的所有的构造函数,避免反射,提高性能 476 | * 但是我们插件一旦更新后,view的构造函数就已经无效了,所以要清理这些缓存 477 | */ 478 | private static void clearViewConstructorCache() { 479 | try { 480 | Field field = LayoutInflater.class.getDeclaredField("sConstructorMap"); 481 | field.setAccessible(true); 482 | Map map = (Map) field.get(null); 483 | map.clear(); 484 | } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException e1) { 485 | e1.printStackTrace(); 486 | } 487 | 488 | //清除TintContextWrapper类中的sCache 489 | try { 490 | Class tintContextWrapper = mNowClassLoader.loadClass("android.support.v7.widget.TintContextWrapper"); 491 | Field sCacheField = tintContextWrapper.getDeclaredField("sCache"); 492 | 493 | Field lockField = tintContextWrapper.getDeclaredField("CACHE_LOCK"); 494 | if(lockField != null){ 495 | lockField.setAccessible(true); 496 | Object lock = lockField.get(null); 497 | synchronized (lock){ 498 | sCacheField.setAccessible(true); 499 | List list = (List) sCacheField.get(null); 500 | list.clear(); 501 | } 502 | }else{ 503 | sCacheField.setAccessible(true); 504 | List list = (List) sCacheField.get(null); 505 | list.clear(); 506 | } 507 | } catch (Throwable e) { 508 | } 509 | } 510 | 511 | private static void clearCacheObject(Object drawableCache) { 512 | if (drawableCache == null) return; 513 | Method clearMethod = PluginUtil.getMethod(drawableCache.getClass(), "clear"); 514 | if (clearMethod != null) { 515 | try { 516 | clearMethod.invoke(drawableCache); 517 | } catch (Exception ignored) { 518 | 519 | } 520 | } 521 | //高android版本走这里 522 | Object mThemedEntries = PluginUtil.getField(drawableCache, "mThemedEntries"); 523 | if (mThemedEntries != null) { 524 | clearMethod = PluginUtil.getMethod(mThemedEntries.getClass(), "clear"); 525 | if (clearMethod != null) { 526 | try { 527 | clearMethod.invoke(mThemedEntries); 528 | } catch (Exception ignored) { 529 | 530 | } 531 | } 532 | } 533 | } 534 | 535 | /** 536 | * 清除resources中的图片缓存,降低内存消耗的一个办法 537 | * 不调用也不会产生严重问题 538 | * 539 | * @param resouces resouces 540 | */ 541 | static private void clearResoucesDrawableCache(Object resouces) { 542 | if (resouces == null) return; 543 | Method flushLayoutCacheMethod = PluginUtil.getMethod(resouces.getClass(), "flushLayoutCache"); 544 | if (flushLayoutCacheMethod != null) { 545 | try { 546 | flushLayoutCacheMethod.invoke(resouces); 547 | } catch (Throwable e) { 548 | e.printStackTrace(); 549 | } 550 | } 551 | clearCacheObject(PluginUtil.getField(resouces, "mDrawableCache")); 552 | clearCacheObject(PluginUtil.getField(resouces, "mColorDrawableCache")); 553 | clearCacheObject(PluginUtil.getField(resouces, "mColorStateListCache")); 554 | clearCacheObject(PluginUtil.getField(resouces, "mAnimatorCache")); 555 | clearCacheObject(PluginUtil.getField(resouces, "mStateListAnimatorCache")); 556 | 557 | //清除对象池中的缓存 558 | Object typedArrayPool = PluginUtil.getField(resouces, "mTypedArrayPool"); 559 | if (typedArrayPool != null) { 560 | Method acquirePath = PluginUtil.getMethod(typedArrayPool.getClass(), "acquire"); 561 | try { 562 | while (acquirePath.invoke(typedArrayPool) != null) ; 563 | } catch (IllegalAccessException e) { 564 | e.printStackTrace(); 565 | } catch (InvocationTargetException e) { 566 | e.printStackTrace(); 567 | } 568 | } 569 | } 570 | /** 571 | * 加载所有已安装的插件的资源,并清除资源中的缓存 572 | */ 573 | private static void reloadInstalledPluginResources() { 574 | try { 575 | AssetManager assetManager = AssetManager.class.newInstance(); 576 | Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class); 577 | addAssetPath.invoke(assetManager, mBaseContext.getPackageResourcePath()); 578 | 579 | //解决android 5.0以上版本启动webView时package id not found的问题 580 | addShareLibPaths(mNowResources, assetManager); 581 | 582 | if (mLoadedPluginList != null && mLoadedPluginList.size() != 0) { 583 | //每个插件的packageID都不能一样 584 | for (String id : mLoadedPluginList.keySet()) { 585 | //只有带有资源的补丁才会执行添加到assetManager中 586 | PluginManifest manifest = mLoadedPluginList.get(id); 587 | if (manifest.hasResoures()) { 588 | addAssetPath.invoke(assetManager, PluginUtil.getAPKPath(id, mLoadedDiffPluginPathinfoList.get(id))); 589 | } 590 | } 591 | } 592 | //这里提前创建一个resource是因为Resources的构造函数会对AssetManager进行一些变量的初始化 593 | //还不能创建系统的Resources类,否则中兴系统会出现崩溃问题 594 | Resources newResources = new Resources(assetManager, 595 | mBaseContext.getResources().getDisplayMetrics(), 596 | mBaseContext.getResources().getConfiguration()); 597 | 598 | PluginUtil.setField(mBaseContext, "mResources", newResources); 599 | PluginUtil.setField(mPackageInfo, "mResources", newResources); 600 | 601 | //清除一下之前的resource的数据,释放一些内存 602 | //因为这个resource有可能还被系统持有着,内存都没被释放 603 | clearResoucesDrawableCache(mNowResources); 604 | 605 | mNowResources = newResources; 606 | //需要清理mtheme对象,否则通过inflate方式加载资源会报错 607 | PluginUtil.setField(mBaseContext, "mTheme", null); 608 | } catch (Throwable e) { 609 | e.printStackTrace(); 610 | } 611 | } 612 | 613 | 614 | /** 615 | * 清除已经加载过的插件 616 | * 617 | * @param pluginId 插件id 618 | */ 619 | private static void removeLoadedPlugin(String pluginId) { 620 | synchronized (mLoadLock) { 621 | if (!isLoaded(pluginId)) return; 622 | if (mLoadedPluginList == null) return; 623 | mLoadedPluginList.remove(pluginId); 624 | clearViewConstructorCache(); 625 | if (mLoadedPluginList.size() == 0) { 626 | PluginUtil.setField(mPackageInfo, "mClassLoader", mBaseClassLoader); 627 | Thread.currentThread().setContextClassLoader(mBaseClassLoader); 628 | mNowClassLoader = mBaseClassLoader; 629 | Thread.currentThread().setContextClassLoader(mBaseClassLoader); 630 | } else { 631 | ClassLoader cl = mNowClassLoader; 632 | if (!(cl instanceof ZeusClassLoader)) return; 633 | ZeusClassLoader classLoader = (ZeusClassLoader) cl; 634 | classLoader.removePlugin(pluginId); 635 | } 636 | reloadInstalledPluginResources(); 637 | } 638 | } 639 | 640 | /** 641 | * 从安装列表中删除一个插件,并从内存中清除加载的插件 642 | * 643 | * @param pluginId 插件ID 644 | * @return true表明删除成功,false表明出现了异常 645 | */ 646 | protected static boolean unInstalledPlugin(String pluginId) { 647 | removeLoadedPlugin(pluginId); 648 | if (getInstalledPlugin() != null) { 649 | synchronized (mInstalledPluginListLock) { 650 | mInstalledPluginList.remove(pluginId); 651 | } 652 | save(); 653 | return true; 654 | } 655 | return false; 656 | } 657 | 658 | /** 659 | * 清除老版本的插件,软件一启动或者软件退出时调用最佳 660 | */ 661 | public static void clearOldPlugin() { 662 | HashMap map = getInstalledPlugin(); 663 | if (map != null) { 664 | for (String key : map.keySet()) { 665 | ZeusPlugin plugin = getPlugin(key); 666 | plugin.clearOldPlugin(); 667 | } 668 | } 669 | } 670 | 671 | private synchronized static void loadHotfixPluginClassLoader(String pluginId) { 672 | //如果不是补丁,或者该补丁已经加载过,则不做处理 673 | if (!PluginUtil.isHotFix(pluginId) || 674 | (getLoadedPlugin() != null && getLoadedPlugin().containsKey(pluginId))) { 675 | return; 676 | } 677 | HashMap installedPluginMaps = getInstalledPlugin(); 678 | //如果开启了instant run,则需要更改为mBaseClassLoader.getParent(); 679 | ClassLoader orgClassLoader = mBaseClassLoader; 680 | //以下这段是补丁框架为了兼容android studio 2.0版本以上在debug时的instant run功能,在打正式包的时候请删除这段无用代码 681 | //---start---- 682 | if (mBaseClassLoader.getParent().getClass().getSimpleName().equals("IncrementalClassLoader")) { 683 | orgClassLoader = mBaseClassLoader.getParent(); 684 | } 685 | //---end---- 686 | ClassLoader classLoader = orgClassLoader.getParent(); 687 | ZeusHotfixClassLoader hotfixClassLoader; 688 | String pathInfo = PluginUtil.getInstalledPathInfo(pluginId); 689 | if (classLoader instanceof ZeusHotfixClassLoader) { 690 | hotfixClassLoader = (ZeusHotfixClassLoader) classLoader; 691 | hotfixClassLoader.addAPKPath(PluginUtil.getAPKPath(pluginId, pathInfo), 692 | PluginUtil.getLibFileInside(pluginId)); 693 | } else { 694 | hotfixClassLoader = new ZeusHotfixClassLoader(PluginUtil.getAPKPath(pluginId, pathInfo), 695 | PluginUtil.getDexCacheParentDirectPath(pluginId), 696 | PluginUtil.getLibFileInside(pluginId), 697 | classLoader); 698 | hotfixClassLoader.setOrgAPKClassLoader(orgClassLoader); 699 | PluginUtil.setField(orgClassLoader, "parent", hotfixClassLoader); 700 | } 701 | putLoadedPlugin(pluginId, installedPluginMaps.get(pluginId), pathInfo); 702 | } 703 | 704 | private static void loadInstalledPlugins() { 705 | synchronized (mLoadLock) { 706 | if (isIniteInstallPlugins) { 707 | return; 708 | } 709 | HashMap installedPluginMaps = getInstalledPlugin(); 710 | if (installedPluginMaps.isEmpty()) { 711 | isIniteInstallPlugins = true; 712 | return; 713 | } 714 | //获取classloader设置classloader 715 | ZeusClassLoader classLoader = null; 716 | boolean isNeedLoadResource = false; 717 | 718 | for (String pluginId : installedPluginMaps.keySet()) { 719 | if(!isSupport(pluginId))continue; 720 | if (PluginUtil.isPlugin(pluginId)) { 721 | if (classLoader == null) { 722 | String nativeLibraryDir = null; 723 | ApplicationInfo applicationInfo = mBaseContext.getApplicationInfo(); 724 | if (applicationInfo != null) { 725 | nativeLibraryDir = applicationInfo.nativeLibraryDir; 726 | } 727 | classLoader = new ZeusClassLoader(mBaseContext.getPackageCodePath(), mBaseContext.getClassLoader(), nativeLibraryDir); 728 | } 729 | String pathInfo = PluginUtil.getInstalledPathInfo(pluginId); 730 | classLoader.addAPKPath(pluginId, PluginUtil.getAPKPath(pluginId, pathInfo), PluginUtil.getLibFileInside(pluginId)); 731 | putLoadedPlugin(pluginId, installedPluginMaps.get(pluginId), pathInfo); 732 | isNeedLoadResource = true; 733 | } 734 | if (PluginUtil.isHotFix(pluginId)) { 735 | loadHotfixPluginClassLoader(pluginId); 736 | isNeedLoadResource = true; 737 | } 738 | } 739 | clearViewConstructorCache(); 740 | if (!isNeedLoadResource) { 741 | isIniteInstallPlugins = true; 742 | return; 743 | } 744 | //设置原始APK所使用的ClassLoader 745 | if (classLoader != null) { 746 | PluginUtil.setField(mPackageInfo, "mClassLoader", classLoader); 747 | Thread.currentThread().setContextClassLoader(classLoader); 748 | mNowClassLoader = classLoader; 749 | } 750 | reloadInstalledPluginResources(); 751 | isIniteInstallPlugins = true; 752 | } 753 | } 754 | 755 | /** 756 | * Application中以及activity中的getResources()方法都应该调用这个方法 757 | * 758 | * @return 返回正在使用的resources 759 | */ 760 | public static Resources getResources() { 761 | return mNowResources; 762 | } 763 | 764 | /** 765 | * 获取最新插件对象,不存在就生成一个 766 | * 767 | * @param pluginId 插件的名称 768 | * @return 获取某个插件对象 769 | */ 770 | public static ZeusPlugin getPlugin(String pluginId) { 771 | ZeusPlugin plugin = null; 772 | try { 773 | plugin = mPluginMap.get(pluginId); 774 | if (plugin != null) { 775 | return plugin; 776 | } 777 | if (PluginUtil.iszeusPlugin(pluginId)) { 778 | plugin = new ZeusPlugin(pluginId); 779 | } 780 | mPluginMap.put(pluginId, plugin); 781 | } catch (Exception e) { 782 | e.printStackTrace(); 783 | } 784 | return plugin; 785 | } 786 | 787 | 788 | public static void startActivity(Activity activity, Intent intent) { 789 | ComponentName componentName = intent.getComponent(); 790 | intent.setClassName(componentName.getPackageName(), PluginConstant.PLUGIN_ACTIVITY_FOR_STANDARD); 791 | intent.putExtra(PluginConstant.PLUGIN_REAL_ACTIVITY, componentName.getClassName()); 792 | activity.startActivity(intent); 793 | } 794 | 795 | public static void startActivity(Intent intent) { 796 | ComponentName componentName = intent.getComponent(); 797 | intent.setClassName(componentName.getPackageName(), PluginConstant.PLUGIN_ACTIVITY_FOR_STANDARD); 798 | intent.putExtra(PluginConstant.PLUGIN_REAL_ACTIVITY, componentName.getClassName()); 799 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 800 | mBaseContext.startActivity(intent); 801 | } 802 | } 803 | -------------------------------------------------------------------------------- /ZeusPlugin/src/main/java/zeus/plugin/PluginManifest.java: -------------------------------------------------------------------------------- 1 | package zeus.plugin; 2 | 3 | import android.text.TextUtils; 4 | 5 | import org.json.JSONException; 6 | import org.json.JSONObject; 7 | 8 | /** 9 | * 插件的配置,对应文件存放在插件apk下的assets/plugin.meta,这也是可配置的 10 | * 之所以没有放在androidManifest.xml中是因为有些手机使用无法获取插件中对应的meta data数据。 11 | * 其实还有一套完整强安全校验方案,想到绝大部分人用不到,就删掉了,想要的可以在单独联系。 12 | */ 13 | public class PluginManifest { 14 | public static final String PLUG_NAME = "name"; 15 | public static final String PLUG_MIN_VERSION = "minVersion"; 16 | public static final String PLUG_MAX_VERSION = "maxVersion"; 17 | public static final String PLUG_VERSION = "version"; 18 | public static final String PLUG_MAINCLASS = "mainClass"; 19 | public static final String PLUG_OTHER_INFO = "otherInfo"; 20 | public static final String PLUG_FLAG = "flag"; 21 | public static final int FLAG_WITHOUT_RESOURCES = 0x1; 22 | public static final int FLAG_WITHOUT_SO_FILE = 0x2; 23 | /** 24 | * 插件显示的名称 25 | */ 26 | public String name = ""; 27 | /** 28 | * 插件依赖的最低平台版本号 29 | */ 30 | public String minVersion = ""; 31 | /** 32 | * 插件依赖的最大平台版本号 33 | */ 34 | public String maxVersion = ""; 35 | /** 36 | * 插件版本号 37 | */ 38 | public String version = ""; 39 | /** 40 | * 插件入口的类名 41 | */ 42 | public String mainClass = ""; 43 | /** 44 | * 插件的其他信息,可扩展,可是其他json格式字符串 45 | */ 46 | public String otherInfo = ""; 47 | 48 | public String flag = ""; 49 | 50 | public PluginManifest() { 51 | } 52 | 53 | public PluginManifest(String manifest) { 54 | try { 55 | JSONObject jsonObject = new JSONObject(manifest); 56 | name = jsonObject.optString(PLUG_NAME); 57 | minVersion = jsonObject.optString(PLUG_MIN_VERSION); 58 | maxVersion = jsonObject.optString(PLUG_MAX_VERSION); 59 | version = jsonObject.optString(PLUG_VERSION); 60 | mainClass = jsonObject.optString(PLUG_MAINCLASS); 61 | otherInfo = jsonObject.optString(PLUG_OTHER_INFO); 62 | flag = jsonObject.optString(PLUG_FLAG); 63 | } catch (JSONException e) { 64 | e.printStackTrace(); 65 | } 66 | } 67 | 68 | public int getVersion() { 69 | if (!TextUtils.isEmpty(version)) { 70 | try { 71 | return Integer.valueOf(version); 72 | } catch (Throwable e) { 73 | return 0; 74 | } 75 | } else { 76 | return 0; 77 | } 78 | } 79 | 80 | public int getFlag() { 81 | return TextUtils.isEmpty(flag) ? 0 : Integer.valueOf(flag); 82 | } 83 | 84 | public boolean hasResoures() { 85 | return (getFlag() & FLAG_WITHOUT_RESOURCES) != FLAG_WITHOUT_RESOURCES; 86 | } 87 | 88 | public boolean hasSoLibrary(){ 89 | return (getFlag() & FLAG_WITHOUT_SO_FILE) != FLAG_WITHOUT_SO_FILE; 90 | } 91 | @Override 92 | public String toString() { 93 | JSONObject jsonObject = new JSONObject(); 94 | try { 95 | jsonObject.put(PLUG_NAME, name); 96 | jsonObject.put(PLUG_MIN_VERSION, minVersion); 97 | jsonObject.put(PLUG_MAX_VERSION, maxVersion); 98 | jsonObject.put(PLUG_VERSION, version); 99 | jsonObject.put(PLUG_MAINCLASS, mainClass); 100 | jsonObject.put(PLUG_OTHER_INFO, otherInfo); 101 | jsonObject.put(PLUG_OTHER_INFO, flag); 102 | } catch (JSONException e) { 103 | e.printStackTrace(); 104 | } 105 | return jsonObject.toString(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /ZeusPlugin/src/main/java/zeus/plugin/PluginUtil.java: -------------------------------------------------------------------------------- 1 | package zeus.plugin; 2 | 3 | import android.content.res.AssetManager; 4 | import android.text.TextUtils; 5 | 6 | import java.io.BufferedInputStream; 7 | import java.io.BufferedReader; 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.Closeable; 10 | import java.io.File; 11 | import java.io.FileInputStream; 12 | import java.io.FileOutputStream; 13 | import java.io.InputStream; 14 | import java.io.InputStreamReader; 15 | import java.lang.reflect.Field; 16 | import java.lang.reflect.Method; 17 | import java.util.zip.ZipEntry; 18 | import java.util.zip.ZipInputStream; 19 | 20 | /** 21 | * 插件的工具类,用到的工具静态方法在这里,包括zip、file、路径配置等等 22 | *

23 | * Created by huangjian on 2016/6/21. 24 | */ 25 | public class PluginUtil { 26 | 27 | private final static int BYTE_IN_SIZE = 4096; 28 | private static final int BUF_SIZE = 8192; 29 | 30 | private static final int CPU_AMR = 1; 31 | private static final int CPU_X86 = 2; 32 | private static final int CPU_MIPS = 3; 33 | 34 | private static String mInsidePluginPath = null; 35 | //start========================获取插件相关目录的方法====================== 36 | 37 | /** 38 | * 获取某个插件的安装目录 39 | * 40 | * @param plugId 插件id 41 | * @return 插件的安装目录 42 | */ 43 | public static String getPlugDir(String plugId) { 44 | return getInsidePluginPath() + plugId + "/"; 45 | } 46 | 47 | /** 48 | * 获取插件模块的目录 49 | * 50 | * @return 插件模块的目录 51 | */ 52 | public static String getInsidePluginPath() { 53 | if (mInsidePluginPath != null) { 54 | return mInsidePluginPath; 55 | } 56 | return mInsidePluginPath = PluginManager.mBaseContext.getFilesDir().getPath() + "/plugins/"; 57 | } 58 | 59 | /** 60 | * 获取安装后的某个插件文件路径 61 | * 62 | * @param pluginName 插件id 63 | * @return 安装后的某个插件文件路径 64 | */ 65 | public static String getAPKPath(String pluginName) { 66 | return getAPKPath(pluginName, getInstalledPathInfo(pluginName)); 67 | } 68 | 69 | public static String getAPKPath(String pluginName, String pathifo){ 70 | return PluginUtil.getPlugDir(pluginName)+ pathifo + PluginConstant.PLUGIN_SUFF; 71 | } 72 | 73 | /** 74 | * 获取安装前某个插件文件的路径 75 | * 76 | * @param pluginName 插件id 77 | * @return 安装前某个插件文件的路径 78 | */ 79 | public static String getZipPath(String pluginName) { 80 | return getPlugDir(pluginName) + pluginName; 81 | } 82 | 83 | /** 84 | * 获取dex优化后的文件地址 85 | * @param pluingid 插件id 86 | * @param apkName apk的名称 87 | * @return dex优化后的文件地址 88 | */ 89 | public static String getDexCacheFilePath(String pluingid, String apkName) { 90 | return getDexCacheParentDirectPath(pluingid) + apkName + ".dex"; 91 | } 92 | 93 | /** 94 | * 获取某个插件id的dex优化后路径 95 | * @param pluginid 插件id 96 | * @return 某个插件id的dex优化后路径 97 | */ 98 | public static String getDexCacheParentDirectPath(String pluginid) { 99 | String path; 100 | if(TextUtils.isEmpty(pluginid)){ 101 | path = getDexCacheParentDirectPath(); 102 | }else { 103 | path = getDexCacheParentDirectPath() + pluginid + "/"; 104 | } 105 | if(!isDirExist(path)){ 106 | createDirWithFile(path); 107 | } 108 | 109 | return path; 110 | } 111 | 112 | /** 113 | * 判断某个文件是否是文件夹 114 | * @param filePathName 115 | * @return 116 | */ 117 | public static boolean isDirExist(String filePathName) { 118 | if(TextUtils.isEmpty(filePathName)) return false; 119 | if(!filePathName.endsWith("/")) filePathName +="/"; 120 | File file = new File(filePathName); 121 | return (file.isDirectory() && file.exists()); 122 | } 123 | /** 124 | * 优化后的odex/opt文件的文件夹路径 125 | * 126 | * @return 优化后的odex/opt文件的文件夹路径 127 | */ 128 | private static String getDexCacheParentDirectPath() { 129 | return getInsidePluginPath() + "dalvik-cache/"; 130 | } 131 | 132 | /** 133 | * 获取某个插件安装后so文件存放目录 134 | * 135 | * @param pluginName 插件id 136 | * @return 某个插件安装后so文件存放目录 137 | */ 138 | public static String getLibFileInside(String pluginName) { 139 | return getInsidePluginPath() + pluginName + "/" + getInstalledPathInfo(pluginName) + "/" + getLibFile(getCpuArchitecture()); 140 | } 141 | //end========================获取插件相关目录的方法======================end 142 | 143 | /** 144 | * 是否是插件或者补丁 145 | * 146 | * @param pluginId 插件id 147 | * @return 是否是插件或者补丁 148 | */ 149 | public static boolean iszeusPlugin(String pluginId) { 150 | return !TextUtils.isEmpty(pluginId) && 151 | (pluginId.startsWith(PluginConstant.EXP_PLUG_PREFIX) || 152 | isHotFix(pluginId)); 153 | } 154 | 155 | /** 156 | * 是否是插件 157 | * 158 | * @param pluginId 插件id 159 | * @return 是否是插件或者补丁 160 | */ 161 | public static boolean isPlugin(String pluginId) { 162 | return !TextUtils.isEmpty(pluginId) && pluginId.startsWith(PluginConstant.EXP_PLUG_PREFIX); 163 | } 164 | 165 | /** 166 | * 是否是补丁文件 167 | * 168 | * @param pluginId 插件id 169 | * @return 是否是补丁文件 170 | */ 171 | public static boolean isHotFix(String pluginId) { 172 | return !TextUtils.isEmpty(pluginId) && pluginId.startsWith(PluginConstant.EXP_PLUG_HOT_FIX_PREFIX); 173 | } 174 | 175 | /** 176 | * 关闭流 177 | * 178 | * @param closeable closeable 179 | */ 180 | public static void close(Closeable closeable) { 181 | try { 182 | if (closeable != null) closeable.close(); 183 | } catch (Throwable e) { 184 | e.printStackTrace(); 185 | } 186 | } 187 | 188 | 189 | //start========================获取cpu类型的方法========================start 190 | 191 | /** 192 | * 获取cpu类型和架构 193 | * 194 | * @return 返回CPU的指令集类型,仅支持arm,x86和mips这三种,arm中不区分armv6,armv7和neon,有需要自行添加. 195 | */ 196 | public static int getCpuArchitecture() { 197 | try { 198 | InputStream is = new FileInputStream("/proc/cpuinfo"); 199 | InputStreamReader ir = new InputStreamReader(is); 200 | BufferedReader br = new BufferedReader(ir); 201 | try { 202 | String nameProcessor = "Processor"; 203 | String nameModel = "model name"; 204 | while (true) { 205 | String line = br.readLine(); 206 | String[] pair; 207 | if (line == null) { 208 | break; 209 | } 210 | pair = line.split(":"); 211 | if (pair.length != 2) 212 | continue; 213 | String key = pair[0].trim(); 214 | String val = pair[1].trim(); 215 | if (key.compareTo(nameProcessor) == 0) { 216 | if (val.contains("ARM")) { 217 | return CPU_AMR; 218 | } 219 | } 220 | 221 | if (key.compareToIgnoreCase(nameModel) == 0) { 222 | if (val.contains("Intel")) { 223 | return CPU_X86; 224 | } 225 | } 226 | 227 | if (key.compareToIgnoreCase(nameProcessor) == 0) { 228 | if (val.contains("MIPS")) { 229 | return CPU_MIPS; 230 | } 231 | } 232 | } 233 | } finally { 234 | close(br); 235 | close(ir); 236 | close(is); 237 | } 238 | } catch (Exception e) { 239 | e.printStackTrace(); 240 | } 241 | 242 | return CPU_AMR; 243 | } 244 | 245 | /** 246 | * 获取当前应当执行的so文件的存放文件夹 247 | */ 248 | public static String getLibFile(int cpuType) { 249 | switch (cpuType) { 250 | case CPU_AMR: 251 | return "lib/armeabi"; 252 | case CPU_X86: 253 | return "lib/x86/"; 254 | case CPU_MIPS: 255 | return "lib/mips/"; 256 | default: 257 | return "lib/armeabi/"; 258 | } 259 | } 260 | //end========================获取cpu类型的方法========================end 261 | 262 | 263 | //start========================文件相关的方法========================start 264 | 265 | /** 266 | * 创建文件夹 267 | * 268 | * @param dirPath 文件夹路径 269 | * @return 是否成功 270 | */ 271 | public static boolean createDir(String dirPath) { 272 | File file = new File(dirPath); 273 | return !file.exists() && file.mkdirs(); 274 | } 275 | 276 | /** 277 | * 删除文件夹 278 | * 279 | * @param file 文件对象 280 | */ 281 | public static void deleteDirectory(File file) { 282 | if (!file.isDirectory()) { 283 | return; 284 | } 285 | File[] paths = file.listFiles(); 286 | for (File pathF : paths) { 287 | if (pathF.isDirectory()) { 288 | deleteDirectory(pathF); 289 | } else { 290 | deleteFile(pathF); 291 | } 292 | } 293 | deleteFile(file); 294 | } 295 | 296 | /** 297 | * 为防止创建一个正在被删除的文件夹,所以在删除前先重命名该文件夹 298 | * 可以解决很多快速创建删除而产生的0字节大小文件问题 299 | * 300 | * @param file 文件对象 301 | * @return 是否成功 302 | */ 303 | public static boolean deleteFile(File file) { 304 | File to = new File(file.getAbsolutePath() + System.currentTimeMillis()); 305 | file.renameTo(to); 306 | return to.delete(); 307 | } 308 | 309 | /** 310 | * 重命名 311 | * 312 | * @param filePathName 原始文件路径 313 | * @param newPathName 新的文件路径 314 | * @return 是否成功 315 | */ 316 | public static boolean rename(String filePathName, String newPathName) { 317 | if (TextUtils.isEmpty(filePathName)) return false; 318 | if (TextUtils.isEmpty(newPathName)) return false; 319 | 320 | delete(newPathName); 321 | 322 | File file = new File(filePathName); 323 | File newFile = new File(newPathName); 324 | if (!file.exists()) { 325 | return false; 326 | } 327 | File parentFile = newFile.getParentFile(); 328 | if (!parentFile.exists()) { 329 | parentFile.mkdirs(); 330 | } 331 | return file.renameTo(newFile); 332 | } 333 | 334 | /** 335 | * 创建目录,整个路径上的目录都会创建 336 | * 337 | * @param path 路径 338 | * @return 文件 339 | */ 340 | public static File createDirWithFile(String path) { 341 | File file = new File(path); 342 | if (!path.endsWith("/")) { 343 | file = file.getParentFile(); 344 | } 345 | if (!file.exists()) { 346 | file.mkdirs(); 347 | } 348 | return file; 349 | } 350 | 351 | /** 352 | * 删除文件 353 | */ 354 | public static boolean delete(String filePathName) { 355 | if (TextUtils.isEmpty(filePathName)) return false; 356 | File file = new File(filePathName); 357 | return file.isFile() && file.exists() && file.delete(); 358 | } 359 | 360 | /** 361 | * 文件是否存在 362 | * 363 | * @param filePathName 文件路径 364 | * @return 文件是否存在 365 | */ 366 | public static boolean exists(String filePathName) { 367 | if (TextUtils.isEmpty(filePathName)) return false; 368 | File file = new File(filePathName); 369 | return (!file.isDirectory() && file.exists()); 370 | } 371 | //end========================文件相关的方法======================== 372 | 373 | //start========================压缩解压相关方法======================== 374 | 375 | /** 376 | * 读取zip文件中某个文件为字符串 377 | * 378 | * @param zipFile 压缩文件 379 | * @param fileNameReg 需要获取的文件名 380 | * @return 获取的字符串 381 | */ 382 | public static String readZipFileString(String zipFile, String fileNameReg) { 383 | String result = null; 384 | byte[] buffer = new byte[BUF_SIZE]; 385 | InputStream in = null; 386 | ZipInputStream zipIn = null; 387 | ByteArrayOutputStream bos = null; 388 | try { 389 | File file = new File(zipFile); 390 | if (!file.exists()) return null; 391 | in = new FileInputStream(file); 392 | zipIn = new ZipInputStream(in); 393 | ZipEntry entry; 394 | while (null != (entry = zipIn.getNextEntry())) { 395 | String zipName = entry.getName(); 396 | if (zipName.equals(fileNameReg)) { 397 | int bytes; 398 | int count = 0; 399 | bos = new ByteArrayOutputStream(); 400 | 401 | while ((bytes = zipIn.read(buffer, 0, BUF_SIZE)) != -1) { 402 | bos.write(buffer, 0, bytes); 403 | count += bytes; 404 | } 405 | if (count > 0) { 406 | result = bos.toString(); 407 | break; 408 | } 409 | } 410 | } 411 | } catch (Exception e) { 412 | e.printStackTrace(); 413 | } finally { 414 | try { 415 | close(in); 416 | close(zipIn); 417 | close(bos); 418 | } catch (Exception e2) { 419 | e2.printStackTrace(); 420 | } 421 | } 422 | return result; 423 | } 424 | 425 | /** 426 | * 将压缩文件中的某个文件夹拷贝到指定文件夹中 427 | * 428 | * @param zipFile 压缩文件 429 | * @param toDir 指定一个存放解压缩文件的文件夹,或者直接指定文件名方法自动识别 430 | * @param fileNameReg 需要解压的文件夹路径如:res/drawable-hdpi/ 431 | * @return 是否成功 432 | */ 433 | public static boolean unzipFile(String zipFile, String toDir, String fileNameReg) { 434 | boolean result = false; 435 | byte[] buffer = new byte[BUF_SIZE]; 436 | InputStream in = null; 437 | ZipInputStream zipIn = null; 438 | try { 439 | File file = new File(zipFile); 440 | in = new FileInputStream(file); 441 | zipIn = new ZipInputStream(in); 442 | ZipEntry entry; 443 | while (null != (entry = zipIn.getNextEntry())) { 444 | String zipName = entry.getName(); 445 | if (zipName.startsWith(fileNameReg)) { 446 | String relName = toDir + zipName; 447 | File unzipFile = new File(toDir); 448 | if (unzipFile.isDirectory()) { 449 | createDirWithFile(relName); 450 | unzipFile = new File(relName); 451 | } 452 | FileOutputStream out = new FileOutputStream(unzipFile); 453 | int bytes; 454 | 455 | while ((bytes = zipIn.read(buffer, 0, BUF_SIZE)) != -1) { 456 | out.write(buffer, 0, bytes); 457 | } 458 | close(out); 459 | } 460 | } 461 | result = true; 462 | } catch (Exception e) { 463 | e.printStackTrace(); 464 | result = false; 465 | } finally { 466 | close(in); 467 | close(zipIn); 468 | } 469 | return result; 470 | } 471 | 472 | /** 473 | * 复制assets下文件到一个路径下 474 | * @param assetsFileName 要复制的assets的文件名 475 | * @param filePath 复制后的文件的绝对路径 476 | * @return true表示成功了 477 | */ 478 | public static boolean copyAssetsFile(String assetsFileName, String filePath){ 479 | FileOutputStream out = null; 480 | InputStream in = null; 481 | try { 482 | AssetManager am = PluginManager.mBaseResources.getAssets(); 483 | in = am.open(assetsFileName); 484 | PluginUtil.createDirWithFile(filePath); 485 | out = new FileOutputStream(filePath, false); 486 | byte[] temp = new byte[2048]; 487 | int len; 488 | while ((len = in.read(temp)) > 0) { 489 | out.write(temp, 0, len); 490 | } 491 | } catch (Exception e) { 492 | e.printStackTrace(); 493 | return false; 494 | } finally { 495 | close(in); 496 | close(out); 497 | } 498 | return true; 499 | } 500 | //end========================压缩解压相关方法======================== 501 | 502 | /** 503 | * 获取某个插件的安装的随机路径信息 504 | * 505 | * @param pluginId 插件id 506 | * @return 某个插件的安装的随机路径信息 507 | */ 508 | public static String getInstalledPathInfo(String pluginId) { 509 | String result = null; 510 | String libFileInfoPath = getPlugDir(pluginId) + PluginConstant.PLUGIN_INSTALLED_INFO_PATH; 511 | BufferedInputStream bis = null; 512 | ByteArrayOutputStream baos = null; 513 | try { 514 | if (!exists(libFileInfoPath)) return null; 515 | bis = new BufferedInputStream(new FileInputStream(libFileInfoPath)); 516 | baos = new ByteArrayOutputStream(); 517 | byte[] buffer = new byte[BYTE_IN_SIZE]; 518 | int length; 519 | while ((length = bis.read(buffer, 0, BYTE_IN_SIZE)) > -1) { 520 | baos.write(buffer, 0, length); 521 | } 522 | result = new String(baos.toByteArray(), "UTF-8"); 523 | } catch (Exception e) { 524 | e.printStackTrace(); 525 | } finally { 526 | close(bis); 527 | close(baos); 528 | } 529 | return result; 530 | } 531 | 532 | /** 533 | * 保存某个插件的安装随机路径信息 534 | * 535 | * @param pluginId 插件id 536 | * @param installedPathInfo 插件的安装随机路径信息 537 | * @return 是否成功 538 | */ 539 | public static boolean writePathInfo(String pluginId, String installedPathInfo) { 540 | String infoPath = PluginUtil.getPlugDir(pluginId) + PluginConstant.PLUGIN_INSTALLED_INFO_PATH; 541 | File file = new File(infoPath); 542 | FileOutputStream out = null; 543 | try { 544 | if (!file.exists()) { 545 | file.createNewFile(); 546 | } 547 | out = new FileOutputStream(file); 548 | out.write(installedPathInfo.getBytes()); 549 | } catch (Exception e) { 550 | e.printStackTrace(); 551 | return false; 552 | } finally { 553 | close(out); 554 | } 555 | return true; 556 | } 557 | 558 | //start========================反射相关方法======================== 559 | 560 | /** 561 | * 反射的方式设置某个类的成员变量的值 562 | * 563 | * @param paramClass 类对象 564 | * @param paramString 域的名称 565 | * @param newClass 新的对象 566 | */ 567 | public static void setField(Object paramClass, String paramString, 568 | Object newClass) { 569 | if (paramClass == null || TextUtils.isEmpty(paramString)) return; 570 | Field field = null; 571 | Class cl = paramClass.getClass(); 572 | for (; field == null && cl != null; ) { 573 | try { 574 | field = cl.getDeclaredField(paramString); 575 | if (field != null) { 576 | field.setAccessible(true); 577 | } 578 | } catch (Throwable ignored) { 579 | 580 | } 581 | if (field == null) { 582 | cl = cl.getSuperclass(); 583 | } 584 | } 585 | if (field != null) { 586 | try { 587 | field.set(paramClass, newClass); 588 | } catch (Throwable e) { 589 | e.printStackTrace(); 590 | } 591 | } else { 592 | System.err.print(paramString + " is not found in " + paramClass.getClass().getName()); 593 | } 594 | } 595 | 596 | /** 597 | * 设置paramClass中所有名称为paramString的成员变量的值为newClass 598 | * @param paramClass 类对象 599 | * @param paramString 域的名称 600 | * @param newClass 新的对象 601 | */ 602 | public static void setFieldAllClass(Object paramClass, String paramString, 603 | Object newClass) { 604 | if (paramClass == null || TextUtils.isEmpty(paramString)) return; 605 | Field field; 606 | Class cl = paramClass.getClass(); 607 | for (; cl != null; ) { 608 | try { 609 | field = cl.getDeclaredField(paramString); 610 | if (field != null) { 611 | field.setAccessible(true); 612 | field.set(paramClass, newClass); 613 | } 614 | } catch (Throwable e) { 615 | 616 | } 617 | cl = cl.getSuperclass(); 618 | } 619 | 620 | return; 621 | } 622 | 623 | /** 624 | * 反射的方式获取某个类的方法 625 | * 626 | * @param cl 类的class 627 | * @param name 方法名称 628 | * @param parameterTypes 方法对应的输入参数类型 629 | * @return 方法 630 | */ 631 | public static Method getMethod(Class cl, String name, Class... parameterTypes) { 632 | Method method = null; 633 | for (; method == null && cl != null; ) { 634 | try { 635 | method = cl.getDeclaredMethod(name, parameterTypes); 636 | if (method != null) { 637 | method.setAccessible(true); 638 | } 639 | } catch (Exception ignored) { 640 | 641 | } 642 | if (method == null) { 643 | cl = cl.getSuperclass(); 644 | } 645 | } 646 | return method; 647 | } 648 | 649 | /** 650 | * 反射的方式获取某个类的某个成员变量值 651 | * 652 | * @param paramClass 类对象 653 | * @param paramString field的名字 654 | * @return field对应的值 655 | */ 656 | public static Object getField(Object paramClass, String paramString) { 657 | if (paramClass == null) return null; 658 | Field field = null; 659 | Object object = null; 660 | Class cl = paramClass.getClass(); 661 | for (; field == null && cl != null; ) { 662 | try { 663 | field = cl.getDeclaredField(paramString); 664 | if (field != null) { 665 | field.setAccessible(true); 666 | } 667 | } catch (Exception ignored) { 668 | 669 | } 670 | if (field == null) { 671 | cl = cl.getSuperclass(); 672 | } 673 | } 674 | try { 675 | if (field != null) 676 | object = field.get(paramClass); 677 | } catch (IllegalArgumentException | IllegalAccessException e) { 678 | e.printStackTrace(); 679 | } 680 | return object; 681 | } 682 | //end========================反射相关方法======================== 683 | } 684 | -------------------------------------------------------------------------------- /ZeusPlugin/src/main/java/zeus/plugin/ZeusBaseActivity.java: -------------------------------------------------------------------------------- 1 | package zeus.plugin; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.res.Resources; 6 | 7 | /** 8 | * 基础的activity 9 | * Created by huangjian on 2016/6/21. 10 | */ 11 | public class ZeusBaseActivity extends Activity { 12 | 13 | //---------------------插件相关的代码-----------------------start 14 | ZeusHelper helper = new ZeusHelper(); 15 | 16 | @Override 17 | public Object getSystemService(String name) { 18 | return helper.getSystemService(this, super.getSystemService(name), name); 19 | } 20 | 21 | @Override 22 | protected void attachBaseContext(Context newBase) { 23 | super.attachBaseContext(newBase); 24 | helper.attachBaseContext(newBase,this); 25 | } 26 | 27 | @Override 28 | public Resources getResources() { 29 | return PluginManager.getResources(); 30 | } 31 | 32 | /** 33 | * 解决有时插件通过inflate找不到资源的问题 34 | * @return Resources.Theme 35 | */ 36 | @Override 37 | public Resources.Theme getTheme() { 38 | return helper.getTheme(this); 39 | } 40 | 41 | /** 42 | * 给ZeusHelper调用的获取原始theme的方法 43 | * @return 44 | */ 45 | public Resources.Theme getSuperTheme() { 46 | return super.getTheme(); 47 | } 48 | //---------------------------插件相关代码-------------------------end 49 | } 50 | -------------------------------------------------------------------------------- /ZeusPlugin/src/main/java/zeus/plugin/ZeusBaseApplication.java: -------------------------------------------------------------------------------- 1 | package zeus.plugin; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.content.res.Configuration; 6 | import android.content.res.Resources; 7 | 8 | /** 9 | * 基础的Application 10 | * Created by huangjian on 2016/6/21. 11 | */ 12 | public class ZeusBaseApplication extends Application { 13 | 14 | //---------------------插件相关的代码-----------------------start 15 | public ZeusHelper helper = new ZeusHelper(); 16 | 17 | @Override 18 | public Object getSystemService(String name) { 19 | return helper.getSystemService(this, super.getSystemService(name), name); 20 | } 21 | 22 | @Override 23 | public Resources getResources() {//这里需要返回插件框架的resources 24 | return PluginManager.getResources(); 25 | } 26 | 27 | @Override 28 | public void onConfigurationChanged(Configuration newConfig) { 29 | super.onConfigurationChanged(newConfig); 30 | //支持切换语言 31 | ZeusHelper.onConfigurationChanged(); 32 | } 33 | //---------------------插件相关的代码-----------------------end 34 | } 35 | -------------------------------------------------------------------------------- /ZeusPlugin/src/main/java/zeus/plugin/ZeusClassLoader.java: -------------------------------------------------------------------------------- 1 | package zeus.plugin; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.io.File; 6 | 7 | import dalvik.system.PathClassLoader; 8 | 9 | import static java.lang.System.arraycopy; 10 | 11 | /*** 12 | * 这是一个空ClassLoader,主要是个容器 13 | *

14 | * Created by huangjian on 2016/6/21. 15 | */ 16 | class ZeusClassLoader extends PathClassLoader {//DexClassLoader { 17 | //这里每个插件对应着一个ClassLoader,一旦插件更新了,则classLoader也会使用新的。 18 | //这样java的class就会从新的classLoader中查找,而不会去使用旧的classLoader的缓存 19 | private ZeusPluginClassLoader[] mClassLoader = null; 20 | private String mNativeLibraryPath = null; 21 | 22 | public ZeusClassLoader(String dexPath, ClassLoader parent, String nativeLibraryPath) { 23 | super(dexPath, parent); 24 | mNativeLibraryPath = nativeLibraryPath; 25 | } 26 | 27 | public ZeusPluginClassLoader[] getClassLoaders() { 28 | return mClassLoader; 29 | } 30 | 31 | /** 32 | * 添加一个插件到当前的classLoader中 33 | * 34 | * @param pluginId 插件名称 35 | * @param dexPath dex文件路径 36 | * @param libPath so文件夹路径 37 | */ 38 | protected void addAPKPath(String pluginId, String dexPath, String libPath) { 39 | if (mClassLoader == null) { 40 | mClassLoader = new ZeusPluginClassLoader[1]; 41 | } else { 42 | int oldLenght = mClassLoader.length; 43 | Object[] old = mClassLoader; 44 | mClassLoader = new ZeusPluginClassLoader[oldLenght + 1]; 45 | arraycopy(old, 0, mClassLoader, 0, oldLenght); 46 | } 47 | mClassLoader[mClassLoader.length - 1] = new ZeusPluginClassLoader(pluginId, dexPath, 48 | PluginUtil.getDexCacheParentDirectPath(pluginId), 49 | libPath, 50 | getParent()); 51 | } 52 | 53 | /** 54 | * 移除一个插件classLoader 55 | * 56 | * @param pluginId 插件id 57 | */ 58 | protected void removePlugin(String pluginId) { 59 | if (mClassLoader == null || TextUtils.isEmpty(pluginId)) return; 60 | for (int i = 0; i < mClassLoader.length; i++) { 61 | ZeusPluginClassLoader cl = mClassLoader[i]; 62 | if (pluginId.equals(cl.getPluginId())) { 63 | if (mClassLoader.length == 1) { 64 | mClassLoader = null; 65 | return; 66 | } 67 | int oldLength = mClassLoader.length; 68 | Object[] old = mClassLoader; 69 | mClassLoader = new ZeusPluginClassLoader[oldLength - 1]; 70 | if (i != 0) { 71 | arraycopy(old, 0, mClassLoader, 0, i); 72 | } 73 | if (i != oldLength - 1) { 74 | arraycopy(old, i + 1, mClassLoader, i, oldLength - i - 1); 75 | } 76 | return; 77 | } 78 | } 79 | } 80 | 81 | @Override 82 | protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException { 83 | Class clazz = null; 84 | try { 85 | //先查找parent classLoader,这里实际就是系统帮我们创建的classLoader,目标对应为宿主apk 86 | clazz = getParent().loadClass(className); 87 | } catch (ClassNotFoundException ignored) { 88 | 89 | } 90 | 91 | if (clazz != null) { 92 | return clazz; 93 | } 94 | 95 | //挨个的到插件里进行查找 96 | if (mClassLoader != null) { 97 | for (ZeusPluginClassLoader classLoader : mClassLoader) { 98 | if (classLoader == null) continue; 99 | try { 100 | //这里只查找插件它自己的apk,不需要查parent,避免多次无用查询,提高性能 101 | clazz = classLoader.loadClassByself(className); 102 | if (clazz != null) { 103 | return clazz; 104 | } 105 | } catch (ClassNotFoundException ignored) { 106 | 107 | } 108 | } 109 | } 110 | throw new ClassNotFoundException(className + " in loader " + this); 111 | } 112 | 113 | @Override 114 | public String findLibrary(String LibraryName) { 115 | String pathName = super.findLibrary(LibraryName); 116 | if (!TextUtils.isEmpty(pathName)) return pathName; 117 | if (!TextUtils.isEmpty(mNativeLibraryPath)) { 118 | String fileName = System.mapLibraryName(LibraryName); 119 | File libraryFile = new File(mNativeLibraryPath, fileName); 120 | if (libraryFile.exists()) { 121 | return libraryFile.getAbsolutePath(); 122 | } 123 | return null; 124 | } 125 | return null; 126 | } 127 | } 128 | 129 | -------------------------------------------------------------------------------- /ZeusPlugin/src/main/java/zeus/plugin/ZeusHelper.java: -------------------------------------------------------------------------------- 1 | package zeus.plugin; 2 | 3 | import android.content.Context; 4 | import android.content.ContextWrapper; 5 | import android.content.res.Resources; 6 | import android.view.LayoutInflater; 7 | 8 | /** 9 | * 一些重复的方法放到这里。 10 | * Created by huangjian on 2016/7/14. 11 | */ 12 | public class ZeusHelper { 13 | 14 | //---------------------插件相关的代码-----------------------start 15 | /** 16 | * 一旦插件resources发生变化,这个resources就可以用来比较了 17 | */ 18 | private Resources mMyResources = null; 19 | 20 | /** 21 | * 配置LAYOUT_INFLATER_SERVICE时的一些参数 22 | * 23 | * @param context 调用着的context 24 | * @param systemServcie systemServer对象 25 | * @param name server的名字 26 | * @return systemServer对象 27 | */ 28 | public static Object getSystemService(Context context, Object systemServcie, String name) { 29 | if (Context.LAYOUT_INFLATER_SERVICE.equals(name)) { 30 | LayoutInflater inflater = (LayoutInflater) systemServcie; 31 | inflater.cloneInContext(context); 32 | //使用某些加固之后该inflater里的mContext变量一直是系统的context,根本不是当前Context 33 | //所以这里手动设置一次 34 | PluginUtil.setField(inflater, "mContext", context); 35 | return inflater; 36 | } 37 | return systemServcie; 38 | } 39 | 40 | /** 41 | * 当系统调用attachBaseContext时,进行一些参数的设置 42 | * 43 | * @param newBase base的context即ContextImpl 44 | * @param context 调用者自己 45 | */ 46 | public void attachBaseContext(Context newBase, ContextWrapper context) { 47 | //某些手机中的是mOuterContext作为context来用 48 | //这样写还可以防止某些手机的内存泄漏,有些手机会记录它启动当前界面的activity作为mOuterContext, 49 | //而如果之前的activity被finish,那么它也不能被GC回收 50 | PluginUtil.setField(newBase, "mOuterContext", context); 51 | //中兴手机是个奇葩,不知道它怎么实现的又重新生成了一个resources,这里得再次替换 52 | PluginUtil.setField(newBase, "mResources", PluginManager.mNowResources); 53 | mMyResources = PluginManager.mNowResources; 54 | } 55 | 56 | /** 57 | * 解决有时插件通过inflate找不到资源的问题 58 | * 59 | * @return Resources.Theme 调用者自己生成的theme 60 | */ 61 | public Resources.Theme getTheme(ZeusBaseActivity zeusBaseActivity) { 62 | Resources localResources = PluginManager.mNowResources; 63 | if ( localResources != null && mMyResources != localResources) { 64 | mMyResources = localResources; 65 | 66 | //中兴的rom中会将zeusBaseActivity.getBaseContext()的一个父类中添加mResources和mTheme,导致设置不全,这里全部设置 67 | PluginUtil.setFieldAllClass(zeusBaseActivity.getBaseContext(), "mResources", localResources); 68 | PluginUtil.setFieldAllClass(zeusBaseActivity.getBaseContext(), "mTheme", null); 69 | 70 | //AppCompatActivity包含了一个Resouces,这里设置为null让其再次生成一遍 71 | PluginUtil.setField(zeusBaseActivity, "mResources", null); 72 | //原始的theme指向的Resources是老的Resources,无法访问新插件,这里设置为null, 73 | // 系统会再次使用新的Resouces来生成一次theme,新的theme才能访问新的插件资源 74 | PluginUtil.setField(zeusBaseActivity, "mTheme", null); 75 | } 76 | return zeusBaseActivity.getSuperTheme(); 77 | } 78 | 79 | /** 80 | * 系统配置改变时的回调,是为了支持插件的语言、地区、字体、字号等的切换 81 | */ 82 | public static void onConfigurationChanged() { 83 | if (PluginManager.mNowResources != null 84 | && PluginManager.mBaseResources != null 85 | && PluginManager.mNowResources != PluginManager.mBaseResources) { 86 | PluginManager.mNowResources.updateConfiguration(PluginManager.mBaseResources.getConfiguration(), 87 | PluginManager.mBaseResources.getDisplayMetrics()); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /ZeusPlugin/src/main/java/zeus/plugin/ZeusHotfixClassLoader.java: -------------------------------------------------------------------------------- 1 | package zeus.plugin; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.lang.reflect.Method; 8 | import java.util.ArrayList; 9 | import java.util.zip.ZipFile; 10 | 11 | import dalvik.system.DexFile; 12 | 13 | import static java.lang.System.arraycopy; 14 | 15 | /*** 16 | * 加载任意后缀的zip格式的classLoader,主要用来加载热修复补丁 17 | * 热修复补丁与插件的区别是热修复补丁只能软件启动的时候加载一次,插件可以随时加载、卸载。 18 | *

19 | * Created by huangjian on 2016/6/21. 20 | */ 21 | class ZeusHotfixClassLoader extends ZeusPluginClassLoader { 22 | private ClassLoader mChild = null; 23 | private Method findClassMethod = null; 24 | private Method findLoadedClassMethod = null; 25 | 26 | public ZeusHotfixClassLoader(String dexPath, String dexOutputDir, String libPath, 27 | ClassLoader parent) { 28 | super(null, dexPath, dexOutputDir, libPath,parent); 29 | } 30 | 31 | protected void setOrgAPKClassLoader(ClassLoader child) { 32 | mChild = child; 33 | findLoadedClassMethod = PluginUtil.getMethod(mChild.getClass(), "findLoadedClass", String.class); 34 | findClassMethod = PluginUtil.getMethod(mChild.getClass(), "findClass", String.class); 35 | } 36 | 37 | protected void addAPKPath(String dexPath, String libPath) { 38 | if(mDexs == null){ 39 | ensureInit(); 40 | } 41 | int oldLength = mDexs.length; 42 | int index = oldLength + 1; 43 | 44 | Object[] old = mDexs; 45 | mDexs = new DexFile[index]; 46 | arraycopy(old, 0, mDexs, 0, index - 1); 47 | 48 | old = mFiles; 49 | mFiles = new File[index]; 50 | arraycopy(old, 0, mFiles, 0, index - 1); 51 | 52 | old = mZips; 53 | mZips = new ZipFile[index]; 54 | arraycopy(old, 0, mZips, 0, index - 1); 55 | 56 | if (!TextUtils.isEmpty(libPath)) { 57 | String pathSep = System.getProperty("path.separator", ":"); 58 | if (mRawLibPath.endsWith(pathSep)) { 59 | mRawLibPath = mRawLibPath + libPath; 60 | } else { 61 | mRawLibPath = mRawLibPath + pathSep + libPath; 62 | } 63 | generateLibPath(); 64 | 65 | } 66 | 67 | File pathFile = new File(dexPath); 68 | mFiles[oldLength] = pathFile; 69 | if (pathFile.isFile()) { 70 | try { 71 | mZips[oldLength] = new ZipFile(pathFile); 72 | } catch (IOException ioex) { 73 | System.out.println("Failed opening '" + pathFile 74 | + "': " + ioex); 75 | } 76 | } 77 | 78 | try { 79 | String outputName = 80 | generateOutputName(dexPath, mDexOutputPath); 81 | mDexs[oldLength] = DexFile.loadDex(dexPath, outputName, 0); 82 | } catch (IOException e) { 83 | e.printStackTrace(); 84 | } 85 | } 86 | 87 | /*** 88 | * 每个插件里也可能有多个dex文件,挨个的查找dex文件 89 | */ 90 | @Override 91 | protected Class findClass(String name) throws ClassNotFoundException { 92 | ensureInit(); 93 | Class clazz; 94 | int length = mFiles.length; 95 | for (int i = 0; i < length; i++) { 96 | 97 | if (mDexs[i] != null) { 98 | String slashName = name.replace('.', '/'); 99 | clazz = mDexs[i].loadClass(slashName, mChild); 100 | if (clazz != null) { 101 | return clazz; 102 | } 103 | } 104 | } 105 | throw new ClassNotFoundException(name + " in loader " + this); 106 | } 107 | 108 | @Override 109 | protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException { 110 | //先查找补丁自己已经加载过的有没有 111 | Class clazz = findLoadedClass(className); 112 | 113 | if (clazz == null) { 114 | try { 115 | //查查parent中有没有,也就是android系统中的 116 | clazz = getParent().loadClass(className); 117 | } catch (ClassNotFoundException ignored) { 118 | 119 | } 120 | 121 | if (clazz == null) { 122 | try { 123 | //查查自己有没有,就是补丁中有没有 124 | clazz = findClass(className); 125 | } catch (ClassNotFoundException ignored) { 126 | 127 | } 128 | } 129 | } 130 | //查查child中有没有,child是设置进来的,实际就是宿主apk中有没有 131 | if (clazz == null && mChild != null) { 132 | try { 133 | if (findLoadedClassMethod != null) { 134 | clazz = (Class) findLoadedClassMethod.invoke(mChild, className); 135 | } 136 | if (clazz != null) return clazz; 137 | if (findClassMethod != null) { 138 | clazz = (Class) findClassMethod.invoke(mChild, className); 139 | return clazz; 140 | } 141 | } catch (Exception ignored) { 142 | 143 | } 144 | } 145 | if (clazz == null) { 146 | throw new ClassNotFoundException(className + " in loader " + this); 147 | } 148 | return clazz; 149 | } 150 | } 151 | 152 | -------------------------------------------------------------------------------- /ZeusPlugin/src/main/java/zeus/plugin/ZeusInstrumentation.java: -------------------------------------------------------------------------------- 1 | package zeus.plugin; 2 | 3 | import android.app.Activity; 4 | import android.app.Instrumentation; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.text.TextUtils; 8 | 9 | /** 10 | * Created by huangjian on 2016/7/28. 11 | */ 12 | public class ZeusInstrumentation extends Instrumentation{ 13 | 14 | @Override 15 | public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { 16 | if(intent != null){ 17 | Bundle bundle = intent.getExtras(); 18 | if(bundle != null){ 19 | //给Bundle设置classLoader以使Bundle中序列化对象可以直接转化为插件中的对象 20 | //类似于在宿主中这么使用:TestInPlugin testInPlugin = (TestInPlugin)bundle.get("TestInPlugin"); 21 | //TestInPlugin是在插件中定义的,如果不这么设置则会找不到TestInPlugin类 22 | bundle.setClassLoader(PluginManager.mNowClassLoader); 23 | if(className.equals("com.zeus.ZeusActivityForStandard")) { 24 | String realActivity = bundle.getString(PluginConstant.PLUGIN_REAL_ACTIVITY); 25 | if (!TextUtils.isEmpty(realActivity)) { 26 | return super.newActivity(cl, realActivity, intent); 27 | } 28 | } 29 | } 30 | } 31 | return super.newActivity(cl, className, intent); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ZeusPlugin/src/main/java/zeus/plugin/ZeusPlugin.java: -------------------------------------------------------------------------------- 1 | package zeus.plugin; 2 | 3 | import android.content.res.AssetManager; 4 | import android.text.TextUtils; 5 | 6 | import org.json.JSONObject; 7 | 8 | import java.io.File; 9 | import java.io.FileOutputStream; 10 | import java.io.InputStream; 11 | 12 | import dalvik.system.DexClassLoader; 13 | 14 | /** 15 | * 插件类,包括插件的安装、卸载、清除 16 | * Created by huangjian on 2016/6/21. 17 | */ 18 | public class ZeusPlugin { 19 | private String mPluginId; //使用插件的安装目录作为插件id 20 | private String mInstalledPathInfo = ""; //安装插件的随机路径信息 21 | 22 | private boolean isInstalling = false; 23 | private boolean isAssetInstalling = false; 24 | 25 | protected ZeusPlugin(String pluginId) { 26 | mPluginId = pluginId; 27 | } 28 | 29 | public synchronized boolean install() { 30 | isInstalling = true; 31 | //创建插件安装目录 32 | PluginUtil.createDir(PluginUtil.getPlugDir(mPluginId)); 33 | 34 | //将当前时间记录为插件的随机数,等效于android系统后面~1、~2等 35 | mInstalledPathInfo = String.valueOf(System.nanoTime()); 36 | 37 | //获取插件apk文件 38 | String path = PluginUtil.getZipPath(mPluginId); 39 | 40 | //插件文件不存在,则安装asset中的默认插件。 41 | if (!PluginUtil.exists(path)) { 42 | if (PluginUtil.iszeusPlugin(mPluginId)) { 43 | isInstalling = false; 44 | mInstalledPathInfo = getInstalledPathInfoNoCache(); 45 | return false; 46 | } 47 | return installAssetPlugin(); 48 | } 49 | //把下载路径下的插件文件,直接重命名到安装目录,不需要耗时的拷贝过程。 50 | boolean ret = PluginUtil.rename(PluginUtil.getZipPath(mPluginId), getAPKPath(mPluginId)); 51 | if (!ret) { 52 | isInstalling = false; 53 | mInstalledPathInfo = getInstalledPathInfoNoCache(); 54 | return false; 55 | } 56 | 57 | //校验是否下载的是正确文件,如果插件下载错误则获取这个配置文件就会失败。 58 | PluginManifest meta = getPluginMeta(); 59 | if (meta == null) { 60 | PluginUtil.deleteFile(new File(getAPKPath(mPluginId))); 61 | mInstalledPathInfo = getInstalledPathInfoNoCache(); 62 | isInstalling = false; 63 | return false; 64 | } 65 | 66 | //拷贝so文件,一些插件是没有so文件,而这个方法耗时还稍微高点,所以对于没有so的插件和补丁是不会拷贝的。 67 | if (((meta.getFlag() & PluginManifest.FLAG_WITHOUT_SO_FILE) != PluginManifest.FLAG_WITHOUT_SO_FILE )&& 68 | !copySoFile(mInstalledPathInfo, PluginUtil.getCpuArchitecture())) { 69 | isInstalling = false; 70 | mInstalledPathInfo = getInstalledPathInfoNoCache(); 71 | return false; 72 | } 73 | 74 | if (!PluginUtil.writePathInfo(mPluginId, mInstalledPathInfo)) { 75 | isInstalling = false; 76 | mInstalledPathInfo = getInstalledPathInfoNoCache(); 77 | return false; 78 | } 79 | try { 80 | //预优化补丁dex的加载 81 | new DexClassLoader(getAPKPath(mPluginId), PluginUtil.getDexCacheParentDirectPath(mPluginId), "", PluginManager.mBaseClassLoader); 82 | }catch (Throwable e){ 83 | e.printStackTrace(); 84 | } 85 | PluginManager.addInstalledPlugin(mPluginId, meta); 86 | isInstalling = false; 87 | return true; 88 | } 89 | 90 | /** 91 | * 安装assets中的插件,assets中插件的文件名要为 mPluginId +".apk" 92 | * 93 | * @return 是否成功 94 | */ 95 | public boolean installAssetPlugin() { 96 | PluginManifest meta; 97 | synchronized (this) { 98 | isAssetInstalling = true; 99 | Integer version = PluginManager.getDefaultPlugin().get(mPluginId); 100 | if (version == null || PluginManager.isInstall(mPluginId, version)) { 101 | isInstalling = false; 102 | return true; 103 | } 104 | PluginUtil.createDir(PluginUtil.getPlugDir(mPluginId)); 105 | mInstalledPathInfo = String.valueOf(System.nanoTime()); 106 | 107 | FileOutputStream out = null; 108 | InputStream in = null; 109 | try { 110 | AssetManager am = PluginManager.mBaseResources.getAssets(); 111 | in = am.open(mPluginId + PluginConstant.PLUGIN_SUFF); 112 | PluginUtil.createDirWithFile(getAPKPath(mPluginId)); 113 | out = new FileOutputStream(getAPKPath(mPluginId), false); 114 | byte[] temp = new byte[2048]; 115 | int len; 116 | while ((len = in.read(temp)) > 0) { 117 | out.write(temp, 0, len); 118 | } 119 | } catch (Exception e) { 120 | e.printStackTrace(); 121 | mInstalledPathInfo = getInstalledPathInfoNoCache(); 122 | isInstalling = false; 123 | return false; 124 | } finally { 125 | PluginUtil.close(in); 126 | PluginUtil.close(out); 127 | } 128 | 129 | meta = getPluginMeta(); 130 | if (meta == null) { 131 | PluginUtil.deleteFile(new File(getAPKPath(mPluginId))); 132 | isAssetInstalling = false; 133 | mInstalledPathInfo = getInstalledPathInfoNoCache(); 134 | return false; 135 | } 136 | 137 | //拷贝so文件 138 | if (!copySoFile(mInstalledPathInfo, PluginUtil.getCpuArchitecture())) { 139 | isInstalling = false; 140 | mInstalledPathInfo = getInstalledPathInfoNoCache(); 141 | return false; 142 | } 143 | 144 | if (!PluginUtil.writePathInfo(mPluginId, mInstalledPathInfo)) { 145 | isAssetInstalling = false; 146 | mInstalledPathInfo = getInstalledPathInfoNoCache(); 147 | return false; 148 | } 149 | isAssetInstalling = false; 150 | } 151 | try { 152 | //预优化补丁dex的加载 153 | new DexClassLoader(getAPKPath(mPluginId), PluginUtil.getDexCacheParentDirectPath(mPluginId), "", PluginManager.mBaseClassLoader); 154 | }catch (Throwable e){ 155 | e.printStackTrace(); 156 | } 157 | PluginManager.addInstalledPlugin(mPluginId, meta); 158 | return true; 159 | } 160 | 161 | /** 162 | * 清除之前版本的旧数据 163 | */ 164 | public synchronized void clearOldPlugin() { 165 | if (getInstalledPathInfo() == null || isAssetInstalling || isInstalling) return; 166 | File pluginDir = new File(PluginUtil.getPlugDir(mPluginId)); 167 | String installedPathInfo = getInstalledPathInfoNoCache(); 168 | if (TextUtils.isEmpty(installedPathInfo)) return; 169 | if (pluginDir.exists() && pluginDir.isDirectory()) { 170 | File[] list = pluginDir.listFiles(); 171 | if (list == null) return; 172 | for (File f : list) { 173 | String fileFullName = f.getName(); 174 | if (fileFullName.endsWith(PluginConstant.PLUGIN_JAR_SUFF) || fileFullName.endsWith(PluginConstant.PLUGIN_SUFF)) { 175 | String fileName = fileFullName.substring(0, fileFullName.lastIndexOf(".")); 176 | if (!fileName.equalsIgnoreCase(installedPathInfo)) { 177 | f.delete(); 178 | File dir = new File(f.getParent() + "/" + fileName); 179 | PluginUtil.deleteDirectory(dir); 180 | 181 | File cacheFile = new File(PluginUtil.getDexCacheFilePath(mPluginId, fileName)); 182 | if (cacheFile.exists()) { 183 | cacheFile.delete(); 184 | } 185 | } 186 | } 187 | } 188 | } 189 | } 190 | 191 | /** 192 | * 将插件中的lib库拷贝入手机内存中。 193 | * 根据当前手机cpu的类型拷贝合适的lib库入手机内存中 194 | * 195 | * @param installedPathInfo 安装的随机路径信息 196 | * @param cpuType CPU_AMR:1 CPU_X86:2 CPU_MIPS:3 具体见{@link PluginUtil}中的getCpuArchitecture()和getLibFile()方法 197 | * @return 是否成功 198 | */ 199 | protected boolean copySoFile(String installedPathInfo, int cpuType) { 200 | String insideLibPath = PluginUtil.getInsidePluginPath() + mPluginId + "/" + installedPathInfo + "/"; 201 | PluginUtil.createDir(insideLibPath); 202 | String apkLibPath = PluginUtil.getLibFile(cpuType); 203 | //首先将apk中libs文件夹下的一级so文件拷贝 204 | return PluginUtil.unzipFile(getAPKPath(mPluginId), insideLibPath, apkLibPath); 205 | } 206 | 207 | /** 208 | * 获取插件已经安装的apk路径 209 | * 210 | * @param pluginName 插件id 211 | * @return 插件已经安装的apk路径 212 | */ 213 | public String getAPKPath(String pluginName) { 214 | return PluginUtil.getPlugDir(pluginName) + getInstalledPathInfo() + PluginConstant.PLUGIN_SUFF; 215 | } 216 | 217 | /** 218 | * 获取当前安装的随机路径信息,不使用缓存,直接读取文件 219 | * 220 | * @return 当前安装的随机路径信息 221 | */ 222 | public String getInstalledPathInfoNoCache() { 223 | return PluginUtil.getInstalledPathInfo(mPluginId); 224 | } 225 | 226 | /** 227 | * 获取插件清单文件信息,不使用缓存,读取速度很快 228 | * 229 | * @return 插件清单文件信息 230 | */ 231 | public PluginManifest getPluginMeta() { 232 | PluginManifest meta = null; 233 | String result = readMeta(); 234 | if (!TextUtils.isEmpty(result)) { 235 | meta = parserMeta(result); 236 | } 237 | return meta; 238 | } 239 | 240 | /** 241 | * 解析清单文件 242 | * 243 | * @param metaString meta字符串 244 | * @return PluginManifest对象 245 | */ 246 | private PluginManifest parserMeta(String metaString) { 247 | PluginManifest meta = new PluginManifest(); 248 | try { 249 | JSONObject jObject = new JSONObject(metaString.replaceAll("\r|\n", "")); 250 | meta.name = jObject.optString(PluginManifest.PLUG_NAME); 251 | meta.minVersion = jObject.optString(PluginManifest.PLUG_MIN_VERSION); 252 | meta.maxVersion = jObject.optString(PluginManifest.PLUG_MAX_VERSION); 253 | meta.version = jObject.optString(PluginManifest.PLUG_VERSION); 254 | meta.mainClass = jObject.optString(PluginManifest.PLUG_MAINCLASS); 255 | meta.otherInfo = jObject.optString(PluginManifest.PLUG_OTHER_INFO); 256 | } catch (Exception e) { 257 | e.printStackTrace(); 258 | return null; 259 | } 260 | return meta; 261 | } 262 | 263 | /** 264 | * 卸载某个插件,通常情况下不需要卸载,除非需要显示调用 265 | * 266 | * @return 是否成功 267 | */ 268 | public boolean uninstall() { 269 | try { 270 | PluginManager.unInstalledPlugin(mPluginId); 271 | //删除手机内存中/data/data/packageName/plugins/mPluginName下的文件 272 | File baseModulePathF = new File(PluginUtil.getPlugDir(mPluginId)); 273 | PluginUtil.deleteDirectory(baseModulePathF); 274 | } catch (Exception e) { 275 | e.printStackTrace(); 276 | return false; 277 | } 278 | return true; 279 | } 280 | 281 | /** 282 | * 读取meta文件 283 | * 284 | * @return meta字符串 285 | */ 286 | private String readMeta() { 287 | return PluginUtil.readZipFileString(getAPKPath(mPluginId), PluginConstant.PLUGINWEB_MAINIFEST_FILE); 288 | } 289 | 290 | /** 291 | * 获取当前安装的随机路径信息,有缓存则使用缓存 292 | * 293 | * @return 当前安装的随机路径信息 294 | */ 295 | private String getInstalledPathInfo() { 296 | if (!TextUtils.isEmpty(mInstalledPathInfo)) { 297 | return mInstalledPathInfo; 298 | } 299 | mInstalledPathInfo = PluginUtil.getInstalledPathInfo(mPluginId); 300 | return mInstalledPathInfo; 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /ZeusPlugin/src/main/java/zeus/plugin/ZeusPluginClassLoader.java: -------------------------------------------------------------------------------- 1 | package zeus.plugin; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.util.zip.ZipFile; 8 | 9 | import dalvik.system.DexFile; 10 | 11 | /*** 12 | * 加载任意后缀的zip格式的classLoader,主要用来加载apk文件 13 | * 如果插件是放在SD卡目录下,最好的格式是jar,有些手机的DexFile只能识别apk或者jar后缀的,因为apk格式会被清理软件当作未安装apk文件清理掉 14 | *

15 | * Created by huangjian on 2016/6/21. 16 | */ 17 | class ZeusPluginClassLoader extends ClassLoader { 18 | 19 | protected String mRawLibPath; 20 | protected final String mDexOutputPath; 21 | protected File[] mFiles; 22 | protected ZipFile[] mZips; 23 | protected DexFile[] mDexs; 24 | protected String[] mLibPaths; 25 | 26 | private boolean mInitialized; 27 | public final String mRawDexPath; 28 | final private String mPluginId; 29 | 30 | public ZeusPluginClassLoader(String pluginId, String dexPath, String dexOutputDir, String libPath, 31 | ClassLoader parent) { 32 | 33 | super(parent); 34 | if (dexPath == null || dexOutputDir == null) 35 | throw new NullPointerException(); 36 | mPluginId = pluginId; 37 | mRawDexPath = dexPath; 38 | mDexOutputPath = dexOutputDir; 39 | mRawLibPath = libPath; 40 | } 41 | 42 | public String getPluginId() { 43 | return mPluginId; 44 | } 45 | 46 | /*** 47 | * 初始化 48 | */ 49 | protected synchronized void ensureInit() { 50 | if (mInitialized) { 51 | return; 52 | } 53 | 54 | String[] dexPathList; 55 | 56 | mInitialized = true; 57 | 58 | dexPathList = mRawDexPath.split(":"); 59 | int length = dexPathList.length; 60 | 61 | mFiles = new File[length]; 62 | mZips = new ZipFile[length]; 63 | mDexs = new DexFile[length]; 64 | 65 | for (int i = 0; i < length; i++) { 66 | File pathFile = new File(dexPathList[i]); 67 | mFiles[i] = pathFile; 68 | 69 | if (pathFile.isFile()) { 70 | try { 71 | mZips[i] = new ZipFile(pathFile); 72 | } catch (IOException ioex) { 73 | System.out.println("Failed opening '" + pathFile 74 | + "': " + ioex); 75 | } 76 | try { 77 | String outputName = 78 | generateOutputName(dexPathList[i], mDexOutputPath); 79 | mDexs[i] = DexFile.loadDex(dexPathList[i], outputName, 0); 80 | } catch (IOException e) { 81 | e.printStackTrace(); 82 | } 83 | } 84 | } 85 | generateLibPath(); 86 | } 87 | 88 | protected void generateLibPath() { 89 | //优先查找本地添加的lib库,然后在去系统级别的去找,防止自己的插件中so跟系统存在的so重名了 90 | String pathList; 91 | String systemPathList = System.getProperty("java.library.path", "."); 92 | String pathSep = System.getProperty("path.separator", ":"); 93 | String fileSep = System.getProperty("file.separator", "/"); 94 | 95 | if (mRawLibPath != null) { 96 | if (systemPathList.length() > 0) { 97 | if (mRawLibPath.endsWith(pathSep)) { 98 | pathList = mRawLibPath + systemPathList; 99 | } else { 100 | pathList = mRawLibPath + pathSep + systemPathList; 101 | } 102 | } else { 103 | pathList = mRawLibPath; 104 | } 105 | } else { 106 | pathList = systemPathList; 107 | } 108 | 109 | mLibPaths = pathList.split(pathSep); 110 | int length = mLibPaths.length; 111 | 112 | for (int i = 0; i < length; i++) { 113 | if (!mLibPaths[i].endsWith(fileSep)) 114 | mLibPaths[i] += fileSep; 115 | } 116 | } 117 | 118 | protected static String generateOutputName(String sourcePathName, 119 | String outputDir) { 120 | StringBuilder newStr = new StringBuilder(80); 121 | 122 | newStr.append(outputDir); 123 | if (!outputDir.endsWith("/")) 124 | newStr.append("/"); 125 | 126 | String sourceFileName; 127 | int lastSlash = sourcePathName.lastIndexOf("/"); 128 | if (lastSlash < 0) 129 | sourceFileName = sourcePathName; 130 | else 131 | sourceFileName = sourcePathName.substring(lastSlash + 1); 132 | 133 | int lastDot = sourceFileName.lastIndexOf("."); 134 | if (lastDot < 0) 135 | newStr.append(sourceFileName); 136 | else 137 | newStr.append(sourceFileName, 0, lastDot); 138 | newStr.append(".dex"); 139 | 140 | return newStr.toString(); 141 | } 142 | 143 | protected Class loadClass(String className, boolean resolve) throws ClassNotFoundException { 144 | Class clazz = findLoadedClass(className); 145 | 146 | if (clazz == null) { 147 | try { 148 | clazz = getParent().loadClass(className); 149 | } catch (ClassNotFoundException e) { 150 | } 151 | 152 | if (clazz == null) { 153 | try { 154 | clazz = findClass(className); 155 | } catch (ClassNotFoundException e) { 156 | throw e; 157 | } 158 | } 159 | } 160 | 161 | return clazz; 162 | } 163 | 164 | /*** 165 | * 每个插件里也可能有多个dex文件,挨个的查找dex文件 166 | */ 167 | @Override 168 | protected Class findClass(String name) throws ClassNotFoundException { 169 | ensureInit(); 170 | Class clazz; 171 | int length = mFiles.length; 172 | for (int i = 0; i < length; i++) { 173 | 174 | if (mDexs[i] != null) { 175 | String slashName = name.replace('.', '/'); 176 | clazz = mDexs[i].loadClass(slashName, this); 177 | if (clazz != null) { 178 | return clazz; 179 | } 180 | } 181 | } 182 | throw new ClassNotFoundException(name + " in loader " + this); 183 | } 184 | 185 | 186 | /** 187 | * 只查找插件自己是否存在该class 188 | * 189 | * @param className 类名字 190 | * @return 类对象 191 | * @throws ClassNotFoundException 192 | */ 193 | public Class loadClassByself(String className) throws ClassNotFoundException { 194 | Class clazz = findLoadedClass(className); 195 | 196 | if (clazz == null) { 197 | clazz = findClass(className); 198 | } 199 | 200 | return clazz; 201 | } 202 | 203 | @Override 204 | protected String findLibrary(String libname) {//根据插件的pathInfo查找对应的so 205 | ensureInit(); 206 | 207 | String fileName = System.mapLibraryName(libname); 208 | for (String libPath : mLibPaths) { 209 | String pathName = libPath + fileName; 210 | File test = new File(pathName); 211 | 212 | if (test.exists()) { 213 | return pathName; 214 | } 215 | } 216 | return null; 217 | } 218 | 219 | } 220 | 221 | 222 | -------------------------------------------------------------------------------- /aapt/aapt(linux64位版): -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iReaderAndroid/ZeusPlugin/ce1008040165602ed9a9544571529af5b6b83d79/aapt/aapt(linux64位版) -------------------------------------------------------------------------------- /aapt/aapt(mac版): -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iReaderAndroid/ZeusPlugin/ce1008040165602ed9a9544571529af5b6b83d79/aapt/aapt(mac版) -------------------------------------------------------------------------------- /aapt/aapt(windows版).exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iReaderAndroid/ZeusPlugin/ce1008040165602ed9a9544571529af5b6b83d79/aapt/aapt(windows版).exe -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | //start----------补丁相关,使用gradle插件,如果宿主是只运行于android 4.4及以上可以不添加这些代码------------------ 4 | apply plugin: 'patch-gradle-plugin' 5 | //end------------补丁相关,使用gradle插件,如果宿主是只运行于android 4.4及以上可以不添加这些代码------------------ 6 | apply plugin: 'jar-gradle-plugin'//生成jar包插件 7 | 8 | //为了可以使用public.xml来固定宿主的资源ID 9 | apply from: 'public-xml.gradle' 10 | 11 | android { 12 | compileSdkVersion 23 13 | buildToolsVersion '23.0.2' 14 | 15 | defaultConfig { 16 | applicationId "zeus.test" 17 | minSdkVersion 8 18 | targetSdkVersion 23 19 | versionCode 1 20 | versionName "1.0" 21 | } 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | /** 29 | * --PLUG-resoure-proguard 表示资源混淆,去掉则资源不混淆 30 | * --PLUG-resoure-id 表示资源id命名(Package ID),后面参数表示对应资源id,去掉则为系统应用默认id'0x7f' 31 | */ 32 | //去掉为了让demo不需要替换aapt就可以运行 33 | // aaptOptions.additionalParameters '--PLUG-resoure-proguard', '--PLUG-resoure-id', '0x7f' 34 | } 35 | 36 | dependencies { 37 | compile fileTree(include: ['*.jar'], dir: 'libs') 38 | compile project(':ZeusPlugin') 39 | } 40 | 41 | 42 | //start----------补丁相关,使用gradle插件,如果宿主是只运行于android 4.4及以上可以不添加这些代码------------------ 43 | //如果enable为true则表明打出的包会在每个类的构造函数中添加如下代码:if (Boolean.FALSE.booleanValue())System.out.println(Predicate.class); 44 | patchPlugin{ 45 | enable = true 46 | } 47 | //end------------补丁相关,使用gradle插件,如果宿主是只运行于android 4.4及以上可以不添加这些代码------------------ 48 | 49 | //start----------插件相关的,用来生成sdk-jar的------------------ 50 | //具体使用参见https://github.com/iReaderAndroid/buildJar 51 | BuildJar{ 52 | //输出目录 53 | outputFileDir= project.buildDir.path+"/jar" 54 | //输出原始jar包名 55 | outputFileName="sdk.jar" 56 | //输出混淆jar包名 57 | outputProguardFileName="sdk_proguard.jar" 58 | //混淆配置 59 | proguardConfigFile="proguard-rules.pro" 60 | //是否需要默认的混淆配置proguard-android.txt 61 | needDefaultProguard=true 62 | // applyMappingFile="originMapping/mapping.txt" 63 | //需要输出jar的包名列表,当此参数为空时,则默认全项目输出,支持多包,如 includePackage=['com/adison/testjarplugin/include','com/adison/testjarplugin/include1'...] 64 | // includePackage=['com/adison/testjarplugin/include'] 65 | // //不需要输出jar的jar包列表,如['baidu.jar','baidu1.jar'...] 66 | // excludeJar=[] 67 | // //不需要输出jar的类名列表,如['baidu.calss','baidu1.class'...] 68 | // excludeClass=['com/adison/testjarplugin/TestExcude.class'] 69 | // //不需要输出jar的包名列表,如 excludePackage=['com/adison/testjarplugin/exclude','com/adison/testjarplugin/exclude1'...] 70 | // excludePackage=['com/adison/testjarplugin/exclude'] 71 | } 72 | 73 | //end----------插件相关的,用来生成sdk-jar的------------------ -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/public-xml.gradle: -------------------------------------------------------------------------------- 1 | afterEvaluate { 2 | for (variant in android.applicationVariants) { 3 | def scope = variant.getVariantData().getScope() 4 | String mergeTaskName = scope.getMergeResourcesTask().name 5 | def mergeTask = tasks.getByName(mergeTaskName) 6 | 7 | mergeTask.doLast { 8 | copy { 9 | int i=0 10 | from(android.sourceSets.main.res.srcDirs) { 11 | include 'values/public.xml' 12 | rename 'public.xml', (i++ == 0? "public.xml": "public_${i}.xml") 13 | } 14 | 15 | into(mergeTask.outputDir) 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/assets/zeushotfix_test.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iReaderAndroid/ZeusPlugin/ce1008040165602ed9a9544571529af5b6b83d79/app/src/main/assets/zeushotfix_test.apk -------------------------------------------------------------------------------- /app/src/main/assets/zeusplugin_test.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iReaderAndroid/ZeusPlugin/ce1008040165602ed9a9544571529af5b6b83d79/app/src/main/assets/zeusplugin_test.apk -------------------------------------------------------------------------------- /app/src/main/assets/zeusplugin_test_version2.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iReaderAndroid/ZeusPlugin/ce1008040165602ed9a9544571529af5b6b83d79/app/src/main/assets/zeusplugin_test_version2.apk -------------------------------------------------------------------------------- /app/src/main/java/zeus/test/MainActivity.java: -------------------------------------------------------------------------------- 1 | package zeus.test; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.content.res.AssetManager; 6 | import android.os.Bundle; 7 | import android.view.View; 8 | import android.widget.Toast; 9 | 10 | import java.io.FileOutputStream; 11 | import java.io.InputStream; 12 | 13 | import zeus.plugin.PluginManager; 14 | import zeus.plugin.PluginUtil; 15 | import zeus.plugin.ZeusBaseActivity; 16 | import zeus.plugin.ZeusPlugin; 17 | import zeus.test.hotfix.TestHotfixActivity1; 18 | import zeus.test.plugin.TestPluginActivity; 19 | 20 | 21 | /** 22 | * Created by huangjian on 2016/6/21. 23 | */ 24 | public class MainActivity extends ZeusBaseActivity { 25 | 26 | @Override 27 | protected void onCreate(Bundle savedInstanceState) { 28 | super.onCreate(savedInstanceState); 29 | setContentView(R.layout.activity_main); 30 | } 31 | 32 | /** 33 | * 插件测试 34 | * 35 | * @param view 36 | */ 37 | public void testPlugin(View view) { 38 | Intent intent = new Intent(MainActivity.this, TestPluginActivity.class); 39 | startActivity(intent); 40 | } 41 | 42 | /** 43 | * 补丁测试 44 | * 45 | * @param view 46 | */ 47 | public void testHotfix(View view) { 48 | //第一次启动是宿主,应用补丁后就是补丁。 49 | Intent intent = new Intent(MainActivity.this, TestHotfixActivity1.class); 50 | startActivity(intent); 51 | } 52 | 53 | /** 54 | * 应用补丁 55 | * 56 | * @param view 57 | */ 58 | public void applyHotfix(View view) { 59 | if(PluginManager.isInstall(MyApplication.HOTFIX_TEST)){ 60 | Toast.makeText(this, "补丁"+MyApplication.HOTFIX_TEST+"已经被安装,不用再次安装", Toast.LENGTH_SHORT).show(); 61 | return; 62 | } 63 | ZeusPlugin zeusPlugin = PluginManager.getPlugin(MyApplication.HOTFIX_TEST); 64 | FileOutputStream out = null; 65 | InputStream in = null; 66 | try { 67 | AssetManager am = PluginManager.mBaseResources.getAssets(); 68 | in = am.open("zeushotfix_test.apk"); 69 | PluginUtil.createDirWithFile(PluginUtil.getZipPath(MyApplication.HOTFIX_TEST)); 70 | out = new FileOutputStream(PluginUtil.getZipPath(MyApplication.HOTFIX_TEST), false); 71 | byte[] temp = new byte[2048]; 72 | int len; 73 | while ((len = in.read(temp)) > 0) { 74 | out.write(temp, 0, len); 75 | } 76 | } catch (Exception e) { 77 | e.printStackTrace(); 78 | } finally { 79 | PluginUtil.close(in); 80 | PluginUtil.close(out); 81 | } 82 | boolean result= zeusPlugin.install(); 83 | if (result) { 84 | Toast.makeText(this, "补丁"+MyApplication.HOTFIX_TEST+"安装成功,下次启动生效", Toast.LENGTH_SHORT).show(); 85 | } 86 | } 87 | 88 | @Override 89 | protected void onDestroy() { 90 | super.onDestroy(); 91 | android.os.Process.killProcess(android.os.Process.myPid()); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/java/zeus/test/MyApplication.java: -------------------------------------------------------------------------------- 1 | package zeus.test; 2 | 3 | import android.app.Application; 4 | 5 | import java.util.HashMap; 6 | 7 | import zeus.plugin.PluginManager; 8 | import zeus.plugin.ZeusBaseApplication; 9 | 10 | /** 11 | * Created by huangjain on 2016/6/21. 12 | */ 13 | public class MyApplication extends ZeusBaseApplication { 14 | public static final String PLUGIN_TEST = "zeusplugin_test"; //插件测试demo 15 | public static final String HOTFIX_TEST = "zeushotfix_test"; //热修复补丁测试demo 16 | @Override 17 | public void onCreate() { 18 | super.onCreate(); 19 | HashMap defaultList = new HashMap<>(); 20 | /** 21 | * apk自带的插件的列表,每次添加内置插件的时候需要添加到这里,格式(pluginName,pluginVersion) 22 | * pluginVersion一定要与插件中PLUGINWEB_MAINIFEST_FILE文件里的version保持一致。 23 | * 对于插件还可以使用diff补丁的形式下载增量包,这样可以降低文件下载的大小。 24 | */ 25 | //补丁必须以EXP_PLUG_HOT_FIX_PREFIX开头 26 | //插件必须以PluginUtil.EXP_PLUG_PREFIX开头,否则不会识别为插件 27 | defaultList.put(PLUGIN_TEST, 1); 28 | PluginManager.init(this, defaultList); 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/zeus/test/StringConstant.java: -------------------------------------------------------------------------------- 1 | package zeus.test; 2 | 3 | /** 4 | * Created by huangjian on 2016/12/6. 5 | * 这里把所有宿主向插件暴露的资源id都列举,其值应与public.xml中的配置保持一致 6 | */ 7 | public class StringConstant { 8 | public static final int string1 = 0x7f050024; 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/zeus/test/hotfix/TestHotFix.java: -------------------------------------------------------------------------------- 1 | package zeus.test.hotfix; 2 | 3 | /** 4 | * Created by huangjian on 2016/8/22. 5 | */ 6 | public class TestHotFix { 7 | public String getTestString(){ 8 | return "这是宿主"; 9 | } 10 | 11 | public String getTestString2(){ 12 | return getTestString() + TestHotFixActivity.getString(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/zeus/test/hotfix/TestHotFixActivity.java: -------------------------------------------------------------------------------- 1 | package zeus.test.hotfix; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.Gravity; 6 | import android.widget.TextView; 7 | import android.widget.Toast; 8 | 9 | import zeus.plugin.ZeusBaseActivity; 10 | import zeus.test.MainActivity; 11 | import zeus.test.R; 12 | import zeus.test.hotfixTest.MyInterface; 13 | 14 | /** 15 | * 补丁测试页面 16 | * 17 | * @author adison 18 | * @date 16/8/21 19 | * @time 上午2:04 20 | */ 21 | public class TestHotFixActivity extends ZeusBaseActivity { 22 | 23 | private MyInterface test = new MyInterface() { 24 | @Override 25 | public String getString() { 26 | return MainActivity.class.getName(); 27 | } 28 | }; 29 | 30 | private MyInterface test1 = new MyInterface() { 31 | @Override 32 | public String getString() { 33 | return MainActivity.class.getName(); 34 | } 35 | }; 36 | 37 | private MyInterface test2 = new MyInterface() { 38 | @Override 39 | public String getString() { 40 | return MainActivity.class.getName(); 41 | } 42 | }; 43 | 44 | private MyInterface test3 = new MyInterface() { 45 | @Override 46 | public String getString() { 47 | return MainActivity.class.getName(); 48 | } 49 | }; 50 | 51 | private MyInterface test4 = new MyInterface() { 52 | @Override 53 | public String getString() { 54 | return MainActivity.class.getName(); 55 | } 56 | }; 57 | 58 | private MyInterface test5 = new MyInterface() { 59 | @Override 60 | public String getString() { 61 | return MainActivity.class.getName(); 62 | } 63 | }; 64 | 65 | private MyInterface test6 = new MyInterface() { 66 | @Override 67 | public String getString() { 68 | return MainActivity.class.getName(); 69 | } 70 | }; 71 | 72 | private MyInterface test7 = new MyInterface() { 73 | @Override 74 | public String getString() { 75 | return MainActivity.class.getName(); 76 | } 77 | }; 78 | 79 | private MyInterface test8 = new MyInterface() { 80 | @Override 81 | public String getString() { 82 | return MainActivity.class.getName(); 83 | } 84 | }; 85 | 86 | @Override 87 | protected void onCreate(Bundle savedInstanceState) { 88 | super.onCreate(savedInstanceState); 89 | setContentView(R.layout.activity_testhotfix); 90 | TextView textView = (TextView) findViewById(R.id.text_view); 91 | textView.setTextColor(getResources().getColor(android.R.color.black)); 92 | textView.setTextSize(18); 93 | textView.setGravity(Gravity.CENTER); 94 | textView.setText(new TestHotFix().getTestString()); 95 | setContentView(textView); 96 | setTitle(new TestHotFix().getTestString2()); 97 | Toast.makeText(this, test.getString(), Toast.LENGTH_LONG).show(); 98 | if(test1 == test2 || 99 | test3 == test4|| 100 | test5 == test6|| 101 | test7 == test8 102 | ){ 103 | int a = 0; 104 | int b =a; 105 | } 106 | } 107 | 108 | public static String getString(){ 109 | return "页面"; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/src/main/java/zeus/test/hotfix/TestHotfixActivity1.java: -------------------------------------------------------------------------------- 1 | package zeus.test.hotfix; 2 | 3 | /** 4 | * Created by jimor on 2017/3/15. 5 | */ 6 | public class TestHotfixActivity1 extends TestHotFixActivity{ 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/zeus/test/hotfixTest/MyInterface.java: -------------------------------------------------------------------------------- 1 | package zeus.test.hotfixTest; 2 | 3 | /** 4 | * Created by jimor on 2017/3/15. 5 | */ 6 | public interface MyInterface { 7 | 8 | String getString(); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/zeus/test/plugin/TestPluginActivity.java: -------------------------------------------------------------------------------- 1 | package zeus.test.plugin; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.content.res.AssetManager; 6 | import android.os.Bundle; 7 | import android.view.View; 8 | import android.widget.Toast; 9 | 10 | import java.io.FileOutputStream; 11 | import java.io.InputStream; 12 | 13 | import zeus.plugin.PluginManager; 14 | import zeus.plugin.PluginUtil; 15 | import zeus.plugin.ZeusBaseActivity; 16 | import zeus.plugin.ZeusPlugin; 17 | import zeus.test.MyApplication; 18 | import zeus.test.R; 19 | 20 | /** 21 | * 插件测试页面 22 | * 23 | * @author adison 24 | * @date 16/8/21 25 | * @time 上午1:09 26 | */ 27 | public class TestPluginActivity extends ZeusBaseActivity { 28 | @Override 29 | protected void onCreate(Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | setContentView(R.layout.activity_plugin); 32 | setTitle("插件测试"); 33 | findViewById(R.id.plugin_test).setOnClickListener(new View.OnClickListener() { 34 | @Override 35 | public void onClick(View v) { 36 | startPlugin(); 37 | } 38 | }); 39 | 40 | findViewById(R.id.plugin_install).setOnClickListener(new View.OnClickListener() { 41 | @Override 42 | public void onClick(View v) { 43 | installPlugin(); 44 | } 45 | }); 46 | } 47 | 48 | /** 49 | * 启动插件 50 | * 51 | */ 52 | public void startPlugin() { 53 | PluginManager.loadLastVersionPlugin(MyApplication.PLUGIN_TEST); 54 | try { 55 | Class cl = PluginManager.mNowClassLoader.loadClass(PluginManager.getPlugin(MyApplication.PLUGIN_TEST).getPluginMeta().mainClass); 56 | Intent intent = new Intent(this, cl); 57 | //这种方式为通过在宿主AndroidManifest.xml中预埋activity实现 58 | // startActivity(intent); 59 | //这种方式为通过欺骗android系统的activity存在性校验的方式实现 60 | PluginManager.startActivity(this,intent); 61 | } catch (ClassNotFoundException e) { 62 | e.printStackTrace(); 63 | } 64 | } 65 | 66 | /** 67 | * 安装assets中高版本插件plugin_test_version2.apk 68 | * 先拷贝到PluginUtil.getZipPath(PluginConfig.PLUGIN_TEST) 69 | * 然后调用install()安装。 70 | * 71 | */ 72 | public void installPlugin() { 73 | ZeusPlugin zeusPlugin = PluginManager.getPlugin(MyApplication.PLUGIN_TEST); 74 | FileOutputStream out = null; 75 | InputStream in = null; 76 | try { 77 | AssetManager am = PluginManager.mBaseResources.getAssets(); 78 | in = am.open("zeusplugin_test_version2.apk"); 79 | PluginUtil.createDirWithFile(PluginUtil.getZipPath(MyApplication.PLUGIN_TEST)); 80 | out = new FileOutputStream(PluginUtil.getZipPath(MyApplication.PLUGIN_TEST), false); 81 | byte[] temp = new byte[2048]; 82 | int len; 83 | while ((len = in.read(temp)) > 0) { 84 | out.write(temp, 0, len); 85 | } 86 | } catch (Exception e) { 87 | e.printStackTrace(); 88 | } finally { 89 | PluginUtil.close(in); 90 | PluginUtil.close(out); 91 | } 92 | 93 | boolean installed=zeusPlugin.install(); 94 | if(installed){ 95 | Toast.makeText(PluginManager.mBaseContext,"高版本插件安装成功",Toast.LENGTH_SHORT).show(); 96 | } 97 | } 98 | 99 | 100 | } 101 | -------------------------------------------------------------------------------- /app/src/main/java/zeus/test/plugin/TestView.java: -------------------------------------------------------------------------------- 1 | package zeus.test.plugin; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.widget.TextView; 6 | 7 | import zeus.test.R; 8 | 9 | /** 10 | * Created by huangjian on 2016/12/6. 11 | * 这是用来测试把宿主中的控件作为公共组件提供给插件 12 | */ 13 | public class TestView extends TextView{ 14 | public TestView(Context context) { 15 | super(context); 16 | setBackgroundResource(R.color.test_view_background); 17 | } 18 | 19 | public TestView(Context context, AttributeSet attrs) { 20 | super(context, attrs); 21 | } 22 | 23 | public TestView(Context context, AttributeSet attrs, int defStyleAttr) { 24 | super(context, attrs, defStyleAttr); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iReaderAndroid/ZeusPlugin/ce1008040165602ed9a9544571529af5b6b83d79/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 |