移动开发整体凉凉的背景下,究竟还剩哪些 Android开发热门前沿知识

1. Android架构设计模式

  • MVC架构设计模式:MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写。
  • MVP架构设计模式:MVC全名是Model View Persenter,MVP由MVC演变而来,是现在主流的开发模式。
  • MVVM架构设计模式:MVVM全名是Model-View-ViewModel,它本质上就是MVC的改进版。

各种模型的主要目的都是是分离视图(View)和模型(Model),即将UI界面显示和业务逻辑进行分离。

架构设计模式-MVC

移动开发整体凉凉的背景下,究竟还剩哪些 Android开发热门前沿知识

(1) 定义:在android开发过程中,比较流行的开发框架曾经采用的是MVC框架模式。

  • M(Model)层:实体模型,处理业务逻辑。如:数据库操作,网络操作,I/O操作,复杂操作和耗时任务等。
  • V(View)层:处理数据显示。在Android开发中,它一般对应着xml布局文件。
  • C(Controller)层:处理用户交互。在Android开发中,它一般对应着Activity/Feagment。android中主要通过activity处理用户交互和业务逻辑,接受用户的输入并调用Model和View去完成用户的需求。

(2) 特点

  • 低耦合
  • 可重用易拓展
  • 模块职责划分明确

(3) 实例

android本身的设计结构符合 MVC 模式。

(4) MVC优缺点

  • MVC的优点:MVC模式通过Controller来掌控全局,同时将View展示和Model的变化分离开
  • MVC也有局限性:View层对应xml布局文件能做的事情非常有限,所以需要把大部分View相关的操作移到Controller层的activity中。导致activity相当于充当了2个角色(View层和Controller层),不仅要处理业务逻辑,还要操作UI。一旦一个页面的业务繁多复杂的话,activity的代码就会越来越臃肿和复杂。

架构设计模式-MVP
移动开发整体凉凉的背景下,究竟还剩哪些 Android开发热门前沿知识

MVP是从经典的MVC模式演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。在Android开发中,MVP的具体实现流程是当Presenter接收到View的请求,便从Model层获取数据,将数据进行处理。处理好的数据再通过View层的接口回调给Activity或Fragment。这样MVP能够让Activity或Fragment成为真正的View,只做与UI相关的事而不处理其他业务流程。

(1) 定义

  • M(Model)层:实体模型,处理业务逻辑。如:数据库操作,网络操作,I/O操作,复杂操作和耗时任务等。
  • V(View)层:负责View的绘制以及与用户交互。在Android开发中,它一般对应着xml布局文件和Activity/Fragment。
  • P(Presenter)层:负责完成Model层和View层间的数据交互和业务逻辑。

(2) 实例

(3) MVC和MVP的区别

MVP中的View并不直接使用Model,它们之间的通信是通过Presenter来进行的,所有的交互都发生在Presenter内部,而在MVC中View会直接从Model中读取数据而不通过Controller

  • MVC和MVP的最大区别:MVC的Model层和View层能够直接交互;MVP的Model层和View层不能直接交互,需通过Presenter层来进行交互。
  • Activity职责不同:Activity在MVC中属于Controller层,在MVP中属于View层,这是MVC和MVP很主要的一个区别。可以说Android从MVC转向MVP开发也主要是优化Activity的代码,避免Activity的代码臃肿庞大。
  • View层不同:MVC的View层指的是XML布局文件(或用Java自定义的View);MVP的View层是Activity(或Fragment)
  • 控制层不同:MVC的控制层是Activity(或Fragment);MVP的控制层是Presenter,里面没有很多的实际东西,主要负责Model层和View层的交互。

(4) MVP优缺点

  • MVP的优点如下:

模型与视图完全分离,我们可以修改视图而不影响模型;项目代码结构清晰,一看就知道什么类干什么事情;我们可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑,这个特性非常的有用,因为视图的变化总是比模型的变化更频繁 ;协同工作(例如在设计师没出图之前可以先写一些业务逻辑代码)

  • MVP也有不足之处:

接口过多,一定程度影响了编码效率。一定程度上导致Presenter的代码量过大。为了降低Presenter中业务繁多的问题,Google又推出了MVVM,试图通过数据驱动来减少Presenter的代码量。

架构设计模式-MVVM
移动开发整体凉凉的背景下,究竟还剩哪些 Android开发热门前沿知识

(1) 定义

  • M(Model)层:仍然是实体模型(但是不同于之前定义的Model层),主要负责数据获取、存储和变化,提供数据接口供 ViewModel 层调用。
  • V(View)层:对应Activity/Feagment 和xml布局文件 ,负责View的绘制以及与用户交互 说明:View层仅能操作UI(数据绑定来实现 UI 更新);不能做任何和业务逻辑有关的数据操作
  • VM(ViewModel)层:负责完成Model层和View层间的数据交互和业务逻辑 说明:ViewModel层仅能做和业务逻辑有关的数据操作;不能做UI相关的操作。

Android开发热门前沿知识下载地址:https://shimo.im/docs/hHWyVjj6TWGrVv9p

2. 热修复

什么是热修复

热修复:让应用能够在无需重新安装的情况实现更新,帮助应用快速建立动态修复能力。

早期遇到Bug我们一般会紧急发布了一个版本。然而这个Bug可能就是简简单单的一行代码,为了这一行代码,进行全量或者增量更新迭代一个版本,未免有点大材小用了。而且新版本的普及需要时间,以Android用户的升级习惯,即使是相对活跃的微信也需要10天以上的时间去覆盖50%的用户。使用热修复技术,能做到1天覆盖70%以上。这也是基于补丁体积较小,可以直接使用移动网络下载更新。

热修复开发流程

移动开发整体凉凉的背景下,究竟还剩哪些 Android开发热门前沿知识

目前Android业内,热修复技术百花齐放,各大厂都推出了自己的热修复方案,使用的技术方案也各有所异。其中QZone超级补丁基于的是dex分包方案,而dex分包是基于Java的类加载机制 ClassLoader

ClassLoader介绍

任何一个 Java 程序都是由一个或多个 class 文件组成,在程序运行时,需要将 class 文件加载到虚拟机 中才可以使用,负责加载这些 class 文件的就是 Java 的类加载机制。 ClassLoader 的作用简单来说就是加载 class 文件,提供给程序运行时使用。每个 Class 对象的内部都有一个 classLoader字段来标识自己是由哪个 ClassLoader 加载的。

class Class<T> {
  ...
 private transient ClassLoader classLoader;
  ...
}

ClassLoader是一个抽象类,而它的主要实现类主要有:

  • BootClassLoader

    用于加载Android Framework层class文件。
  • PathClassLoader

    用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex
  • DexClassLoader

    用于加载指定的dex,以及jar、zip、apk中的classes.dex
    

很多博客里说 PathClassLoader只能加载已安装的apk的dex,但是实际上 PathClassLoaderDexClassLoader一样都能够加载sdcard中的dex。

Log.e(TAG, "Activity.class 由:" + Activity.class.getClassLoader() +" 加载");
Log.e(TAG, "MainActivity.class 由:" + MainActivity.class.getClassLoader() +" 加载");

//输出:
Activity.class 由:java.lang.BootClassLoader@d3052a9 加载

MainActivity.class 由:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.enjoy.enjoyfix-1/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.enjoyfix-1/lib/x86, /system/lib, /vendor/lib]]] 加载

它们之间的关系如下:

移动开发整体凉凉的背景下,究竟还剩哪些 Android开发热门前沿知识

PathClassLoaderDexClassLoader的共同父类是 BaseDexClassLoader

public class DexClassLoader extends BaseDexClassLoader {

 public DexClassLoader(String dexPath, String optimizedDirectory,
 String librarySearchPath, ClassLoader parent) {
 super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

public class PathClassLoader extends BaseDexClassLoader {

 public PathClassLoader(String dexPath, ClassLoader parent) {
 super(dexPath, null, null, parent);
    }

 public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent){
 super(dexPath, null, librarySearchPath, parent);
    }
}

可以看到两者唯一的区别在于:创建 DexClassLoader需要传递一个 optimizedDirectory参数,并且会将其创建为 File对象传给 super,而 PathClassLoader则直接给到null。因此两者都可以加载指定的dex,以及jar、zip、apk中的classes.dex

PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());

File dexOutputDir = context.getCodeCacheDir();
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/xx.dex",dexOutputDir.getAbsolutePath(), null,getClassLoader());

optimizedDirectory参数为odex的目录。实际上Android中的ClassLoader在加载dex时,会首先经过dexopt对dex执行优化,产生odex文件。optimizedDirectory为null时的默认路径为:/data/dalvik-cache。并且处于安全考虑,此目录需要使用app私有目录,如:getCodeCacheDir()
在API 26源码中,将DexClassLoader的optimizedDirectory标记为了 deprecated 弃用,实现也变为了:
javapublicDexClassLoader(StringdexPath,StringoptimizedDirectory,StringlibrarySearchPath,ClassLoaderparent){super(dexPath,null,librarySearchPath,parent);}
和PathClassLoader一摸一样了!

