第十三章 杂七杂八

面试技巧 专栏收录该内容
31 篇文章 8 订阅

进程保活

关于 Android 进程保活,你所需要知道的一切

Android进程回收策略 及 进程优先级

  • Android 进程回收策略(一种根据 OOM_ADJ 阈值级别触发相应力度的内存回收的机制)
    Android 系统将尽量长时间地保持应用进程,但随着打开的应用越多,后台应用进程也越多。容易导致系统内存不足。
    为了新建进程或运行更重要的进程,最终需要清除旧进程来回收内存。 为了确定保留或终止哪些进程,系统会根据进程的状态等,给进程分配一个优先级。当系统内存不足时,系统会按照优先级高低依次清除进程,回收系统资源。

  • 进程优先级

优先级排序进程类型说明
1前台进程用户当前操作所必需的进程。通常在任意给定时间前台进程都为数不多。
只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。
2可见进程没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程,可见进程被视为是极其重要的进程。
除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。
3服务进程尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。
因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。
4后台进程后台进程对用户体验没有直接影响,通常会有很多后台进程在运行,它们会保存在 LRU 列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。
系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。
5空进程保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。
为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。

进程保活方案

核心思想:提高进程优先级

  1. 通过开启服务提升进程优先级
    对于需要在后台长期运行的操作可以通过创建对应的服务Service(排名3)而不是在Activity开启一个后台子线程(排名4)提高进程优先级
  2. 利用通知(Notification)提升权限
    启动一个前台的Service进程,这样会在系统的通知栏生成一个通知(Notification),使用户可见该运行的app。从而使进程的优先级仅仅低于用户当前正在交互的进程,与可见进程优先级一致,使进程被杀死的概率大大降低。
  3. 通过广播唤醒(不同app进程之间/系统提供广播)
  • 场景1:系统监听开机、网络状态、拍照等事件,产生广播唤醒app
  • 场景2:接入第三方SDK唤醒相应的app进程(如微信sdk,包括微信支付会唤醒微信、或者支付宝会唤醒淘宝等同属于阿里系的app)经常通过推送SDK对app进程保活

Service如何保持不被杀死

  1. onStartCommand方法,返回START_STICKY
    服务的onStartCommand方法
@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);  
}  
  1. 在AndroidManifest中通过android:priority提升service优先级
    在AndroidManifest.xml文件中对于intent-filter可以通过android:priority = "1000"这个属性设置最高优先级,1000是最高值,如果数字越小则优先级越低,同时适用于广播。
<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>  
  1. 使用startForeground 将service放到前台状态,提升service进程优先级
    Android中的进程是托管的,当系统进程空间紧张的时候,会依照优先级自动进行进程的回收。Android将进程分为6个等级,它们按优先级顺序由高到低依次是:
    前台进程( FOREGROUND_APP)、可视进程(VISIBLE_APP )、次要服务进程(SECONDARY_SERVER )、后台进程 (HIDDEN_APP)、内容供应节点(CONTENT_PROVIDER)、空进程(EMPTY_APP)
    当service运行在低内存的环境时,将会kill掉一些存在的进程。因此进程的优先级将会很重要,可以使用startForeground 将service放到前台状态。这样在低内存时被kill的几率会低一些。
@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,并在系统的状态栏显示出来
}  
  1. onDestroy方法里重启service
    service +broadcast 方式,就是当service调用ondestory的时候,发送一个自定义的广播,当收到广播的时候,重新启动service;或直接startService()重新打开服务

屏幕适配

一种极低成本的Android屏幕适配方式——字节跳动技术团队

  • 传统dp适配方式
    android中的dp在渲染前会将dp转为px,计算公式:
  • px = density * dp;
  • density = dpi / 160;
  • px = dp * (dpi / 160);

