Android 360开源全面插件化框架RePlugin 实战
来源:互联网 发布:tensorflow最新版本 编辑:程序博客网 时间:2024/06/06 12:31
1 RePlugin 介绍
RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案,由360手机卫士的RePlugin Team研发,也是业内首个提出”全面插件化“(全面特性、全面兼容、全面使用)的方案。
其主要优势有:
极其灵活:主程序无需升级(无需在Manifest中预埋组件),即可支持新增的四大组件,甚至全新的插件
**非常稳定:**Hook点仅有一处(ClassLoader),无任何Binder Hook!如此可做到其崩溃率仅为“万分之一”,并完美兼容市面上近乎所有的Android ROM
特性丰富:支持近乎所有在“单品”开发时的特性。包括静态Receiver、Task-Affinity坑位、自定义Theme、进程坑位、AppCompat、DataBinding等
易于集成:无论插件还是主程序,只需“数行”就能完成接入
管理成熟:拥有成熟稳定的“插件管理方案”,支持插件安装、升级、卸载、版本管理,甚至包括进程通讯、协议版本、安全校验等
数亿支撑:有360手机卫士庞大的数亿用户做支撑,三年多的残酷验证,确保App用到的方案是最稳定、最适合使用的
以上是官方的介绍,github地址如下:
https://github.com/Qihoo360/RePlugin
总之,根据网上的反馈以及14日成都参加thoughtworks 的replugin分享活动来看,Replugin将会是一个比较有潜力和完善的差价化框架。今天就来根据官方的wiki自己来接入到程序中。
Replugin的接入个人觉得分为三部分:
宿主的接入:这个主要是决定你要将那么module作为你的宿主,用于加载插件
插架的接入:这里可以新建一个module作为插件,也可以将现有的module(APK)改造为插件
插架化的使用:主要讲宿主和插件之间组件的通信,以及宿主和插件的调用等。
本篇博文不讲插件化的原理,只讲最基础的如何接入Replugin。想要了解原理的可以阅读官方的源代码及参考以下的几篇博文:
Replugin 全面解析 (1) http://www.jianshu.com/p/5994c2db1557
Replugin 全面解析 (2) http://www.jianshu.com/p/74a70dd6adc9
Replugin 全面解析 (3) http://www.jianshu.com/p/8465585b3507
Replugin 全面解析 (4) http://www.jianshu.com/p/f456f608aa92
Replugin 全面解析 (5) http://www.jianshu.com/p/fb9d40f4173c
2 宿主接入步骤
根据官方的文档,如下宿主的接入分为三个步骤:
https://github.com/Qihoo360/RePlugin/wiki/%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8B
第 1 步:添加 RePlugin Host Gradle 依赖
这一步没什么好说的。
在项目根目录的 build.gradle(注意:不是 app/build.gradle) 中添加 replugin-host-gradle 依赖:
buildscript { dependencies { classpath 'com.qihoo360.replugin:replugin-host-gradle:2.2.1' ... }}
第 2 步:添加 RePlugin Host Library依赖
在 app/build.gradle 中应用 replugin-host-gradle 插件,并添加 replugin-host-lib 依赖:
// ATTENTION!!! Must be PLACED AFTER "android{}" to read the applicationIdapply plugin: 'replugin-host-gradle'/** * 配置项均为可选配置,默认无需添加 * 更多可选配置项参见replugin-host-gradle的RepluginConfig类 * 可更改配置项参见 自动生成RePluginHostConfig.java */repluginHostConfig { /** * 是否使用 AppCompat 库 * 不需要个性化配置时,无需添加 */ useAppCompat = true /** * 背景不透明的坑的数量 * 不需要个性化配置时,无需添加 */ countNotTranslucentStandard = 6 countNotTranslucentSingleTop = 2 countNotTranslucentSingleTask = 3 countNotTranslucentSingleInstance = 2}dependencies { compile 'com.qihoo360.replugin:replugin-host-lib:2.2.1' ...}
需要注意的是apply plugin: ‘replugin-host-gradle’ 这一行需要放在 android{}之后。
另外一个需要注意版本的问题,目前最新的版本是2.2.1 如下,要不然下载不下来。
然后点击gradle同步,同步完成之后就可以进行下一步了
第 3 步:配置 Application 类
根据官方文档的描述可以继承RePluginApplication,也可以采用非继承式来初始化,由于我一般不太愿意采用继承式,所以就采用非继承式吧。这里我的配置如下:
/** * Email: 1273482124@qq.com * Created by qiyei2015 on 2017/5/8. * Version: 1.0 * Description: */public class BaseApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); RePlugin.App.attachBaseContext(this); } @Override public void onCreate() { super.onCreate(); RePlugin.App.onCreate(); try { SDKManager.initSDK(this); } catch (Exception e) { e.printStackTrace(); } //初始化皮肤管理器 SkinManager.getInstance().init(this); } @Override public void onLowMemory() { super.onLowMemory(); /* Not need to be called if your application's minSdkVersion > = 14 */ RePlugin.App.onLowMemory(); } @Override public void onTrimMemory(int level) { super.onTrimMemory(level); /* Not need to be called if your application's minSdkVersion > = 14 */ RePlugin.App.onTrimMemory(level); } @Override public void onConfigurationChanged(Configuration config) { super.onConfigurationChanged(config); /* Not need to be called if your application's minSdkVersion > = 14 */ RePlugin.App.onConfigurationChanged(config); }}
多余的代码暂时不用管,这里注意事项就参照官方的来吧。
另外,如果你的宿主配置过程中有什么错误,建议先下载官方的demo进行运行,然后参考官方的demo进行配置。
3 插件接入步骤
插件部分的接入也可以按照官方的教程来。插件的接入其实很简单,不管是新开发的还是将现有的项目改造成插件化。都一般有以下几步
第 1 步:添加 RePlugin Plugin Gradle 依赖
在项目根目录的 build.gradle(注意:不是 app/build.gradle) 中添加 replugin-plugin-gradle 依赖:
buildscript { dependencies { classpath 'com.qihoo360.replugin:replugin-plugin-gradle:2.2.1' ... }}
这里我想说以下的是,如果是单独的一个工程肯定是没问题的,但是大家看我的工程目录:
EssayJokeApp 与appdemo都是Application类型的module,因此我刚刚在主工程已经添加了RePlugin Host Gradle的依赖,这里我就直接在该工程下添加RePlugin Plugin Gradle 即可。
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.3.2' classpath 'com.qihoo360.replugin:replugin-host-gradle:2.2.1' classpath 'com.qihoo360.replugin:replugin-plugin-gradle:2.2.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files }}
第 2 步:添加 RePlugin Plugin Library 依赖
在 app/build.gradle 中应用 replugin-plugin-gradle 插件,并添加 replugin-plugin-lib 依赖:
apply plugin: 'replugin-plugin-gradle'dependencies { compile 'com.qihoo360.replugin:replugin-plugin-lib:2.2.1' ...}
由于这里我想将appdemo module作为一个插架,因此配置如下:
apply plugin: 'com.android.application'android { compileSdkVersion 25 buildToolsVersion "26.0.0" defaultConfig { applicationId "com.qiyei.appdemo" minSdkVersion 16 targetSdkVersion 25 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } sourceSets { main { jni.srcDirs = ['src/main/jni', 'src/main/cpp/'] jniLibs.srcDirs = ['libs'] //配置生成jniLibs } } aaptOptions { cruncherEnabled = false useNewCruncher = false } repositories { // 本地的libs目录 flatDir { dirs 'libs' //aar目录 } } //解决多语言未翻译问题 lintOptions { checkReleaseBuilds false abortOnError false }}// 这个plugin需要放在android配置之后,因为需要读取android中的配置项apply plugin: 'replugin-plugin-gradle'/*** * plugin配置 */repluginPluginConfig { pluginName = "appdemo" hostApplicationId = "com.qiyei.essayjoke" hostAppLauncherActivity = "com.qiyei.essayjoke.activity.WelcomeActivity"}dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') compile(name: 'hotfix_core-release-3.1.0', ext: 'aar') androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:25.3.1' compile 'com.qihoo360.replugin:replugin-plugin-lib:2.2.1' testCompile 'junit:junit:4.12' compile project(':Framework') compile files('libs/alicloud-android-utils-1.0.3.jar') compile files('libs/utdid4all-1.1.5.3_proguard.jar')}
其中repluginPluginConfig是我参考官方demo加上去的。
官方到这里就完了,但是我强烈建议你把下面一步也做了。
第3步 配置插件的别名
在插件的AndroidManifest.xml文件中配置插件的别名,方便后面的使用
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.qiyei.appdemo"> ...... <application android:name=".BaseApplication" android:allowBackup="true" android:icon="@mipmap/icon" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <!--声明其插件别名--> <meta-data android:name="com.qihoo360.plugin.name" android:value="appdemo" /> ...... </application></manifest>
android:value字段的值就是插件的别名。
这样,插件部分的接入基本完成了。下面有一步我觉得可以做一下。
4 测试插件的正确性
将接入后的插架编译成单独的apk,测试其功能是否正常,如果不正常则需要排查原因,做这一步是表明插件在安装前是正常的,防止因为插件本身的问题导致的错误。
4 使用插架
宿主接入了,插件也接入了,并且单独运行也没问题了,接下来我就看如何使用了。关于插架的使用可以参考官方文档。不过我这里要介绍一下我的一些简单使用。
1 插件的安装
Replugin的插件分为内部插件,外部插件。简单的说在编译前放入assets\plugins 下的xxx.jar就是内部插件,其他在运行期从网上下载的或者放入到其他位置,例如SD卡上的xxx.apk都是外部插件。内部插件与外部插件有以下几大不同
1 内部插件是xxx.jar(编译出apk以后,直接将文件改成xxx.jar)目录是assets\plugins,外置插件就是xxx.apk形式(内置插件进行升级之后也变为外置插件了)
2 内部插件不需要安装,直接调用。外部插件需要安装
3 内部插件默认的别名是 文件名 ,外部插件如果没有在manifest.xml中声明就是默认的包名
所以这也是为什么我刚刚让manifest.xml进行声明的原因了。
因此,对于内部插件,直接在编译前放入assets\plugins即可。后续可直接使用
对于外部插件,使用前需要调用RePlugin.install(fileName);进行显式的安装,安装代码如下:
String pluginApk = "appdemo.apk"; String fileName = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + pluginApk; File pluginFile = new File(fileName); //文件不存在就返回 if (!pluginFile.exists()){ return; } PluginInfo info = null; if (pluginFile.exists()) { info = RePlugin.install(fileName); }
我这里是直接将apk放在了sd卡根目录的,也可以从网上下载下来进行安装的。
2 宿主调用插件的组件
这里以启动一个插件的Activity为例进行介绍。
先来看我的插件中声明的Activity
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.qiyei.appdemo"> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.MOUNT_FORMAT_FILESYSTEMS"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/> <uses-permission android:name="android.permission.VIBRATE"/> <application android:name=".BaseApplication" android:allowBackup="true" android:icon="@mipmap/icon" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <!--声明其插件别名--> <meta-data android:name="com.qihoo360.plugin.name" android:value="appdemo" /> <meta-data android:name="com.taobao.android.hotfix.IDSECRET" android:value="Yours" /> <meta-data android:name="com.taobao.android.hotfix.APPSECRET" android:value="Yours" /> <meta-data android:name="com.taobao.android.hotfix.RSASECRET" android:value="Yours" /> <activity android:name=".activity.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".activity.ColorTrackTextViewActivity"> </activity> <activity android:name=".activity.ViewPagerTestActivity"> </activity> <activity android:name=".activity.RecyclerViewTestActivity"> </activity> <activity android:name=".activity.EasyJokeMainActivity"> </activity> <activity android:name=".activity.SkinTestActivity"> </activity> <activity android:name=".activity.BannerTestActivity"> </activity> <activity android:name=".activity.ImageSelectedTestActivity"> </activity> <activity android:name=".activity.TestActivity"> </activity> <service android:name=".service.LocalService" android:enabled="true" android:exported="false"> </service> <service android:name=".service.RemoteService" android:enabled="true" android:exported="false" android:process=":remote"> </service> <service android:name=".service.JobWakeUpService" android:enabled="true" android:exported="false" android:permission="android.permission.BIND_JOB_SERVICE"> </service> <service android:name=".service.TestService" android:enabled="true" android:exported="false"> </service> <activity android:name=".activity.DataCenterTestActivity"> </activity> <activity android:name=".activity.BinderTestActivity"> </activity> </application></manifest>
这里我在宿主中去调用插件中的com.qiyei.appdemo.activity.MainActivity。代码如下:
/** * Email: 1273482124@qq.com * Created by qiyei2015 on 2017/6/24. * Version: 1.0 * Description: app的主界面 */public class HomeActivity extends BaseSkinActivity { @ViewById(R.id.main_tab_layout) private FrameLayout mMainTabLayout; @ViewById(R.id.tab_list) private RadioGroup mTabList; @ViewById(R.id.home_rb) private RadioButton mHomeButton; @ViewById(R.id.find_rb) private RadioButton mFindButton; @ViewById(R.id.new_rb) private RadioButton mNewButton; @ViewById(R.id.message_rb) private RadioButton mMessageButton; private HomeFragment mHomeFragment; private FindFragment mFindFragment; private NewFragment mNewFragment; private MessageFragment mMessageFragment; private FragmentHelper mFragmentHelper; private CommonTitleBar navigationBar; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); initData(); initView(); } @Override protected void initContentView() { setContentView(R.layout.activity_home); } @Override protected void initData() { mContext = this; mFragmentHelper = new FragmentHelper(getSupportFragmentManager(),R.id.main_tab_layout); if (!RePlugin.isPluginInstalled("appdemo")){ String pluginApk = "appdemo.apk"; String fileName = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + pluginApk; File pluginFile = new File(fileName); //文件不存在就返回 if (!pluginFile.exists()){ return; } PluginInfo info = null; if (pluginFile.exists()) { info = RePlugin.install(fileName); } if (info != null){ //预先加载 RePlugin.preload(info); ToastUtil.showLongToast("安装 appdemo 成功 " + info.getName()); } } } @Override protected void initView() { navigationBar = new CommonTitleBar.Builder(this) .setTitle(getString(R.string.home_page)) .setRightText(getString(R.string.test)) .setRightClickListener(new View.OnClickListener() { @Override public void onClick(View v) { RePlugin.startActivity(mContext, RePlugin.createIntent("appdemo", "com.qiyei.appdemo.activity.MainActivity")); } }) .build(); SystemStatusBarUtil.statusBarTintColor(this,getResources().getColor(R.color.title_bar_bg_day)); mHomeButton.setOnClickListener(this); mFindButton.setOnClickListener(this); mNewButton.setOnClickListener(this); mMessageButton.setOnClickListener(this); mHomeFragment = new HomeFragment(); mFragmentHelper.add(mHomeFragment); mHomeButton.setChecked(true); } @Override public void onClick(View v) { switch (v.getId()){ case R.id.home_rb: if (mHomeFragment == null){ mHomeFragment = new HomeFragment(); } navigationBar.setTitle(getString(R.string.home_page)); mFragmentHelper.switchFragment(mHomeFragment); break; case R.id.find_rb: if (mFindFragment == null){ mFindFragment = new FindFragment(); } navigationBar.setTitle(getString(R.string.find)); mFragmentHelper.switchFragment(mFindFragment); break; case R.id.new_rb: if (mNewFragment == null){ mNewFragment = new NewFragment(); } navigationBar.setTitle(getString(R.string.fresh)); mFragmentHelper.switchFragment(mNewFragment); break; case R.id.message_rb: if (mMessageFragment == null){ mMessageFragment = new MessageFragment(); } navigationBar.setTitle(getString(R.string.message)); mFragmentHelper.switchFragment(mMessageFragment); break; default: break; } } /** * 从assets目录中复制某文件内容 * @param fileName assets目录下的Apk源文件路径 * @param newFileName 复制到/data/data/package_name/files/目录下文件名 */ private void copyAssetsFileToAppFiles(String fileName, String newFileName) { InputStream is = null; FileOutputStream fos = null; int buffsize = 1024; try { is = new FileInputStream(fileName); fos = this.openFileOutput(newFileName, Context.MODE_PRIVATE); int byteCount = 0; byte[] buffer = new byte[buffsize]; while((byteCount = is.read(buffer)) != -1) { fos.write(buffer, 0, byteCount); } fos.flush(); } catch (Exception e) { e.printStackTrace(); } finally { try { is.close(); fos.close(); } catch (Exception e) { e.printStackTrace(); } } }}
这里我做了插件的预加载,提供了首次的运行速度。另外其他代码忽略,只看下面即可。
RePlugin.startActivity(mContext, RePlugin.createIntent("appdemo", "com.qiyei.appdemo.activity.MainActivity"));
这里的appdemo就是我指定的别名,要是是外置插件,不指定别名,就只能去按照包名“com.qiyei.appdemo”来调用了。
实测效果如下:
录制的效果不太好,有白屏的现象出现,真实的情况是没有的。另外我的机器是nexus 6p 系统是Android8.0 目测支持的还不错,没有什么大问题。
由于我的插件中集成了阿里的Sophix热修复框架,可能有一些bug,不过暂时不用管。
综合来看,Replugin还是挺有意思的,继续深入理解原理吧。
参考代码:github https://github.com/qiyei2015/EssayJoke dev分支
- Android 360开源全面插件化框架RePlugin 实战
- android插件化框架-Replugin
- Android全面插件化方案-RePlugin踩坑
- 360开源的插件化框架Replugin深度剖析
- 360开源的插件化框架Replugin深度剖析
- Android 插件化框架 RePlugin 使用心得
- Android插件化开发—RePlugin插件化框架
- 插件化系列开发之九--Android 全面插件化 RePlugin 流程与源码解析
- 360家的又一个插件化框架---RePlugin
- Replugin插件化框架简要分析
- Android之插件化框架RePlugin——献给Android世界的“最好礼物”
- 插件化框架 RePlugin 应用之一:配置及内置插件
- 插件化框架 RePlugin 应用之三:外置插件
- Android 插件化原理 完胜360插件框架 技术实战
- Android 插件化 RePlugin 入坑记录一
- Android 插件化 RePlugin 入坑记录二
- 插件化-360的DroidPlugin与RePlugin研究及集成
- 360插件化Replugin爬坑之路
- java学习笔记(1)-基础知识记录
- 深度学习在推断阶段的硬件实现方法概述
- Python 多进程默认不能共享全局变量
- 数据结构与算法-排序:选择排序
- 摄像头测距原理与代码实现
- Android 360开源全面插件化框架RePlugin 实战
- Hibernate单表操作
- 【bzoj 1600】建造栅栏(DP)
- “跳出”的几个兄弟。
- 十月十四号总结
- Android 学习笔记(二):引导界面
- hdu-5980-Find Small A
- Java的三种代理模式
- DeepLearing学习笔记-改善深层神经网络(第一周作业-2-正则化)