双亲委托机制

创建 ClassLoader需要接收一个 ClassLoaderparent参数。这个 parent为父类加载。即:某个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。这就是双亲委托机制

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{

 // 检查class是否有被加载
 Class c = findLoadedClass(name);
 if (c == null) {
 long t0 = System.nanoTime();
 try {
 if (parent != null) {
 //如果parent不为null,则调用parent的loadClass进行加载
                c = parent.loadClass(name, false);
            } else {
 //parent为null,则调用BootClassLoader进行加载
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {

        }

 if (c == null) {
 // 如果都找不到就自己查找
 long t1 = System.nanoTime();
            c = findClass(name);
        }
    }
 return c;
}

因此我们自己创建的ClassLoader: newPathClassLoader("/sdcard/xx.dex",getClassLoader());并不仅仅只能获得 xx.dex中的Class,还能够获得其父ClassLoader中加载的Class。

findClass

在所有父ClassLoader无法加载Class时,则会调用自己的 findClass方法。findClass在ClassLoader中的定义为:

protected Class<?> findClass(String name) throws ClassNotFoundException {
 throw new ClassNotFoundException(name);
}

其实任何ClassLoader子类,都可以重写 loadClassfindClass。一般如果你不想使用双亲委托,则重写 loadClass修改其实现。而重写 findClass则表示在双亲委托下,父ClassLoader都找不到Class的情况下,定义自己如何去查找一个Class。而我们的 PathClassLoader会自己负责加载 MainActivity这样的程序中自己编写的类,利用双亲委托父ClassLoader加载Framework中的 Activity。说明 PathClassLoader并没有重写 loadClass,因此我们可以来看看PathClassLoader中的 findClass 是如何实现的。

public BaseDexClassLoader(String dexPath, File optimizedDirectory,String 
                        librarySearchPath, ClassLoader parent) {
 super(parent);
 this.pathList = new DexPathList(this, dexPath, librarySearchPath,
                                    optimizedDirectory);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
 List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
 //查找指定的class
 Class c = pathList.findClass(name, suppressedExceptions);
 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;
}

实现非常简单,从 pathList中查找class。继续查看 DexPathList

public DexPathList(ClassLoader definingContext, String dexPath,
 String librarySearchPath, File optimizedDirectory) {
 //.........
 // splitDexPath 实现为返回 List<File>.add(dexPath)
 // makeDexElements 会去 List<File>.add(dexPath) 中使用DexFile加载dex文件返回 Element数组
 this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);
 //.........

}

public Class findClass(String name, List<Throwable> suppressed) {
 //从element中获得代表Dex的 DexFile
 for (Element element : dexElements) {
 DexFile dex = element.dexFile;
 if (dex != null) {
 //查找class
 Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
 if (clazz != null) {
 return clazz;
            }
        }
    }
 if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
 return null;
}

热修复

PathClassLoader中存在一个Element数组,Element类中存在一个 dexFile成员表示dex文件,即:APK中有X个dex,则Element数组就有X个元素。

移动开发整体凉凉的背景下,究竟还剩哪些 Android开发热门前沿知识

而对于类的查找,由代码 for(Elementelement:dexElements)得知,会由数组从前往后进行查找。

移动开发整体凉凉的背景下,究竟还剩哪些 Android开发热门前沿知识

PathClassLoader中的Element数组为:[patch.dex , classes.dex , classes2.dex]。如果存在Key.class位于patch.dex与classes2.dex中都存在一份,当进行类查找时,循环获得 dexElements中的 DexFile,查找到了Key.class则立即返回,不会再管后续的element中的 DexFile是否能加载到Key.class了。

因此,可以将出现Bug的class单独的制作一份patch.dex文件(补丁包),然后在程序启动时,从服务器下载patch.dex保存到某个路径,再通过patch.dex的文件路径,用其创建 Element对象,然后将这个 Element对象插入到我们程序的类加载器 PathClassLoaderpathList中的 dexElements数组头部。这样在加载出现Bug的class时会优先加载patch.dex中的修复类,从而解决Bug。QQ空间热修复的原理就是这样,利用反射Hook了PathClassLoader中pathList的dexElements数组。

Android开发热门前沿知识下载地址:https://shimo.im/docs/hHWyVjj6TWGrVv9p

3.插件化

前言

插件化技术最初源于免安装运行 apk 的想法,这个免安装的 apk 就可以理解为插件,而支持插件的 app 我们一般

叫宿主。宿主可以在运行时加载和运行插件,这样便可以将 app 中一些不常用的功能模块做成插件,一方面减小

了安装包的大小,另一方面可以实现 app 功能的动态扩展。
插件化的实现

我们如何去实现一个插件化呢?

首先我们要知道,插件apk是没有安装的,那我们怎么加载它呢?不知道。。。

没关系,这儿我们还可以细分下,一个 apk 主要就是由代码和资源组成,所以上面的问题我们可以变为:如何加载

插件的类?如何加载插件的资源?这样的话是不是就有眉目了。

然后我们还需要解决类的调用的问题,这个地方主要是四大组件的调用问题。我们都知道,四大组件是需要注册

的,而插件的四大组件显然没有注册,那我们怎么去调用呢?

所以我们接下来就是解决这三个问题,从而实现插件化

1. 如何加载插件的类?

2. 如何加载插件的资源?

3. 如何调用插件类?

类加载(ClassLoader)

我们在学 java 的时候知道,java 源码文件编译后会生成一个 class 文件,而在 Android 中,将代码编译后会生成

一个 apk 文件,将 apk 文件解压后就可以看到其中有一个或多个 classes.dex 文件,它就是安卓把所有 class 文件

进行合并,优化后生成的。

java 中 JVM 加载的是 class 文件,而安卓中 DVM 和 ART 加载的是 dex 文件,虽然二者都是用的 ClassLoader 加

载的,但因为加载的文件类型不同,还是有些区别的,所以接下来我们主要介绍安卓的 ClassLoader 是如何加载

dex 文件的。

ClassLoader的实现类

ClassLoader是一个抽象类,实现类主要分为两种类型:系统类加载器和自定义加载器。

其中系统类加载器主要包括三种:

BootClassLoader

用于加载Android Framework层class文件。

PathClassLoader

用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex

DexClassLoader

用于加载指定的dex,以及jar、zip、apk中的classes.dex

类继承关系如下图:

移动开发整体凉凉的背景下,究竟还剩哪些 Android开发热门前沿知识

我们先来看下 PathClassLoader 和 DexClassLoader。

// /libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
// optimizedDirectory 直接为 null
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
// optimizedDirectory 直接为 null
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
// API 小于等于 26/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
// 26开始,super里面改变了,看下面两个构造方法
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
// API 26/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
// DexPathList 的第四个参数是 optimizedDirectory,可以看到这儿为 null
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null);
}
// API 25/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}