而dpi是根据屏幕真实的分辨率和尺寸来计算的,每个设备都可能不一样的。
通常情况下,一部手机的分辨率是宽x高,屏幕大小是以寸为单位,那么三者的关系是(屏幕分辨率为:1920*1080,屏幕尺寸为5吋的话,那么dpi为440。)
在这里插入图片描述
由于不同手机屏幕尺寸、分辨率不同,因此dpi的值很乱,导致dp适配效果(px = (dpi/160)*dp)结果很乱,没有规律,因此应该使用新的适配方式。

  • 屏幕适配切入点
    根据 dp和px的转换公式 :px = dp * density ,可通过修改density保证所有设备计算出的px值满足该设备的要求。
    布局文件中dp的转换,最终都是调用 TypedValue#applyDimension(int unit, float value, DisplayMetrics metrics) 来进行转换。因此我们只需要修改 DisplayMetrics 中和 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;
}
  • 最终方案
    下面假设设计图宽度是360dp,以宽维度来适配。
    那么适配后的 density = 设备真实宽(单位px) / 360,接下来只需要把我们计算好的 density 在系统中修改下即可。同时应该修改文字的scaledDensity,可以通过计算之前scaledDensity和density的比获得现在的scaledDensity。并通过registerComponentCallbacks注册监听文字的切换。
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 ?对比?

React NativeweexFlutter
出品FacebookAlibabaGoogle
语言JavaScriptJavaScriptDart
引擎JSCoreJS V8Flutter Engine
设计模式ReactVue响应式
社区丰富,Facebook终点维护有点残念,托管apache较多拥护者
难度较大较小一般
支持Android IOSAndroid IOS WebAndroid IOS 等等
现状作为RN平台最大支持者Airbnb放弃使用RN
项目庞大维护困难,第三方库良莠不齐,兼容性差
被托管到了Apache,拭目以待Flutter 是 Google 跨平台移动UI框架,被重点维护

谷歌新出的官方开发语言Kotlin了解吗 & 和Java相比它有哪些特点。

由 JetBrains 开发。用于现代多平台应用的静态编程语言。
Kotlin可以编译成Java字节码,也可以编译成JavaScript,方便在没有JVM的设备上运行。
Kotlin已正式成为Android官方支持开发语言。
兼容/无缝对接java,可以java代码和kotlin代码互相调用。一键java转kotlin,如果你有遗留的java代码,可以一键转换
与Java对比

  • 更简洁:这是它重要的优点之一,可以比Java编写少得多的代码。
  • 更安全:Kotlin是空安全的,它在编译期间就会处理各种为null的情况,无需像java一样添加很多的判空代码,节约很多调试空指针异常的时间,很大程度上避免出现NullPointException。
  • 易扩展:扩展函数意味着我们不仅可以扩展我们原有写好的类,还可以扩展系统级的类,非常灵活,另外如果在类里编写扩展函数,那么只对当前类生效。
  • 函数式:Kotlin使用了很多函数式编程的概念,比如用到了lambda表达式来更方便地解决问题。
  • Kotlin Android Extensions:再也不用编写烦人的findViewById()了,如果你集成了ButterKnife,是时候删除对它的依赖了,Kotlin支持了对于View以id形式访问。
  • 不用写分号,就像你看到的上述代码一样,对于很多写过脚本语言的童鞋来说,不要写分号这一点真是节省了很多时间,对于一天写几百行几千行甚至上万行代码的童鞋们来说,相当于省了多少个分号.

ReactNative 和 Android 如何交互?

ReactNative 官方文档

  • Android 端

创建一个原生模块类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()); 
}
  • ReactNative 端

把原生模块封装成一个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 标识

  • Android端 发送消息
@ReactMethod
fun rnCallNativePromise(String msg,Promise promise) {
    Toast.makeText(reactContext, msg, Toast.LENGTH_LONG).show();
    promise.resolve("Android 发送消息");
}
  • RN端 收到消息
const commonModule = NativeModules.CommonModule;
function callAndroidPromise() {
	commonModule.rnCallNativePromise('RN Promise 调用 Android 原生')
    .then((msg) => {
        Alert.alert('RN Promise 收到消息', msg)
    })
    .catch((error) => {
        console.log(error)
    })
}

一个APP从启动到主页面显示经历了哪些过程?

Github:一个APP从启动到主页面显示经历了哪些过程?
在这里插入图片描述
创建进程(AMS → Zygote)

  1. 点击桌面App图标,Launcher进程采用Binder IPC向system_server进程发起startActivity请求;
  2. system_server进程接收到请求后,调用ASM的startActivity方法,最后向zygote进程发送创建进程的请求;
  3. Zygote进程fork出新的子进程,即App进程;
  4. App进程内调用ActivityThread.main()方法,随后依次调用Looper.prepareLoop()和Looper.loop()来开启消息循环。
    绑定Application(App → AMS)
  5. App进程随后调用attach()方法,通过Binder IPC向sytem_server进程发起attachApplication请求;
  6. system_server进程在收到请求后,将进程和指定的Application绑定起来。
    显示Activity界面(AMS → App)
  7. 此时系统已经拥有了该application的进程,它通过binder IPC向App进程发送scheduleLaunchActivity请求启动一个新进程的activity;
  8. App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;
  9. 主线程在收到Message后,开始创建目标Activity,通过performLaunchActiivty()方法回调Activity.onCreate()和onStart()方法。
  10. 到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法,UI渲染结束后便可以看到App的主界面。

