关于 Android 进程保活,你所需要知道的一切
Android 进程回收策略(一种根据 OOM_ADJ 阈值级别触发相应力度的内存回收的机制)
Android 系统将尽量长时间地保持应用进程,但随着打开的应用越多,后台应用进程也越多。容易导致系统内存不足。
为了新建进程或运行更重要的进程,最终需要清除旧进程来回收内存。 为了确定保留或终止哪些进程,系统会根据进程的状态等,给进程分配一个优先级。当系统内存不足时,系统会按照优先级高低依次清除进程,回收系统资源。
进程优先级
优先级排序 | 进程类型 | 说明 |
1 | 前台进程 | 用户当前操作所必需的进程。通常在任意给定时间前台进程都为数不多。 只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。 |
2 | 可见进程 | 没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程,可见进程被视为是极其重要的进程。 除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。 |
3 | 服务进程 | 尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。 因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。 |
4 | 后台进程 | 后台进程对用户体验没有直接影响,通常会有很多后台进程在运行,它们会保存在 LRU 列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。 系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 |
5 | 空进程 | 保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。 |
核心思想:提高进程优先级
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// intent - 启动时,启动组件传递过来的intent(其他组件如activity传递参数)
// flags - 启动请求时是否有额外参数,默认为0
// startId - 指明当前服务的唯一ID,与stopSelfResult(startId)配合使用,可以更安全地根据ID停止服务
return super.onStartCommand(intent, flags, startId);
}
onStartCommand 返回值
返回值 | 含义 | 适用场景 |
---|---|---|
START_STICKY | 当Service因内存不足而被系统kill后,一段时间后内存再次空闲时,系统将会尝试重新创建此Service,一旦创建成功后将回调onStartCommand方法,但其中的Intent将是null,也就是onStartCommand方法虽然会执行但是获取不到intent信息 | 这个状态下比较适用于任意时刻开始、结束的服务如音乐播放器 |
START_NOT_STICKY | 当Service因内存不足而被系统kill后,即使系统内存再次空闲时,系统也不会尝试重新创建此Service。除非程序中再次调用startService启动此Service,这是最安全的选项,可以避免在不必要时以及应用能够轻松重启所有未完成的作业时运行服务 | 某个Service执行的工作被中断几次无关紧要 |
START_REDELIVER_INTENT | 当Service因内存不足而被系统kill后,则会重建服务,并通过传递给服务的最后一个 Intent 调用 onStartCommand(),任何挂起 Intent均依次传递。与START_STICKY不同的是,其中的传递的Intent将是非空,是最后一次调用startService中的intent | 适用于主动执行应该立即恢复的作业(例如下载文件)的服务 |
手动返回START_STICKY,当service因内存不足被kill,等到内存空闲后,service又被重新创建。
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
flags = START_STICKY;
return super.onStartCommand(intent, flags, startId);
}
<service
android:name="com.dbjtech.acbxt.waiqin.UploadService"
android:enabled="true" >
<intent-filter android:priority="1000" >
<action android:name="com.dbjtech.myservice" />
intent-filter>
service>
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//添加下列代码将后台Service变成前台Service
//构建"点击通知后打开MainActivity"的Intent对象
Intent notificationIntent = new Intent(this,MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,0,notificationIntent,0);
//新建Builer对象
Notification.Builder builer = new Notification.Builder(this);
builer.setContentTitle("前台服务通知的标题");//设置通知的标题
builer.setContentText("前台服务通知的内容");//设置通知的内容
builer.setSmallIcon(R.mipmap.ic_launcher);//设置通知的图标
builer.setContentIntent(pendingIntent);//设置点击通知后的操作
Notification notification = builer.getNotification();//将Builder对象转变成普通的notification
startForeground(1, notification);//让Service变成前台Service,并在系统的状态栏显示出来
}
一种极低成本的Android屏幕适配方式——字节跳动技术团队
而dpi是根据屏幕真实的分辨率和尺寸来计算的,每个设备都可能不一样的。
通常情况下,一部手机的分辨率是宽x高,屏幕大小是以寸为单位,那么三者的关系是(屏幕分辨率为:1920*1080,屏幕尺寸为5吋的话,那么dpi为440。)
由于不同手机屏幕尺寸、分辨率不同,因此dpi的值很乱,导致dp适配效果(px = (dpi/160)*dp)结果很乱,没有规律,因此应该使用新的适配方式。
public static float applyDimension(int unit, float value,
DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}
private static float sRoncompatDennsity;
private static float sRoncompatScaledDensity;
private void setCustomDensity(@NonNull Activity activity, final @NonNull Application application) {
//application
final DisplayMetrics appDisplayMetrics = application.getResources().getDisplayMetrics();
if (sRoncompatDennsity == 0) {
sRoncompatDennsity = appDisplayMetrics.density;
sRoncompatScaledDensity = appDisplayMetrics.scaledDensity;
application.registerComponentCallbacks(new ComponentCallbacks() {
Override
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig != null && newConfig.fontScale > 0) {
sRoncompatScaledDensity = application.getResources().getDisplayMetrics().scaledDensity;
}
}
@Override
public void onLowMemory() {
}
});
}
//计算宽为360dp 同理可以设置高为640dp的根据实际情况
final float targetDensity = appDisplayMetrics.widthPixels / 360;
final float targetScaledDensity = targetDensity * (sRoncompatScaledDensity / sRoncompatDennsity);
final int targetDensityDpi = (int) (targetDensity * 160);
appDisplayMetrics.density = targetDensity;
appDisplayMetrics.densityDpi = targetDensityDpi;
appDisplayMetrics.scaledDensity = targetScaledDensity;
//activity
final DisplayMetrics activityDisplayMetrics = activity.getResources().getDisplayMetrics();
activityDisplayMetrics.density = targetDensity;
activityDisplayMetrics.densityDpi = targetDensityDpi;
activityDisplayMetrics.scaledDensity = targetScaledDensity;
}
跨平台开发是为了增加代码复用,减少开发者对多个平台差异适配的工作量,降低开发成本,提高业务专注的同时,提供比web更好的体验。
React Native | weex | Flutter | |
---|---|---|---|
出品 | Alibaba | ||
语言 | JavaScript | JavaScript | Dart |
引擎 | JSCore | JS V8 | Flutter Engine |
设计模式 | React | Vue | 响应式 |
社区 | 丰富,Facebook终点维护 | 有点残念,托管apache | 较多拥护者 |
难度 | 较大 | 较小 | 一般 |
支持 | Android IOS | Android IOS Web | Android IOS 等等 |
现状 | 作为RN平台最大支持者Airbnb放弃使用RN 项目庞大维护困难,第三方库良莠不齐,兼容性差 |
被托管到了Apache,拭目以待 | Flutter 是 Google 跨平台移动UI框架,被重点维护 |
由 JetBrains 开发。用于现代多平台应用的静态编程语言。
Kotlin可以编译成Java字节码,也可以编译成JavaScript,方便在没有JVM的设备上运行。
Kotlin已正式成为Android官方支持开发语言。
兼容/无缝对接java,可以java代码和kotlin代码互相调用。一键java转kotlin,如果你有遗留的java代码,可以一键转换
与Java对比
ReactNative 官方文档
创建一个原生模块类CommonModule,继承ReactContextBaseJavaModule,并复写getName()方法,创建暴露给RN调用的方法,并使用@ReactMethod注解修饰
public class CommonModule extends ReactContextBaseJavaModule {
public CommonModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
// 实现getName()方法,该方法用来返回RN,在JS端标记这个模块
// JS端可以通过 React.NativeModules.CommonModule 访问到这个模块
return "CommonModule";
}
@ReactMethod
public void show(String msg) {
// 加上@ReactMethod注解用于暴露给RN调用的方法
// 该方法返回值类型为void,因为RN跨语言访问时是异步进行的,原生代码执行结束只能通过回调函数或发送消息给RN
// 调用该方法弹出一个弹窗到界面
Toast.makeText(reactContext, msg, Toast.LENGTH_LONG).show()
}
}
创建类 CommonPackage 实现接口 ReactPackage 包管理器,并把第1步中创建好的 CommonModule 类添加进来
public class CommonPackage implements ReactPackage {
@Override
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new CommonModule(reactContext));
return modules;
}
将创建好的 CommonPackage 包管理器添加到 ReactPackage 列表中,即在MainApplication.java文件中getPackages方法中提供
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new CommonPackage());
}
把原生模块封装成一个JavaScript模块
'use strict';
import { NativeModules } from 'react-native';
export default NativeModules.CommonModule;
调用该模块方法
import CommonModule from './CommonModule';
CommonModule.show('Awesome');
补:RN 用 Promise 机制与安卓原生代码通信
在原生代码 CommonModule 类中创建桥接方法,当桥接的方法最后一个参数是 Promise 对象,那么该方法就会返回一个JS 的 Promise 对象给对应的 JS 方法
首先需要在 CommonModule 中定义一个暴露给 RN 的方法,并且要用 @ReactMethod 标识
@ReactMethod
fun rnCallNativePromise(String msg,Promise promise) {
Toast.makeText(reactContext, msg, Toast.LENGTH_LONG).show();
promise.resolve("Android 发送消息");
}
const commonModule = NativeModules.CommonModule;
function callAndroidPromise() {
commonModule.rnCallNativePromise('RN Promise 调用 Android 原生')
.then((msg) => {
Alert.alert('RN Promise 收到消息', msg)
})
.catch((error) => {
console.log(error)
})
}
Github:一个APP从启动到主页面显示经历了哪些过程?
创建进程(AMS → Zygote)
补充:Binder通信
简称:
ATP: ApplicationThreadProxy syetem_server 客户端
AMS: ActivityManagerService syetem_server 服务端
AT: ApplicationThread 新创建/App进程 服务端
AMP: ActivityManagerProxy 新创建/App进程 客户端
图解:
①system_server进程中调用startProcessLocked方法,该方法最终通过socket方式,将需要创建新进程的消息告知Zygote进程,并阻塞等待Socket返回新创建进程的pid;
②Zygote进程接收到system_server发送过来的消息, 则通过fork的方法,将zygote自身进程复制生成新的进程,并将ActivityThread相关的资源加载到新进程app process,这个进程可能是用于承载activity等组件;
③ 在新进程app process向servicemanager查询system_server进程中binder服务端AMS, 获取相对应的Client端,也就是AMP. 有了这一对binder c/s对, 那么app process便可以通过binder向跨进程system_server发送请求,即attachApplication()
④system_server进程接收到相应binder操作后,经过多次调用,利用ATP向app process发送binder请求, 即bindApplication. system_server拥有ATP/AMS, 每一个新创建的进程都会有一个相应的AT/AMP,从而可以跨进程 进行相互通信. 这便是进程创建过程的完整生态链。
主流四种IM协议:XMPP协议、IMPP协议(即时信息和空间协议)、PRIM协议(空间和即时信息协议)、SIP协议(即时通讯和空间平衡扩充的进程开始协议)
XMPP协议是针对消息推送的协议,精简。开源、简单且可扩展性强。
private void initSocket() throws UnknownHostException, IOException {
Socket socket = new Socket(HOST, PORT);
mSocket = new WeakReference<Socket>(socket);
mReadThread = new ReadThread(socket);
mReadThread.start();
mHandler.postDelayed(heartBeatRunnable, HEART_BEAT_RATE);// 初始化成功后,就准备发送心跳包(HEART_BEAT_RATE 毫秒后发送)
}
// 发送心跳包
private Handler mHandler = new Handler();
private Runnable heartBeatRunnable = new Runnable() {
@Override
public void run() {
if (System.currentTimeMillis() - sendTime >= HEART_BEAT_RATE) {
boolean isSuccess = sendMsg("");// 就发送一个\r\n过去, 如果发送失败,就重新初始化一个socket
if (!isSuccess) {
mHandler.removeCallbacks(heartBeatRunnable);
mReadThread.release();
releaseLastSocket(mSocket);
new InitSocketThread().start();
}
}
mHandler.postDelayed(this, HEART_BEAT_RATE);
}
};
(2)封装发送数据 / 解析接收数据
建立Socket长连接后,数据在Socket通道中以字节流的形式传输,通过InputStream和outputStream读取和发送数据。我们应该定义通信的数据格式(如HTTP格式包含:协议头+协议主体+校验码),可参考XMPP协议封装数据。
DVM & JVM 区别
(1)执行文件不同
Java虚拟机运行的是Java字节码,Dalvik虚拟机运行的是Dalvik字节码。
传统的Java程序经过编译,生成Java字节码保存在class文件中,Java虚拟机通过解码class文件中的内容来运行程序。
Dalvik虚拟机运行的是Dalvik字节码,所有的Dalvik字节码由Java字节码转换而来,并被打包到一个DEX(Dalvik Executable)可执行文件中。Dalvik虚拟机通过解释DEX文件来执行这些字节码。
Dalvik可执行文件体积小。Android SDK中有一个叫dx的工具负责将Java字节码转换为Dalvik字节码。
dx工具消除java类文件的冗余信息,重新组合形成一个常量池,所有的类文件共享同一个常量池。由于dx工具对常量池的压缩,使得相同的字符串,常量在DEX文件中只出现一次,从而减小了文件的体积。
简单来讲,dex格式文件就是将多个class文件中公有的部分统一存放,去除冗余信息。
(2)架构不同
Java虚拟机基于栈架构,Dalvik虚拟机基于寄存器架构。
Java虚拟机基于栈架构,程序在运行时虚拟机需要频繁的从栈上读取或写入数据,这个过程需要更多的指令分派与内存访问次数,会耗费不少CPU时间,对于像手机设备资源有限的设备来说,这是相当大的一笔开销。Dalvik虚拟机基于寄存器架构。数据的访问通过寄存器间直接传递,这样的访问方式比基于栈方式要快很多。
结构
一个应用首先经过DX工具将class文件转换成Dalvik虚拟机可以执行的dex文件,然后由类加载器加载原生类和Java类,接着由解释器根据指令集对Dalvik字节码进行解释、执行。最后,根据dvm_arch参数选择编译的目标机体系结构。
Android 从5.0开始默认使用ART虚拟机执行程序,抛弃了Dalvik虚拟机.加快了Android的运行效率,提高系统的流畅性。
ART 机制
ART代表Android Runtime,其处理应用程序执行的方式完全不同于Dalvik,Dalvik是依靠一个Just-In-Time (JIT)编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,即翻译工作是在程序运行时进行的。这一机制并不高效,但让应用能更容易在不同硬件和架构上运行。
ART则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫Ahead-Of-Time (AOT)编译,ART在APK在安装时就对其包含的Dex字节码进行翻译,得到对应的本地机器指令,于是就可以在运行时直接执行了,即翻译工作是在APK安装时进行的。应用程序执行将更有效率,启动更快;缺点就是需占用更大的存储空间与更长的应用安装时间(空间换取时间策略)。
ART虚拟机相对于Dalvik虚拟机的提升
Dalvik虚拟机 | ART虚拟机 | |
---|---|---|
预编译 | 采用的是JIT来做及时翻译(动态翻译),在运行时将dex通过解释器翻译成native code | 使用了AOT直接在安装时将dex翻译成native code |
垃圾回收机制 | 标记-清除 算法 (非并发过程STW)如果出现内存不足时,GC频繁会导致UI卡顿掉帧不流畅 |
标记-压缩 + 部分并发 提高GC效率 |
内存管理 | 内存碎片化严重(标记清除算法) | 进行内存管理,减少内存碎片化,提高内存效率 (标记-压缩:将不连续的物理内存块进行压缩) |
Android dex分包方案和热补丁原理
安卓App热补丁动态修复技术介绍
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions); ->> 分析1
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
// 分析1:DexPathList.findClass
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
由上述函数可知,当我们需要加载一个class时,实际是从pathList(DexPathList)中去查找。pathList是一个存储dex文件(每个dex文件实际上是一个DexFile对象)的数组(Element数组,Element是一个内部类),然后依次去加载所需要的class文件,直到找到为止。
public String inject(String libPath) {
boolean hasBaseDexClassLoader = true;
try {
Class.forName("dalvik.system.BaseDexClassLoader");
} catch (ClassNotFoundException e) {
hasBaseDexClassLoader = false;
}
if (hasBaseDexClassLoader) {
PathClassLoader pathClassLoader = (PathClassLoader)sApplication.getClassLoader();
DexClassLoader dexClassLoader = new DexClassLoader(libPath, sApplication.getDir("dex", 0).getAbsolutePath(), libPath, sApplication.getClassLoader());
try {
Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));
Object pathList = getPathList(pathClassLoader);
setField(pathList, pathList.getClass(), "dexElements", dexElements);
return "SUCCESS";
} catch (Throwable e) {
e.printStackTrace();
return android.util.Log.getStackTraceString(e);
}
}
return "SUCCESS";
}
现在试着启动app,并在TestUrlActivity(在第一个dex包中)中去启动SecondActivity(在第二个dex包中),启动成功。这种方案是可行。
注意点:
- 由于第二个dex包是在Application的onCreate中动态注入的,如果dex包过大,会使app的启动速度变慢,因此,在dex分包过程中一定要注意,第二个dex包不宜过大。
- 由于上述第一点的限制,假如我们的app越来越臃肿和庞大,往往会采取dex分包方案和插件化方案配合使用,将一些非核心独立功能做成插件加载,核心功能再分包加载。
为什么使用DexClassLoader加载第二个包而不用PathClassLoader?
因为PathClassLoader只能加载已安装到系统中(即/data/app目录下)的apk文件。第二个包位于资源文件中dex文件,因此只能用DexClassLoader加载。
源码可参考Google MultiDex方案的实现。
H3c —— Android插件化开发
(1)创建插件的工程PluginAUtils,并生成PluginAUtils.apk
(2)创建主工程PluginA,在app/src/main/目录下创建asserts文件夹,把刚刚编译好的pluginA.apk放到里面。创建一个AssertsDexLoader.java用来动态加载PluginAUtils工程。
AssertsDexLoader逻辑为拷贝asserts里的apk文件到外置SD卡上某位置(减少应用安装体积),再把ClassLoader,拷贝的目录及文件三个参数传递给installBundleDexs()即可动态加载其方法至内存。
private static List<DexClassLoader> bundleDexClassLoaderList = new ArrayList<DexClassLoader>();
private static void installBundleDexs(ClassLoader loader, File dexDir, List<File> files) {
if (!files.isEmpty()) {
for (File f : files) {
DexClassLoader bundleDexClassLoader = new DexClassLoader(
f.getAbsolutePath(), dexDir.getAbsolutePath(), null, loader);
bundleDexClassLoaderList.add(bundleDexClassLoader);
}
}
}
使用DexClassLoader代替PathClassLoader除了可以解决Dex加载与系统版本密切问题之外,还可以将第三方apk复制到外置SD卡上减少应用安装后的体积。
(3)在任何需要的地方通过反射调用即可
// class 获取方式
// 用插件化apk直接创建系统的DexClassLoader
// 反射调用的时候先检查PathClassLoader中是否存在,如果不存在就在DexClassLoader list中查找。
public static Class<?> loadClass(String className) throws ClassNotFoundException {
try {
Class<?> clazz = Class.forName(className);
if (clazz != null) {
System.out.println("debug: class find in main classLoader");
return clazz;
}
} catch (Exception e) {
e.printStackTrace();
}
for (DexClassLoader bundleDexClassLoader : bundleDexClassLoaderList) {
try {
Class<?> clazz = bundleDexClassLoader.loadClass(className);
if (clazz != null) {
System.out.println("debug: class find in bundle classLoader");
return clazz;
}
} catch (Exception e) {
e.printStackTrace();
}
}
throw new ClassCastException(className + " not found exception");
}
private void runAssertsDexMethod() throws Exception {
Class<?> clazz = loadClass("h3c.plugina.PluginAUtils");
Constructor<?> constructor = clazz.getConstructor();
Object bundleUtils = constructor.newInstance();
Method printSumMethod = clazz.getMethod("showToastInfo", Context.class);
printSumMethod.setAccessible(true);
printSumMethod.invoke(bundleUtils, getApplicationContext());
}
(二)生命周期的管理
public class BaseActivity extends Activity {
....
// 通过隐式调用宿主的ProxyActivity
public static final String PROXY_VIEW_ACTION = "h3c.pluginapp.ProxyActivity";
// 因为插件的Activity没有Context,所以一切与Context的行为都必须通过宿主代理Activity实现!
protected Activity mProxyActivity;
public void setProxy(Activity proxyActivity) {
mProxyActivity = proxyActivity;
}
@Override
public void setContentView(int layoutResID) {
mProxyActivity.setContentView(layoutResID);
}
@Override
public View findViewById(int id) {
return mProxyActivity.findViewById(id);
}
// 插件的startActivity其实就是调用宿主开启另一个ProxyActivity
public void startActivity(String className) {
Intent intent = new Intent(PROXY_VIEW_ACTION);
intent.putExtra("Class", className);
mProxyActivity.startActivity(intent);
}
....
}
public class ProxyActivity extends Activity {
....
// 因为插件Activity获得的是宿主的Context,这样就拿不到自己的资源,所以这里要用插件的Resource替换ProxyActivity的Resource!
private Resources mBundleResources;
@Override
protected void attachBaseContext(Context context) {
replaceContextResources(context);
super.attachBaseContext(context);
}
public void replaceContextResources(Context context){
try {
Field field = context.getClass().getDeclaredField("mResources");
field.setAccessible(true);
if (null == mBundleResources) {
mBundleResources = AssertsDexLoader.getBundleResource(context, context.getDir(AssertsDexLoader.APK_DIR, Context.MODE_PRIVATE).
getAbsolutePath() + "/app-debug.apk");
}
field.set(context, mBundleResources);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ProxyActivity要加载的插件Activity名字
String className = getIntent().getStringExtra("Class");
try {
Class<?> localClass = AssertsDexLoader.loadClass(className);
Constructor<?> localConstructor = localClass
.getConstructor(new Class[] {});
Object instance = localConstructor.newInstance(new Object[] {});
// 把当前的傀儡Activity注入到插件中
Method setProxy = localClass.getMethod("setProxy",new Class[] { Activity.class });
setProxy.setAccessible(true);
setProxy.invoke(instance, new Object[] { this });
// 调用插件的onCreate()
Method onCreate = localClass.getDeclaredMethod("onCreate",
new Class[] { Bundle.class });
onCreate.setAccessible(true);
onCreate.invoke(instance, new Object[] { null });
} catch (Exception e) {
e.printStackTrace();
}
}
....
}
跳转
Intent intent = new Intent(MainActivity.this, ProxyActivity.class);
intent.putExtra("Class", "h3c.plugina.AActivity");
startActivity(intent);
(2)宿主动态创建Activity模式(动态代理模式)
Activity有着自己的生命周期,但是必须提前在宿主AndroidManifest文件中注册。
(三)资源的加载
要想获得资源文件必须得到一个Resource对象,想要获得插件的资源文件,必须得到一个插件的Resource对象,好在android.content.res.AssetManager.java中包含一个私有方法addAssetPath。只需要将apk的路径作为参数传入,就可以获得对应的AssetsManager对象,从而创建一个Resources对象,然后就可以从Resource对象中访问apk中的资源了。
// 引入插件的AssetManager
private static AssetManager createAssetManager(String apkPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(
assetManager, apkPath);
return assetManager;
} catch (Throwable th) {
th.printStackTrace();
}
return null;
}
// 获得插件的Resource
public static Resources getBundleResource(Context context, String apkPath){
AssetManager assetManager = createAssetManager(apkPath);
return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}
宿主中跳转到ProxyActivity,根据传入的参数反射创建一个插件的Activity,把插件的Resource注入到自己中,并把自己注入到插件Activity中实现生命周期的同步。
缺点
MVC模式下实际上就是Activty与Model之间交互,View完全独立出来了。
View对应于布局文件,其实能做的事情特别少,实际上关于该布局文件中的数据绑定的操作,事件处理的代码都在Activity中,造成了Activity既像View又像Controller,使得Activity变得臃肿。
2. MVP模式 & 优点
MVP,全称 Model-View-Presenter,即模型-视图-层现器。具体如下:
优点
MVP模式通过Presenter实现数据和视图之间的交互,简化了Activity的职责。同时即避免了View和Model的直接联系,又通过Presenter实现两者之间的沟通。
MVP模式减少了Activity的职责,简化了Activity中的代码,将复杂的逻辑代码提取到了Presenter中进行处理,模块职责划分明显,层次清晰。与之对应的好处就是,耦合度更低,更方便的进行测试。
public class User {
private String password;
private String username;
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String toString() {
return "User{" +
"password='" + password + '\'' +
", username='" + username + '\'' +
'}';
}
}
封装了用户名、密码,方便数据传递。
②接口
public interface LoginModel {
void login(User user, OnLoginFinishedListener listener);
}
其中OnLoginFinishedListener 是presenter层的接口,方便实现回调presenter,通知presenter业务逻辑的返回结果,具体在presenter层介绍。
③接口实现类
public class LoginModelImpl implements LoginModel {
@Override
public void login(User user, final OnLoginFinishedListener listener) {
final String username = user.getUsername();
final String password = user.getPassword();
new Handler().postDelayed(new Runnable() {
@Override public void run() {
boolean error = false;
if (TextUtils.isEmpty(username)){
listener.onUsernameError();//model层里面回调listener
error = true;
}
if (TextUtils.isEmpty(password)){
listener.onPasswordError();
error = true;
}
if (!error){
listener.onSuccess();
}
}
}, 2000);
}
}
实现Model层逻辑:延时模拟登陆(2s),如果用户名或者密码为空则登陆失败,否则登陆成功。
2.View层
视图:将Modle层请求的数据呈现给用户。一般的视图都只是包含用户界面(UI),而不包含界面逻辑,界面逻辑由Presenter来实现。
从上图的包结构图中可以看出,View包含内容:
①接口,上面我们说过Presenter与View交互是通过接口。其中接口中方法的定义是根据Activity用户交互需要展示的控件确定的。
②接口实现类,将上述定义的接口中的方法在Activity中对应实现具体操作。
下面以代码的形式一一展开。
①接口
public interface LoginView {
//login是个耗时操作,我们需要给用户一个友好的提示,一般就是操作ProgressBar
void showProgress();
void hideProgress();
//login当然存在登录成功与失败的处理,失败给出提示
void setUsernameError();
void setPasswordError();
//login成功,也给个提示
void showSuccess();
}
上述5个方法都是presenter根据model层返回结果需要view执行的对应的操作。
②接口实现类
即对应的登录的Activity,需要实现LoginView接口。
public class LoginActivity extends AppCompatActivity implements LoginView, View.OnClickListener {
private ProgressBar progressBar;
private EditText username;
private EditText password;
private LoginPresenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
progressBar = (ProgressBar) findViewById(R.id.progress);
username = (EditText) findViewById(R.id.username);
password = (EditText) findViewById(R.id.password);
findViewById(R.id.button).setOnClickListener(this);
//创建一个presenter对象,当点击登录按钮时,让presenter去调用model层的login()方法,验证帐号密码
presenter = new LoginPresenterImpl(this);
}
@Override
protected void onDestroy() {
presenter.onDestroy();
super.onDestroy();
}
@Override
public void showProgress() {
progressBar.setVisibility(View.VISIBLE);
}
@Override
public void hideProgress() {
progressBar.setVisibility(View.GONE);
}
@Override
public void setUsernameError() {
username.setError(getString(R.string.username_error));
}
@Override
public void setPasswordError() {
password.setError(getString(R.string.password_error));
}
@Override
public void showSuccess() {
progressBar.setVisibility(View.GONE);
Toast.makeText(this,"login success",Toast.LENGTH_SHORT).show();
}
@Override
public void onClick(View v) {
User user = new User();
user.setPassword(password.getText().toString());
user.setUsername(username.getText().toString());
presenter.validateCredentials(user);
}
}
View层实现Presenter层需要调用的控件操作,方便Presenter层根据Model层返回的结果进行操作View层进行对应的显示。
3.Presenter层
Presenter是用作Model和View之间交互的桥梁。 从上图的包结构图中可以看出,Presenter包含内容:
①接口,包含Presenter需要进行Model和View之间交互逻辑的接口,以及上面提到的Model层数据请求完成后回调的接口。
②接口实现类,即实现具体的Presenter类逻辑。
下面以代码的形式一一展开。
①接口
public interface OnLoginFinishedListener {
void onUsernameError();
void onPasswordError();
void onSuccess();
}
当Model层得到请求的结果,需要回调Presenter层,让Presenter层调用View层的接口方法。
public interface LoginPresenter {
void validateCredentials(User user);
void onDestroy();
}
登陆的Presenter 的接口,实现类为LoginPresenterImpl,完成登陆的验证,以及销毁当前view。
②接口实现类
public class LoginPresenterImpl implements LoginPresenter, OnLoginFinishedListener {
private LoginView loginView;
private LoginModel loginModel;
public LoginPresenterImpl(LoginView loginView) {
this.loginView = loginView;
this.loginModel = new LoginModelImpl();
}
@Override
public void validateCredentials(User user) {
if (loginView != null) {
loginView.showProgress();
}
loginModel.login(user, this);
}
@Override
public void onDestroy() {
loginView = null;
}
@Override
public void onUsernameError() {
if (loginView != null) {
loginView.setUsernameError();
loginView.hideProgress();
}
}
@Override
public void onPasswordError() {
if (loginView != null) {
loginView.setPasswordError();
loginView.hideProgress();
}
}
@Override
public void onSuccess() {
if (loginView != null) {
loginView.showSuccess();
}
}
}
由于presenter完成二者的交互,那么肯定需要二者的实现类(通过传入参数,或者new)。
presenter里面有个OnLoginFinishedListener, 其在Presenter层实现,给Model层回调,更改View层的状态, 确保 Model层不直接操作View层。
核心流程总结
View与Model并不直接交互,而是使用Presenter作为View与Model之间的桥梁。其中Presenter中同时持有View层的Interface的引用以及Model层的引用,而View层持有Presenter层引用。当View层某个界面需要展示某些数据的时候,首先会调用Presenter层的引用,然后Presenter层会调用Model层请求数据,当Model层数据加载成功之后会调用Presenter层的回调方法通知Presenter层数据加载情况,最后Presenter层再调用View层的接口将加载后的数据展示给用户。本例模式:
一般模式: