Activity插件化解决方案
--摘自《android插件化开发指南》
1.宿主App加载插件中的类
2.最简单的插件化方案就是在宿主的androidmanifest.xml中申明插件中的四大组件
把插件dex合并到宿主dex中,那么宿主app对应的classloader就可以加载插件中的任意类
/**
* 由于应用程序使用的ClassLoader为PathClassLoader
* 最终继承自 BaseDexClassLoader
* 查看源码得知,这个BaseDexClassLoader加载代码根据一个叫做
* dexElements的数组进行, 因此我们把包含代码的dex文件插入这个数组
* 系统的classLoader就能帮助我们找到这个类
*
* 这个类用来进行对于BaseDexClassLoader的Hook
* 类名太长, 不要吐槽.
* @author weishu
* @date 16/3/28
*/
public final class BaseDexClassLoaderHookHelper { public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
// 获取 BaseDexClassLoader : pathList
Object pathListObj = RefInvoke.getFieldObject(DexClassLoader.class.getSuperclass(), cl, "pathList"); // 获取 PathList: Element[] dexElements
Object[] dexElements = (Object[]) RefInvoke.getFieldObject(pathListObj, "dexElements"); // Element 类型
Class<?> elementClass = dexElements.getClass().getComponentType(); // 创建一个数组, 用来替换原始的数组
Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1); // 构造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 这个构造函数
Class[] p1 = {File.class, boolean.class, File.class, DexFile.class};
Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};
Object o = RefInvoke.createObject(elementClass, p1, v1); Object[] toAddElementArray = new Object[] { o };
// 把原始的elements复制进去
System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
// 插件的那个element复制进去
System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length); // 替换
RefInvoke.setFieldObject(pathListObj, "dexElements", newElements);
}
}
加载插件中的资源
private static void reloadInstalledPluginResources(ArrayList<String> pluginPaths) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, mBaseContext.getPackageResourcePath()); for(String pluginPath: pluginPaths) {
addAssetPath.invoke(assetManager, pluginPath);
} Resources newResources = new Resources(assetManager,
mBaseContext.getResources().getDisplayMetrics(),
mBaseContext.getResources().getConfiguration()); RefInvoke.setFieldObject(mBaseContext, "mResources", newResources);
//这是最主要的需要替换的,如果不支持插件运行时更新,只留这一个就可以了
RefInvoke.setFieldObject(mPackageInfo, "mResources", newResources); mNowResources = newResources;
//需要清理mTheme对象,否则通过inflate方式加载资源会报错
//如果是activity动态加载插件,则需要把activity的mTheme对象也设置为null
RefInvoke.setFieldObject(mBaseContext, "mTheme", null);
} catch (Throwable e) {
e.printStackTrace();
}
}
public class TestActivity1 extends ZeusBaseActivity {
private final static String TAG = "TestActivity1"; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test1); findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
Intent intent = new Intent(); String activityName = "jianqiang.com.hostapp.ActivityA";
intent.setComponent(new ComponentName("jianqiang.com.hostapp", activityName)); startActivity(intent); } catch (Exception e) {
e.printStackTrace();
}
}
});
}
}
以上方案有2个缺点,就是新增的Activity无法在宿主AndroidManifest文件中预先占位了;并且插件和宿主的资源都合并到了一起,资源id会有冲突
启动没有在AndroidManifest中声明的插件Activity(采用欺上瞒下的做法)
class MockClass1 implements InvocationHandler { private static final String TAG = "MockClass1"; Object mBase; public MockClass1(Object base) {
mBase = base;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Log.e("bao", method.getName()); if ("startActivity".equals(method.getName())) {
// 只拦截这个方法
// 替换参数, 任你所为;甚至替换原始Activity启动别的Activity偷梁换柱 // 找到参数里面的第一个Intent 对象
Intent raw;
int index = 0; for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
raw = (Intent) args[index]; Intent newIntent = new Intent(); // 替身Activity的包名, 也就是我们自己的包名
String stubPackage = "jianqiang.com.activityhook1"; // 这里我们把启动的Activity临时替换为 StubActivity
ComponentName componentName = new ComponentName(stubPackage, StubActivity.class.getName());
newIntent.setComponent(componentName); // 把我们原始要启动的TargetActivity先存起来
newIntent.putExtra(AMSHookHelper.EXTRA_TARGET_INTENT, raw); // 替换掉Intent, 达到欺骗AMS的目的
args[index] = newIntent; Log.d(TAG, "hook success");
return method.invoke(mBase, args);
} return method.invoke(mBase, args);
}
}
class MockClass2 implements Handler.Callback { Handler mBase; public MockClass2(Handler base) {
mBase = base;
} @Override
public boolean handleMessage(Message msg) { switch (msg.what) {
// ActivityThread里面 "LAUNCH_ACTIVITY" 这个字段的值是100
// 本来使用反射的方式获取最好, 这里为了简便直接使用硬编码
case 100:
handleLaunchActivity(msg);
break;
} mBase.handleMessage(msg);
return true;
} private void handleLaunchActivity(Message msg) {
// 这里简单起见,直接取出TargetActivity; Object obj = msg.obj; // 把替身恢复成真身
Intent raw = (Intent) RefInvoke.getFieldObject(obj, "intent"); Intent target = raw.getParcelableExtra(AMSHookHelper.EXTRA_TARGET_INTENT);
raw.setComponent(target.getComponent()); //修改packageName,这样缓存才能命中
ActivityInfo activityInfo = (ActivityInfo) RefInvoke.getFieldObject(obj, "activityInfo");
activityInfo.applicationInfo.packageName = target.getPackage() == null ?
target.getComponent().getPackageName() : target.getPackage(); try {
hookPackageManager();
} catch (Exception e) {
e.printStackTrace();
}
} private static void hookPackageManager() throws Exception { // 这一步是因为 initializeJavaContextClassLoader 这个方法内部无意中检查了这个包是否在系统安装
// 如果没有安装, 直接抛出异常, 这里需要临时Hook掉 PMS, 绕过这个检查.
Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread"); // 获取ActivityThread里面原始的 sPackageManager
Object sPackageManager = RefInvoke.getFieldObject(currentActivityThread, "sPackageManager"); // 准备好代理对象, 用来替换原始的对象
Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),
new Class<?>[] { iPackageManagerInterface },
new MockClass3(sPackageManager)); // 1. 替换掉ActivityThread里面的 sPackageManager 字段
RefInvoke.setFieldObject(currentActivityThread, "sPackageManager", proxy);
}
}
public class AMSHookHelper { public static final String EXTRA_TARGET_INTENT = "extra_target_intent"; /**
* Hook AMS
* 主要完成的操作是 "把真正要启动的Activity临时替换为在AndroidManifest.xml中声明的替身Activity",进而骗过AMS
*/
public static void hookAMN() throws ClassNotFoundException,
NoSuchMethodException, InvocationTargetException,
IllegalAccessException, NoSuchFieldException { //获取AMN的gDefault单例gDefault,gDefault是final静态的
Object gDefault = RefInvoke.getStaticFieldObject("android.app.ActivityManagerNative", "gDefault"); // gDefault是一个 android.util.Singleton<T>对象; 我们取出这个单例里面的mInstance字段
Object mInstance = RefInvoke.getFieldObject("android.util.Singleton", gDefault, "mInstance"); // 创建一个这个对象的代理对象MockClass1, 然后替换这个字段, 让我们的代理对象帮忙干活
Class<?> classB2Interface = Class.forName("android.app.IActivityManager");
Object proxy = Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class<?>[] { classB2Interface },
new MockClass1(mInstance)); //把gDefault的mInstance字段,修改为proxy
RefInvoke.setFieldObject("android.util.Singleton", gDefault, "mInstance", proxy);
} /**
* 由于之前我们用替身欺骗了AMS; 现在我们要换回我们真正需要启动的Activity
* 不然就真的启动替身了, 狸猫换太子...
* 到最终要启动Activity的时候,会交给ActivityThread 的一个内部类叫做 H 来完成
* H 会完成这个消息转发; 最终调用它的callback
*/
public static void hookActivityThread() throws Exception { // 先获取到当前的ActivityThread对象
Object currentActivityThread = RefInvoke.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread"); // 由于ActivityThread一个进程只有一个,我们获取这个对象的mH
Handler mH = (Handler) RefInvoke.getFieldObject(currentActivityThread, "mH"); //把Handler的mCallback字段,替换为new MockClass2(mH)
RefInvoke.setFieldObject(Handler.class,
mH, "mCallback", new MockClass2(mH));
}
}
动态替换Activity的插件化方案
1)为插件创建一个LoadedApk对象,并把它事先放到mPackages缓存中。这样getPackageInfo方法就会直接返回这个插件的LoadedApk对象,也就是永远命中缓存,永远不会走下面创建LoadedApk对象的逻辑
2)反射得到插件的loadedApk对象的mClassLoader字段,设置为插件的ClassLoader。
public class LoadedApkClassLoaderHookHelper { public static Map<String, Object> sLoadedApk = new HashMap<String, Object>(); public static void hookLoadedApkInActivityThread(File apkFile) throws ClassNotFoundException,
NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException, InstantiationException { // 先获取到当前的ActivityThread对象
Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread"); // 获取到 mPackages 这个静态成员变量, 这里缓存了dex包的信息
Map mPackages = (Map) RefInvoke.getFieldObject(currentActivityThread, "mPackages"); //准备两个参数
// android.content.res.CompatibilityInfo
Object defaultCompatibilityInfo = RefInvoke.getStaticFieldObject("android.content.res.CompatibilityInfo", "DEFAULT_COMPATIBILITY_INFO");
//从apk中取得ApplicationInfo信息
ApplicationInfo applicationInfo = generateApplicationInfo(apkFile); //调用ActivityThread的getPackageInfoNoCheck方法loadedApk,得到,上面两个数据都是用来做参数的
Class[] p1 = {ApplicationInfo.class, Class.forName("android.content.res.CompatibilityInfo")};
Object[] v1 = {applicationInfo, defaultCompatibilityInfo};
Object loadedApk = RefInvoke.invokeInstanceMethod(currentActivityThread, "getPackageInfoNoCheck", p1, v1); //为插件造一个新的ClassLoader
String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath();
String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath();
ClassLoader classLoader = new CustomClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());
RefInvoke.setFieldObject(loadedApk, "mClassLoader", classLoader); //把插件LoadedApk对象放入缓存
WeakReference weakReference = new WeakReference(loadedApk);
mPackages.put(applicationInfo.packageName, weakReference); // 由于是弱引用, 因此我们必须在某个地方存一份, 不然容易被GC; 那么就前功尽弃了.
sLoadedApk.put(applicationInfo.packageName, loadedApk);
} /**
* 这个方法的最终目的是调用
* android.content.pm.PackageParser#generateActivityInfo(android.content.pm.PackageParser.Activity, int, android.content.pm.PackageUserState, int)
*/
public static ApplicationInfo generateApplicationInfo(File apkFile)
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException { // 找出需要反射的核心类: android.content.pm.PackageParser
Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
Class<?> packageParser$PackageClass = Class.forName("android.content.pm.PackageParser$Package");
Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState"); // 我们的终极目标: android.content.pm.PackageParser#generateApplicationInfo(android.content.pm.PackageParser.Package,
// int, android.content.pm.PackageUserState)
// 要调用这个方法, 需要做很多准备工作; 考验反射技术的时候到了 - -!
// 下面, 我们开始这场Hack之旅吧! // 首先拿到我们得终极目标: generateApplicationInfo方法
// API 23 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// public static ApplicationInfo generateApplicationInfo(Package p, int flags,
// PackageUserState state) {
// 其他Android版本不保证也是如此. // 首先, 我们得创建出一个Package对象出来供这个方法调用
// 而这个需要得对象可以通过 android.content.pm.PackageParser#parsePackage 这个方法返回得 Package对象得字段获取得到
// 创建出一个PackageParser对象供使用
Object packageParser = packageParserClass.newInstance(); // 调用 PackageParser.parsePackage 解析apk的信息
// 实际上是一个 android.content.pm.PackageParser.Package 对象
Class[] p1 = {File.class, int.class};
Object[] v1 = {apkFile, 0};
Object packageObj = RefInvoke.invokeInstanceMethod(packageParser, "parsePackage", p1, v1); // 第三个参数 mDefaultPackageUserState 我们直接使用默认构造函数构造一个出来即可
Object defaultPackageUserState = packageUserStateClass.newInstance(); // 万事具备!!!!!!!!!!!!!!
Class[] p2 = {packageParser$PackageClass, int.class, packageUserStateClass};
Object[] v2 = {packageObj, 0, defaultPackageUserState};
ApplicationInfo applicationInfo = (ApplicationInfo)RefInvoke.invokeInstanceMethod(packageParser, "generateApplicationInfo", p2, v2); String apkPath = apkFile.getPath();
applicationInfo.sourceDir = apkPath;
applicationInfo.publicSourceDir = apkPath; return applicationInfo;
}
}
上述代码的思想是,反射PackageParser的generateApplicationInfo方法,硬生生地创建出一个ApplicationInfo对象
加载插件中类的方案1:为每个插件创建一个ClassLoader
public class CustomClassLoader extends DexClassLoader { public CustomClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, libraryPath, parent);
}
}
总结就是把插件apk对应的LoadedApk对象,直接放入了缓存里,然后把这个LoadedApk对象的ClassLoader改为插件的ClassLoader
加载插件中类的方案2:合并多个dex
1)根据宿主的ClassLoader,获取宿主的dexElements字段
首先反射出BaseDexClassLoader的pathList字段,它是DexPathList类型的
然后反射出DexPathList的dexElements字段,这是个数组
2)根据插件的apkFile,反射出一个Element类型的对象,这就是插件dex
3)把插件dex和宿主dexElements合并成一个新的dex数组,替换宿主之前的dexElements字段
public final class BaseDexClassLoaderHookHelper { public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
// 获取 BaseDexClassLoader : pathList
Object pathListObj = RefInvoke.getFieldObject(DexClassLoader.class.getSuperclass(), cl, "pathList"); // 获取 PathList: Element[] dexElements
Object[] dexElements = (Object[]) RefInvoke.getFieldObject(pathListObj, "dexElements"); // Element 类型
Class<?> elementClass = dexElements.getClass().getComponentType(); // 创建一个数组, 用来替换原始的数组
Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1); // 构造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 这个构造函数
Class[] p1 = {File.class, boolean.class, File.class, DexFile.class};
Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};
Object o = RefInvoke.createObject(elementClass, p1, v1); Object[] toAddElementArray = new Object[] { o };
// 把原始的elements复制进去
System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
// 插件的那个element复制进去
System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length); // 替换
RefInvoke.setFieldObject(pathListObj, "dexElements", newElements);
}
}
解决插件化Activity启动模式的问题,使用的是占位Activity的思想,即事先为这3中启动模式创建很多的StubActivity,占位activity是什么启动模式,对应的插件activity就是什么启动模式
加载插件中类的方案3:修改app原生的ClassLoader
class ZeusClassLoader extends PathClassLoader {
private List<DexClassLoader> mClassLoaderList = null; public ZeusClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, parent); mClassLoaderList = new ArrayList<DexClassLoader>();
} /**
* 添加一个插件到当前的classLoader中
*/
protected void addPluginClassLoader(DexClassLoader dexClassLoader) {
mClassLoaderList.add(dexClassLoader);
} @Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = null;
try {
//先查找parent classLoader,这里实际就是系统帮我们创建的classLoader,目标对应为宿主apk
clazz = getParent().loadClass(className);
} catch (ClassNotFoundException ignored) { } if (clazz != null) {
return clazz;
} //挨个的到插件里进行查找
if (mClassLoaderList != null) {
for (DexClassLoader classLoader : mClassLoaderList) {
if (classLoader == null) continue;
try {
//这里只查找插件它自己的apk,不需要查parent,避免多次无用查询,提高性能
clazz = classLoader.loadClass(className);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException ignored) { }
}
}
throw new ClassNotFoundException(className + " in loader " + this);
}
}
public class PluginManager {
public final static List<PluginItem> plugins = new ArrayList<PluginItem>(); //正在使用的Resources
public static volatile Resources mNowResources; //原始的application中的BaseContext,不能是其他的,否则会内存泄漏
public static volatile Context mBaseContext; //ContextImpl中的LoadedAPK对象mPackageInfo
private static Object mPackageInfo = null; public static volatile ClassLoader mNowClassLoader = null; //系统原始的ClassLoader
public static volatile ClassLoader mBaseClassLoader = null; //系统原始的ClassLoader public static void init(Application application) {
//初始化一些成员变量和加载已安装的插件
mPackageInfo = RefInvoke.getFieldObject(application.getBaseContext(), "mPackageInfo");
mBaseContext = application.getBaseContext();
mNowResources = mBaseContext.getResources(); mBaseClassLoader = mBaseContext.getClassLoader();
mNowClassLoader = mBaseContext.getClassLoader(); try {
AssetManager assetManager = application.getAssets();
String[] paths = assetManager.list(""); ArrayList<String> pluginPaths = new ArrayList<String>();
for(String path : paths) {
if(path.endsWith(".apk")) {
String apkName = path; Utils.extractAssets(mBaseContext, apkName); PluginItem item = generatePluginItem(apkName);
plugins.add(item); pluginPaths.add(item.pluginPath);
}
} reloadInstalledPluginResources(pluginPaths); } catch (Exception e) {
e.printStackTrace();
} ZeusClassLoader classLoader = new ZeusClassLoader(mBaseContext.getPackageCodePath(), mBaseContext.getClassLoader()); File dexOutputDir = mBaseContext.getDir("dex", Context.MODE_PRIVATE);
final String dexOutputPath = dexOutputDir.getAbsolutePath();
for(PluginItem plugin: plugins) {
DexClassLoader dexClassLoader = new DexClassLoader(plugin.pluginPath,
dexOutputPath, null, mBaseClassLoader);
classLoader.addPluginClassLoader(dexClassLoader);
} RefInvoke.setFieldObject(mPackageInfo, "mClassLoader", classLoader);
Thread.currentThread().setContextClassLoader(classLoader);
mNowClassLoader = classLoader; } private static PluginItem generatePluginItem(String apkName) {
File file = mBaseContext.getFileStreamPath(apkName);
PluginItem item = new PluginItem();
item.pluginPath = file.getAbsolutePath();
item.packageInfo = DLUtils.getPackageInfo(mBaseContext, item.pluginPath); return item;
} private static void reloadInstalledPluginResources(ArrayList<String> pluginPaths) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class); addAssetPath.invoke(assetManager, mBaseContext.getPackageResourcePath()); for(String pluginPath: pluginPaths) {
addAssetPath.invoke(assetManager, pluginPath);
} Resources newResources = new Resources(assetManager,
mBaseContext.getResources().getDisplayMetrics(),
mBaseContext.getResources().getConfiguration()); RefInvoke.setFieldObject(mBaseContext, "mResources", newResources);
//这是最主要的需要替换的,如果不支持插件运行时更新,只留这一个就可以了
RefInvoke.setFieldObject(mPackageInfo, "mResources", newResources); //清除一下之前的resource的数据,释放一些内存
//因为这个resource有可能还被系统持有着,内存都没被释放
//clearResoucesDrawableCache(mNowResources); mNowResources = newResources;
//需要清理mtheme对象,否则通过inflate方式加载资源会报错
//如果是activity动态加载插件,则需要把activity的mTheme对象也设置为null
RefInvoke.setFieldObject(mBaseContext, "mTheme", null);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
调用
Intent intent = new Intent();
String serviceName = PluginManager.plugins.get(0).packageInfo.packageName + ".TestService1";
intent.setClass(this, getClassLoader().loadClass(serviceName));
startService(intent);
以上的方案对于四大组件都是适用的
欢迎关注我的微信公众号:安卓圈
Activity插件化解决方案的更多相关文章
- BroadcastReceiver插件化解决方案
--摘自<android插件化开发指南> 1.静态广播和动态广播仅区别于注册方式的不同.静态广播的注册信息保存在PMS中,动态广播的注册信息保存在AMS中 2.发送广播,也就是Contex ...
- Service插件化解决方案
--摘自<android插件化开发指南> 1.ActivityThread最终是通过Instrumentation启动一个Activity的.而ActivityThread启动Servic ...
- ContentProvider插件化解决方案
--摘自<android插件化开发指南> 1.当要传输的数据量大小不超过1M的时候,使用Binder:数据量超过1M时,Binder就搞不定了,需要ContentProvider 2.Co ...
- 《Android插件化开发指南》面世
本书在京东购买地址:https://item.jd.com/31178047689.html 本书Q群:389329264 (一)这是一本什么书 如果只把本书当作纯粹介绍Android插件化技术的书籍 ...
- Android插件化的兼容性(上):Android O的适配
首先声明,<Android插件化开发指南>这本书所介绍的Android底层是基于Android6.0(API level 23)的,而本书介绍的各种插件化解决方案,以及配套的70多个例子, ...
- Android插件化的兼容性(中):Android P的适配
Android系统的每次版本升级,都会对原有代码进行重构,这就为插件化带来了麻烦. Android P对插件化的影响,主要体现在两方面,一是它重构了H类中Activity相关的逻辑,另一个是它重构了I ...
- 包建强的培训课程(10):Android插件化从入门到精通
@import url(http://i.cnblogs.com/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/c ...
- Android 插件化开发(四):插件化实现方案
在经过上面铺垫后,我们可以尝试整体实现一下插件化了.这里我们先介绍一下最简单的实现插件化的方案. 一.最简单的插件化实现方案 最简单的插件化实现方案,对四大组件都是适用的,技术面涉及如下: 1). 合 ...
- Android Small插件化框架解读——Activity注册和生命周期
通过对嵌入式企鹅圈原创团队成员degao之前发表的<Android Small插件化框架源码分析>的学习,对Android使用的插件化技术有了初步的了解,但还是有很多需要认真学习的地方,特 ...
随机推荐
- 饿了么vue-cli3.0+cube-ui笔记
1.目录结构 模板文件是public里的index.html,运行项目的时候,会引用src/main.js(入口文件) 详细文档在这里:https://cli.vuejs.org/zh/config/ ...
- Android:四大架构的优缺点,你真的了解吗?
声明|转载于作者:KunMinX原文链接:https://www.jianshu.com/p/9ef813d5c1af 前言 前不久刚结束对 20 模块项目的第 3 轮重构,一路见证 MVC.MVP. ...
- Python基础之关于表达式
初识表达式: 优雅.清晰和务实是python的核心价值观,如果想通过操作和处理一个序列(或其他的可迭代对象)来创建一个新 的列表时可以使用列表解析(List comprehensions)和生成表达式 ...
- 《剑指offer》旋转数组中的最小数字
本题来自<剑指offer> 旋转数组中的最小数字 题目: 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转. 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素. 例 ...
- HTML&javaSkcript&CSS&jQuery&ajax(11)
1.localStorage 没有 时间的限制数据存储, sessionStorage 针对一个session的存储,首先检查浏览器是否支持对这两个的存储, ifI(type(Storage)!==& ...
- Jmeter 获取CSV行数
import java.io.BufferedReader; import java.io.FileInputStream; String str = "E:\\Desktop\\WOS接口 ...
- 论文阅读笔记十四:Decoupled Deep Neural Network for Semi-supervised Semantic Segmentation(CVPR2015)
论文链接:https://arxiv.org/abs/1506.04924 摘要 该文提出了基于混合标签的半监督分割网络.与当前基于区域分类的单任务的分割方法不同,Decoupled 网络将分割与分类 ...
- 在 Python 中使用 JSON
在 Python 中使用 JSON 本教程将会教我们如何使用 Python 编程语言编码和解码 JSON.让我们先来准备环境以便针对 JSON 进行 Python 编程. 环境 在我们使用 Pytho ...
- python requests 正则爬虫
代码: import requests from multiprocessing import Pool from requests.exceptions import RequestExceptio ...
- 使用newtonjson解决Json日期格式问题
继承 JsonResult 方式 使用Json.Net代替最简单的方法就是使用下面的JsonNetResult 来作为 ActionResult 返回. 1) Install-Package newt ...