补充: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,从而可以跨进程 进行相互通信. 这便是进程创建过程的完整生态链。

APP 推送

  1. 定义 & 使用场景
  • 定义
    服务端主动发送消息,客户端被动接收服务端数据
  • 使用场景
    广告、及时通讯
  1. 实现原理
  • 轮询(Pull)
    应用程序隔固定时间主动与服务器进行连接并查询是否有新的消息。
    不适用于即时通讯产品,客户端需要不断检测服务器数据(每隔很短时间连一次服务器),浪费客户端资源(CPU、网络流量、系统电量)
  • SMS(Push)
    服务器有新消息时,发送1条类似短信的信令给客户端,客户端通过拦截信令,解析消息内容 / 向服务器获取信息。
    可以实现实时操作,但是实现成本很高,需要向移动公司缴纳相应费用。
  • 长连接(Push)
    长连接是目前APP推送 最佳 & 主要 的底层实现机制
    客户端主动和服务器建立TCP长连接之后, 客户端定期向服务器发送心跳包, 有消息的时候, 服务器直接通过这个已经建立好的TCP连接通知客户端。
    (1)TCP长连接
    长连接即客户端与服务端建立连接后,互相通信,数据发送完成后也不主动断开连接,之后有需要发送的数据就继续通过该连接发送。
    TCP连接在默认情况下为长连接,即如果连接双方不主动关闭连接,这个连接就一直存在。有一些情况会导致连接切断,如:链路故障,服务器宕机,NAT超时,网络状态变化……
    (2)心跳包(保活TCP连接)
    客户端通过每隔一段时间发送一段极短的数据,证明客户端还活着。如果服务端在一定时间收不到客户端数据,则说明连接断开,服务端便不再向该客户端发送推送消息。
  1. 解决方案
  • C2DM
    Google提供了C2DM(Cloud to Device Messaging)服务。Android Cloud to Device Messaging (C2DM)是一个用来帮助开发者从服务器向Android应用程序发送数据的服务。该服务提供了一个简单的、轻量级的机制,允许服务器可以通知移动应用程序直接与服务器进行通信,以便于从服务器获取应用程序更新和用户数据。C2DM服务负责处理诸如消息排队等事务并向运行于目标设备上的应用程序分发这些消息。
    在这里插入图片描述
  • MQTT协议
    轻量级的消息发布/订阅协议,基于Push方式,wmqtt.jar 是IBM提供的MQTT协议的实现。
  • XMPP协议
    Extensible Messageing and Presence Protocol,可扩展消息与存在协议,是基于可扩展标记语言(XML)的协议,是目前主流的四种IM协议之一。

主流四种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 毫秒后发送)
	}
  • 心跳检测
    通过Handler.postDelayed(Runnable , TimeMillis)实现定时发送心跳检测功能。因为客户端与服务端可能不属于同一线程,考虑AIDL。
// 发送心跳包
	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协议封装数据。

Android 虚拟机及编译过程

Dalvik虚拟机

  1. Dalvik 虚拟机
    Dalvik是Google公司自己设计用于Android平台的Java虚拟机,它是Android平台的重要组成部分,支持dex格式(Dalvik Executable)的Java应用程序的运行。dex格式是专门为Dalvik设计的一种压缩格式,适合内存和处理器速度有限的系统。Google对其进行了特定的优化,使得Dalvik具有高效、简洁、节省资源的特点。从Android系统架构图知,Dalvik虚拟机运行在Android的运行时库层。
    Dalvik作为面向Linux、为嵌入式操作系统设计的虚拟机,主要负责完成对象生命周期管理、堆栈管理、线程管理、安全和异常管理,以及垃圾回收等。
  2. 特点
  • 体积小,占用内存空间小;
  • 专有的DEX可执行文件格式,体积更小,执行速度更快;
  • 常量池采用32位索引值,寻址类方法名,字段名,常量更快;
  • 基于寄存器架构,并拥有一套完整的指令系统;
  • 提供了对象生命周期管理,堆栈管理,线程管理,安全和异常管理以及垃圾回收等重要功能;
  • 所有的Android程序都运行在Android系统进程里,每个进程对应着一个Dalvik虚拟机实例。
  1. 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虚拟机基于寄存器架构。数据的访问通过寄存器间直接传递,这样的访问方式比基于栈方式要快很多。

  2. 结构
    在这里插入图片描述
    一个应用首先经过DX工具将class文件转换成Dalvik虚拟机可以执行的dex文件,然后由类加载器加载原生类和Java类,接着由解释器根据指令集对Dalvik字节码进行解释、执行。最后,根据dvm_arch参数选择编译的目标机体系结构。