根据源码了解到,PathClassLoader 和 DexClassLoader 都是继承自 BaseDexClassLoader,且类中只有构造方

法,它们的类加载逻辑完全写在 BaseDexClassLoader 中。

其中我们值的注意的是,在8.0之前,它们二者的唯一区别是第二个参数 optimizedDirectory,这个参数的意思是

生成的 odex(优化的dex)存放的路径,PathClassLoader 直接为null,而 DexClassLoader 是使用用户传进来的

路径,而在8.0之后,二者就完全一样了。

下面我们再来了解下 BootClassLoader 和 PathClassLoader 之间的关系。

// 在 onCreate 中执行下面代码
ClassLoader classLoader = getClassLoader();
while (classLoader != null) {
Log.e("leo", "classLoader:" + classLoader);
classLoader = classLoader.getParent();
}
Log.e("leo", "classLoader:" + Activity.class.getClassLoader());

打印结果:

classLoader:dalvik.system.PathClassLoader[DexPathList[[zip file
"/data/user/0/com.enjoy.pluginactivity/cache/plugin-debug.apk", zip file
"/data/app/com.enjoy.pluginactivity-T4YwTh-
8gHWWDDS19IkHRg==/base.apk"],nativeLibraryDirectories=[/data/app/com.enjoy.pluginactivity-
T4YwTh-8gHWWDDS19IkHRg==/lib/x86_64, /system/lib64, /vendor/lib64]]]
classLoader:java.lang.BootClassLoader@a26e88d
classLoader:java.lang.BootClassLoader@a26e88d

通过打印结果可知,应用程序类是由 PathClassLoader 加载的,Activity 类是 BootClassLoader 加载的,并且