ART虚拟机与Dalvik虚拟机的区别

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 APK 编译打包流程

在这里插入图片描述

  1. Java编译器对工程本身的java代码进行编译,这些java代码有三个来源:app的源代码,由资源文件生成的R文件(aapt工具),以及有aidl文件生成的java接口文件(aidl工具)。产出为.class文件。
    ①用AAPT编译R.java文件
    ②编译AIDL的java文件
    ③把java文件编译成class文件
  2. .class文件和依赖的三方库文件通过dex工具生成Delvik虚拟机可执行的.dex文件,包含了所有的class信息,包括项目自身的class和依赖的class。产出为.dex文件。
  3. apkbuilder工具将.dex文件和编译后的资源文件生成未经签名对齐的apk文件。这里编译后的资源文件包括两部分,一是由aapt编译产生的编译后的资源文件,二是依赖的三方库里的资源文件。产出为未经签名的.apk文件。
  4. 分别由Jarsigner和zipalign对apk文件进行签名和对齐,生成最终的apk文件。
    总结为:编译–>DEX–>打包–>签名和对齐

Android dex分包方案和热补丁原理

Android dex分包方案和热补丁原理
安卓App热补丁动态修复技术介绍

Android dex文件 & 类加载器DexLoader

  1. 类加载器简介
    对于Android的应用程序,本质上虽然也是用Java开发,并且使用标准的Java编译器编译出Class文件,但最终的APK文件中包含的却是dex类型的文件。dex文件是将所需的所有Class文件重新打包,打包的规则不是简单的压缩,而是完全对Class文件内部的各种函数表、变量表等进行优化,并产生一个新的文件,这就是dex文件。由于dex文件是一种经过优化的Class文件,因此要加载这样特殊的Class文件就需要特殊的类装载器,这就是DexClassLoader,Android SDK中提供的DexClassLoader类就是出于这个目的。
    DexClassLoader是一个可以从包含classes.dex实体的.jar或.apk文件中加载classes的类加载器。可以用于实现dex的动态加载、代码热更新等等。
    DexClassLoader和PathClassLoader的区别
  • DexClassLoader:能够加载未安装的jar/apk/dex(可加载外部dex文件,如SD卡中)
  • PathClassLoader:Android 系统类&应用类的加载器,只能加载系统中已经安装过的apk(/data/data/包名目录下apk)
  1. 加载原理
    Android的ClassLoader体系
    在这里插入图片描述
    由上图可以看出,在叶子节点上,我们能使用到的是DexClassLoader和PathClassLoader,两个叶子节点的类都继承BaseDexClassLoader中,而具体的类加载逻辑也在此类中:
    // BaseDexClassLoader.java
@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文件,直到找到为止。

dex分包方案

  1. 分包原因
    当一个app的功能越来越复杂,代码量越来越多,也许有一天便会突然遇到下列现象:
  • 生成的apk在2.3以前的机器无法安装,提示INSTALL_FAILED_DEXOPT
    Android2.3及以前版本用来执行dexopt(用于优化dex文件)的内存只分配了5M
  • 方法数量过多,编译时出错,提示:Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536
    一个dex文件最多只支持65536个方法
  1. 分包原理
    将编译好的class文件拆分打包成2个dex,在运行时再动态加载第二个dex文件中。(绕过dex方法数量的限制以及安装时的检查)
    此时,除了第一个dex文件(一个apk唯一包含一个dex文件),其他dex文件都以资源的方式存放在安装包中,并在Application的onCreate()回调中被注意到系统的ClassLoader中。
  2. 分包流程
  • 编译时分包
    (1)将 $ {classes}(该文件夹下都是要打包到第一个dex的文件)打包生成第一个dex。
    (2)将 $ {secclasses}中的文件打包生成第二个dex,并将其打包到资资源文件中
  • 将dex分包(第二个dex)注入ClassLoader
    根据DexClassLoader加载原理:遍历一个存储dex文件的数组,然后依次去加载所需要的class文件,直到找到为止。知注入的解决方案:假如我们将第二个dex文件放入这个数组中,那么在加载第二个dex包中的类时,应该可以直接找到。
    在我们自定义的BaseApplication的onCreate中,我们执行注入操作:
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";  
}   
  • 参数libPath是第二个dex包的文件信息(包含完整路径,我们当初将其打包到了assets目录下),然后将其使用DexClassLoader来加载。
  • 通过反射获取PathClassLoader中的DexPathList中的Element数组(已加载了第一个dex包,由系统加载),以及DexClassLoader中的DexPathList中的Element数组(刚将第二个dex包加载进去)
  • 将两个Element数组合并之后,再将其赋值给PathClassLoader的Element数组
    通常情况下,dexElements数组中只会有一个元素,就是apk安装包中的classes.dex
    而我们则可以通过反射,强行的将一个外部的dex文件添加到此dexElements中,这就是dex的分包原理了。

现在试着启动app,并在TestUrlActivity(在第一个dex包中)中去启动SecondActivity(在第二个dex包中),启动成功。这种方案是可行。

注意点:

  1. 由于第二个dex包是在Application的onCreate中动态注入的,如果dex包过大,会使app的启动速度变慢,因此,在dex分包过程中一定要注意,第二个dex包不宜过大。
  2. 由于上述第一点的限制,假如我们的app越来越臃肿和庞大,往往会采取dex分包方案和插件化方案配合使用,将一些非核心独立功能做成插件加载,核心功能再分包加载。

为什么使用DexClassLoader加载第二个包而不用PathClassLoader?
因为PathClassLoader只能加载已安装到系统中(即/data/app目录下)的apk文件。第二个包位于资源文件中dex文件,因此只能用DexClassLoader加载。

源码可参考Google MultiDex方案的实现。

热补丁

  1. 适用场景
    当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。
    这时候就提出一个问题:有没有办法以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装?
    空间Android独立版5.2发布后,收到用户反馈,结合版无法跳转到独立版的访客界面,每天都较大的反馈。在以前只能紧急换包,重新发布。成本非常高,也影响用户的口碑。最终决定使用热补丁动态修复技术,向用户下发Patch,在用户无感知的情况下,修复了外网问题,取得非常好的效果。
  2. 实现原理
    该方案基于的是android dex分包方案的。原理可概括为:
    App的一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。
    理论上,Dex分包方案中是没有重复类的,因此,如果在不同的dex中有重复的类存在,那么会优先选择排在前面的dex文件的类,如下图:
    在这里插入图片描述
    在此基础上,我们构想了热补丁的方案,把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex补丁包插入到Elements的最前面,这样出现bug的类就会被覆盖。如下图:
    在这里插入图片描述
  3. 实现方案
  • 把需修复、含Bug的类 独立打包到1个Dex文件中(记为:patch.dex)
  • 将该 Dex文件 插入到ClassLoader中集合 dexElements的最前面
    在这里插入图片描述

Android 插件化

H3c —— Android插件化开发

  1. 简介
    所谓插件化,就是让我们的应用不必再像原来一样把所有的内容都放在一个apk中,可以把一些功能和逻辑单独抽出来放在插件apk中,然后主apk做到[按需调用],这样的好处是一来可以减少主apk的体积,让应用更轻便,二来可以做到热插拔(将功能需要的时候插上去,废弃的时候拔下来),更加动态化。
    插件化可以解决如下问题:
  • 应用体积越来越大,需要拆分apk完成模块化与热部署——减小主包大小
  • 应用频繁更新,用户黏性降低——不发版上新功能
  • 需求不确定时,添加新功能——允许动态添加新功能,一旦不适用或发生问题,可以进行动态替换(不需要紧急发布补丁或进行升级)
  • 主应用用户量较大,同系新应用需要导流,传统特性只能引导用户下载安装——bug修复工具
  1. 需解决的问题 & 解决方案
    插件化核心问题 —— Android动态加载
    即动态调用外部的 dex文件,极端的情况下,Android APK自身带有的Dex文件只是一个程序的入口(或者说空壳),所有的功能都通过从服务器下载最新的Dex文件完成。
    通过ClassLoader加载,然后通过代理模式让Activity等组件具有生命周期实现真正的功能,并且解决了资源访问问题。是解决插件化的基本问题。
    (一)类的加载
  • DexClassLoader:要想实现加载外部dex文件(即插件)来实现热部署,那么必然要把其中的class文件加载到内存中。其中涉及到两种ClassLoader:DexClassLoader和PathClassLoader。而DexClassLoader可以加载外部的jar,dex等文件,正是我们需要的。
  • 反射:因为插件apk与宿主apk不在一个apk内,那么一些类的访问必然要通过反射进行获取。