BootClassLoader 是 PathClassLoader 的 parent,这里要注意 parent 与父类的区别。这个打印结果我们下面还会提到。篇幅原因可点击下方链接前往免费下载学习!

......

Android开发热门前沿知识下载地址:https://shimo.im/docs/hHWyVjj6TWGrVv9p

4. Android进程保活

(1) 进程保活概念

进程保活:让进程在内存中永远存在且无法杀死,就算被杀死也能保活。进程被杀死的原因:人为地调用kill;被第三方安全软件杀死。

进程保活并非是一种流氓手段,在很多场景下我们需要一个常驻进程来为用户提供服务,如:

  • 接收屏幕开关的系统广播:因为广播接收者不支持静态注册,必须在进程中动态注册广播接收者来接收,如果没有常驻进程,那么锁屏应用无法为用户正常提供服务。
  • 定位服务:需要在后台维护一个长连接,以便及时地将信息(推送的信息/定位信息等)传达给用户。

缺点:进程保活在内存,不管如何优化,或多或少都会增加性能的开销。所以需在进程保活和内存消耗之间寻找平衡点来为用户进程保活。

(2) android进程优先级和回收策略

android进程优先级:前台进程 > 可见进程 > 服务进程 > 后台进程 > 空进程

android进程的回收策略:主要依靠LMK ( Low Memory Killer )机制来完成。LMK机制通过 oom_adj 这个阀值来判断进程的优先级,oom_adj 的值越高,优先级越低,越容易被杀死。

拓展:LMK ( Low Memory Killer ) 机制基于Linux的OOM(Out Of Memery)机制,通过一些比较复杂的评分机制,对进程进行打分,将分数高的进程判定为bad进程,杀死并释放内存。LMS机制和OOM机制的不同之处在于:OOM只有当系统内存不足时才会启动检查,而LMS机制是定时进行检查。

(3) android进程保活方案

  • 利用系统广播拉活 在发生系统事件时,系统会发出相对响应的广播(常用的广播事件如:开机、网络状态变化、文件或sd卡的卸载等),我们可以在mainfest.xml文件中静态注册广播监听器

缺点(无法拉活的情形):广播接收者被管理软件或系统软件通过自启动管理等功能禁用的场景下是无法接受广播的,从而无法自启动进行系统拉活;系统广播事件是不可控制的,只有在发生事件时才能进行拉活,无法保证进程被杀死后立即被拉活。

  • 利用系统Service机制拉活 将Service中的onStartCommand()回调方法的返回值设为START_STICKY,就可以利用系统机制在Service挂掉后自动拉活。

拓展:onStartCommand()的返回值表明当Service由于系统内存不足而被系统杀掉之后,在未来的某个时间段内当系统内存足够的情况下,系统会尝试创建这个Service,一旦创建成功就又会回调onStartCommand()方法。

缺点(无法拉活的情形):Service第一次被异常杀死后会在5s内重启,第二次会在10s内重启,第三次会在20s内重启,若Service在短时间内被杀死的次数超过3次以上系统就会不惊醒拉活;进程被取得root权限的管理工具或系统工具通过强制stop时,通过Service机制无法重启进程。

  • 利用Native进程拉活 思想:利用Linux中的fork机制创建一个Native进程,在Native进程可以监控主进程的存活,当主进程挂掉之后,Native进程可以立即对主进程进行拉活。

在Native进程中如何监听主进程被杀死:可在Native进程中通过死循环或定时器,轮询地判断主进程被杀死,但是此方案会耗时耗资源;在主线程中创建一个监控文件,并且在主进程中持有文件锁,在拉活进程启动后申请文件锁将会被阻塞,一旦成功获取到锁说明主进程挂掉了。

如何在Native进程中拉活主进程:主要通过一个am命令即可拉活。说明:android5.0后系统对Native进程加强了管理,利用Native进程拉活的方式已失效。

  • 利用JobScheduler机制拉活

说明:android在5.0后提供了JobScheduler接口,这个接口能够监听主进程的存活,然后拉活进程。

  • 利用账号同步机制拉活(已失效)

说明:android系统的账号同步机制会定期同步账号信息,这个方案主要是利用账号同步机制进行进程拉活。不过最新的android版本对账号同步机制做了改动,该方法可能不再生效。
Android开发热门前沿知识下载地址:https://shimo.im/docs/hHWyVjj6TWGrVv9p

上一篇:Bootstrap<基础十六> 导航元素


下一篇:《深入理解Java虚拟机》读书笔记:Java内存区域