(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());
    }

(二)生命周期的管理

  • 代理模式插件化实现的过程主要靠欺上瞒下,坑蒙拐骗来实现。想想虽然加载进来了Activity等组件,但也仅仅是最为一个对象而存在,并没有在AndroidManifest中注册,没有生命周期的回调,并不能实现我们想要的效果。因此无论是dynamic_load_apk通过代理activity来操控插件activity的方式,还是DroidPlugin通过hook activity启动过程来启动插件activity的方式,都是对代理模式的应用。
    (1)宿主代理Activity模式(静态代理模式)
    宿主代理无需在宿主中注册Activity,所有跳转均由一个傀儡Activity完成,这样的好处是无需过多的改变宿主即可完成插件开发,但是插件Activity并不享有系统提供的生命周期,其所有生命周期必须由宿主通过反射的方式传递。
  • 在PluginA工程中创建BaseActivity.java,关键代码如下:
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);
    }
    ....
}
  • 在PluginA工程中创建AActivity.java和BActivity.java。让AActivity可以点击跳转到BActivity即可。
  • 重新编译PluginA,将Apk替换到宿主中。
  • 在宿主工程中创建ProxyActivity.java并在AndroidManifest文件中注册。关键代码:
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中实现生命周期的同步。

Android MVP模式

  1. MVC模式 & 缺点
    MVC,全称Model-View-Controller,即模型-视图-控制器。 具体如下:
  • View:对应于布局文件
  • Model:业务逻辑和实体模型
  • Controllor:对应于Activity

缺点
MVC模式下实际上就是Activty与Model之间交互,View完全独立出来了。
View对应于布局文件,其实能做的事情特别少,实际上关于该布局文件中的数据绑定的操作,事件处理的代码都在Activity中,造成了Activity既像View又像Controller,使得Activity变得臃肿。
在这里插入图片描述
2. MVP模式 & 优点
MVP,全称 Model-View-Presenter,即模型-视图-层现器。具体如下:

  • View 对应于Activity,负责View的绘制以及与用户交互
  • Model 依然是业务逻辑和实体模型
  • Presenter 负责完成View于Model间的交互

优点
MVP模式通过Presenter实现数据和视图之间的交互,简化了Activity的职责。同时即避免了View和Model的直接联系,又通过Presenter实现两者之间的沟通。
MVP模式减少了Activity的职责,简化了Activity中的代码,将复杂的逻辑代码提取到了Presenter中进行处理,模块职责划分明显,层次清晰。与之对应的好处就是,耦合度更低,更方便的进行测试。
在这里插入图片描述

  1. MVP & MVC 区别
    在这里插入图片描述
    MVC中是允许Model和View进行交互的,而MVP中很明显,Model与View之间的交互由Presenter完成。还有一点就是Presenter与View之间的交互是通过接口的。
  2. MVP模式 典例 —— 登录案例
    结构图
    在这里插入图片描述
    1.Model层
    在本例中,M0del层负责对从登录页面获取地帐号密码进行验证(一般需要请求服务器进行验证,本例直接模拟这一过程)。 从上图的包结构图中可以看出,Model层包含内容:
    ①实体类bean
    ②接口,表示Model层所要执行的业务逻辑
    ③接口实现类,具体实现业务逻辑,包含的一些主要方法
    下面以代码的形式一一展开。
    ①实体类bean
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层的接口将加载后的数据展示给用户。本例模式:
在这里插入图片描述
一般模式:
在这里插入图片描述

  • 5
    点赞
  • 0
    评论
  • 6
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:终极编程指南 设计师:CSDN官方博客 返回首页

打赏作者

李一恩

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值