0%

Linux 和Android中的IPC机制

Linux 中的IPC机制

Linux中提供了很多进程间通信机制,主要有: 管道(Pipe)、信号(Singal)、信号量(Semophore)、消息队列(Message)、共享内存(Share Memory) 和套接字(Socket)等。

管道:管道的主要思想是在内存中创建一个共享文件,从而使通信双方利用这个共享文件来传递信息,这个共享文件比较特殊,它不属于文件系统并且只能存在于内存中。此外,管道采用半双工通信方式,数据只能在一个方向上流动。

信号:信号是软件层上对中断机制的一种模拟,信号是一种异步通信方式,进程不必通过任何操作来等待信号的到达。内核可以利用信号来通知用户空间的进程发生了哪些系统事件。但是,信号不适合信息交换,比较适用于进程中断控制

信号量:信号量是一个计数器,用来控制多个进程对共享资源的访问。信号量常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。信号量主要用于进程间以及同一进程内不同线程之间的同步手段

消息队列:消息队列是消息的链表,具有特定格式,允许一个或者多个进程向它写入与读取消息。使用消息队列会使信息复制两次,因此对于频繁通信或者信息量大的通信不宜使用消息队列

共享内存:共享内存的多个进程可以直接读写一块内存空间,是针对其他通信机制运行效率较低而设计的。为了在多个进程间交换信息,内核专门留出一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。这样进程就能直接读写这块内存而不需要进行数据复制。

Android中的IPC机制

序列化:Serializable 和 Parcelable 。

Messenger:是一种轻量级的IPC方案,对AIDL进行封装,它是以串行的方式来处理客户端发送的信息的,如果有大量消息发送到服务端,那么服务端仍然逐个处理在响应客户端明显是不合适的。并且,虽然能够跨进程数据传递,但Messenger不能跨进程方法调用

AIDL:相对Messenger,可以并发处理客户端发来的消息,能够进行跨进程方法调用。

Bundle、文件共享、ContentProvider 以及 BroadcastReceiver 则略过。

开启多进程

Android中开启多进程的原因主要有以下几点:

  • 单进程内存分配不够,需要更多内存。尤其是早期的Android系统只为一个单进程分配 16MB 的可用内存。
  • 独立运行的组件,比如个推,它的服务会另外开启一个进程。
  • 运行一些”不可见人“的操作,比如获取用户的隐私信息,比如防止双守护进程被用户杀掉

一、屏幕刷新机制

简单来说,屏幕刷新包括三个步骤:CPU计算、GPU进一步处理和缓存、最后display 再将缓存(buffer)中的数据显示出来

Screen Tearing(撕裂)

在早期设备上,采用单缓冲模式,由于 display 的处理频率是固定的,而CPU/GPU 处理数据的时间不确定,因此,会有屏幕撕裂的情况:

屏幕撕裂

从上图可看出,由于 CPU/GPU 仅完成部分工作,因此,在 0.015s 时,屏幕上部分显示的是2画面,下部分显示的是 1 画面的撕裂画面。

Double-buffer

双缓冲技术,原理就是采用2个 buffer,一块 back buffer 用于 CPU/GPU 后台绘制,另一块 framebuffer 则用于显示,当 back buffer 准备就绪之后,才进行交换,这样就避免了屏幕撕裂。

VSync

在 Android 4.1 之前,没有采用 VSync 信号,此时 CPU 或 GPU 往 buffer 中写数据比较随意,因此会造成丢帧:

无VSync造成丢帧

当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次循环,此时有一段时间空隙,这个时间点就是我们进行缓冲区交换的最佳时间。

在 Android 4.1 之后,Android display 系统进行了重构(黄油计划:Project Butter),引入三个核心元素: VSync、Triple Buffer 以及 Choreographer,它规定一旦收到 VSync 通知(约16ms触发一次),CPU 和 GPU 就立刻开始工作把显示数据写入 buffer。根据 VSync 新来来同步处理数据,让 CPU/GPU 有完整的 16ms 时间来处理数据,减少 jank。

Triple Buffer

双缓存机制并不完美,当 CPU/GPU 工作时间较长时,会出现如下情况:

双缓冲下的jank

从图可以看出,当 CPU/GPU 的处理时间超过 16ms 时,第一个 VSync 信号来了,缓冲区 B 还没准备好,于是只能继续显示 A 缓冲区的内容,而在 B 完成后又因为第2个 VSync 信号还没来,不能及时展示,只能等待下一个 VSync 信号到来,在这一过程中,有一大段时间是被浪费的

三缓冲虽然不能解决双缓冲第一次 jank 问题,但是当第一次 VSync 发生后,CPU 不用再等待了,它会使用第三个buffer C 来进行下一帧的准备工作。虽然对缓冲区 C 的处理所需时间也超狗 16ms ,但并不影响显示屏——第2次 VSync 到来时,会选择 buffer B 进行展示,而第 3 次 VSync 时,会接着采用 C ,而不是像 double buffer 时只能再显示一遍 B ,这样就有效地减低了 jank,如下图所示:

三缓冲示意图

App 相关的屏幕绘制

总的来说,当有屏幕刷新操作时(是否时invalidate ?),系统会将 View 树的测量、布局和绘制等封装到一个 Runnable(当然,会过滤掉同一帧内的重复调用) ,然后监听 VSync 信号,待信号来时再触发此 Runnable。

卡顿分析利器——Systrace 工具

systrace 的使用

一般来说,系统很多关键地方都添加了 trace ,可以看到很多关键代码耗时,当然,我们自己也可以自定义(叫做添加自己的 Label):

1
2
3
4
Trace.beginSection("名称"):
//doSomething

Trace.endSection():

一般来说,添加自己的 Label 需要注意以下几点:

  • begin 和 end 必须成对出现
  • Label 支持嵌套,但是 begin 会找最近的 end
  • begin 和 end 必须在同一线程
  • 抓取 systrace 时,必须指定包名

但是,还有一种情况,那就是 异步trace,使用方法如下:

1
2
3
4
5
6
Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "Stop bootanim", 0);

//dosomeThing

Trace.asyncTraceEnd(Trace.TRACE_TAG_WINDOW_MANAGER, "Stop bootanim", 0);

Systrace 文件获取

Android 在 platform-tools 中提供了 systrace 文件获取的工具,这是一个 python 脚本,用命令行就能够获取:

python systrace.py –help

一般来说,我们只需要使用 -o ,-b, -t,-a 对应输出文件路径,buffersize,time,需要分析的应用程序包名。

查看trace 文件

使用 chrome 打开导出的 trace 文件,大概如下图所示:

trace文件分析

重点关注带 F 的原点,表示一帧,只有圆点为绿色时才表示这一帧没有超过 16ms ,其他颜色都是大于 16ms ,红色表示严重超时。

异步 trace

以上内容参考自CSDN上的博客简书上的博客

页面整体

  • App选择(Android、iOS)
  • 版本选择(生产版本、测试版本)
  • 时间段(1天、1周、1个月,最多支持3个月)

对于选中的

  • App启动次数、网络请求次数(地域分布)
  • App崩溃次数,崩溃率(可选时间段)
  • ANR分析、卡顿分析
  • http请求错误率、CDN性能
  • 服务响应时间(平均0.4s 的水平)
  • Webview(响应最差的接口、http错误率最高的接口)
  • js错误类型
  • 交互信息:设备型号分布、操作系统版本、App版本(各个版本占比多少)

以上内容对照现有的APM系统界面编写

360开源的 ArgusAPM 线上监控

主要功能

ArgusAPM 主要支持如下性能指标:

  • 交互分析:分析 Activity 生命周期耗时
  • 网络请求分析:监控流量使用情况,发现并定位各种网络问题
  • 内存分析:全面监控内存的使用
  • 进程监控:针对多进程应用,统计进程启动情况,发现启动异常(耗电、存活率等)
  • 文件监控: 监控 App 私有文件大小/变化,避免私有文件过大导致的卡顿
  • 卡顿分析:监控并发现卡顿原因
  • ANR 分析: 捕获 ANR 异常

结构

主要注意有一个

数据采集

1、卡顿(block) 信息

通过使用 :

1
Looper.getMainLooper().setMessageLogging()

方法设置自定义的 Printer,监听消息的开始和消息的结束。

2、FPS

主要通过监听系统执行下一帧的回调来做到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//这个方法的使用方式
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback(){

@Override
public void doFrame(long frameTimeNanos) {

}
});

//在 ArgusAPM 中的使用:
public void doFrame(long frameTimeNanos) {
mFpsCount ++;
mFrameTimeNanos = frameTimeNanos;
if(isCanWork()) {
Choreographer.getInstance().postFrameCallback(this);
} else {
mFpsCount = 0;
}
}

在 doFrame 回调里面,每次都重新注册这个监听,当到了定时任务,就计算当前 mFpsCount 数量(总帧数),我们直到 FPS 无非就是每秒绘制的帧数,所以,我们可以计算出 FPS 的值:总帧数/时间。

3、Memory

内存收集,在 ArgusAPM 中是通过 getMemoryInfo 方法来获得的:

1
2
3
4
5
6
7

private MemoryInfo getMemoryInfo() {
// 注意:这里是耗时和耗CPU的操作,一定要谨慎调用
Debug.MemoryInfo info = new Debug.MemoryInfo();
Debug.getMemoryInfo(info);
return new MemoryInfo(ProcessUtils.getCurrentProcessName(), info.getTotalPss(), info.dalvikPss, info.nativePss, info.otherPss);
}

通过 Debug 类的相关功能,最后在Native层面实现。 Debug 这个类很有用,我们可以看下主要的方法,会有意想不到的收获。比如启动时等待调试就是调用的 waitForDebugger 方法;获取当前的虚拟机信息可以通过 getVmFeatureList 方法。此外,这个比较耗时和cpu,所以谨慎调用,需要合理降低采集的次数。

4、watchdog

WatchDogTask 做的事情和 blockTask 类似,都是卡顿检测,不过采用另外的思路,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private Runnable runnable = new Runnable() {
@Override
public void run() {
if (null == mHandler) {
Log.e(TAG, "handler is null");
return;
}

mHandler.post(new Runnable() {
@Override
public void run() {
mTick++;
}
});

try {
Thread.sleep(DELAY_TIME);
} catch (InterruptedException e) {
e.printStackTrace();
}

if (TICK_INIT_VALUE == mTick) {
String stack = captureStacktrace();
saveWatchdogInfo(stack);
} else {
mTick = TICK_INIT_VALUE;
}

AsyncThreadTask.getInstance().executeDelayed(runnable, ArgusApmConfigManager.getInstance().getArgusApmConfigData().funcControl.getWatchDogIntervalTime());
}
};

主要思路是:往主线程post 一个任务,对一个变量 mTick 执行 ++ 操作,然后再当前线程中 sleep 一段时间,然后再去检测 mTick,假如主线程没有卡顿的话,那么 ++ 操作肯定会得到执行,这时候 mTick 就不会与初始值相等。如果相等就可以认为这个等待时间里面,主线程发生了卡顿,这个时候就采集数据,采集的主要是堆栈

5、ANR

发生 ANR 时都会在 data/anr 下产生 trace 文件,因此 anr 就是以 trace 文件为核心。ArgusAPM 提供了两种思路: 1)通过Fileobserver 的方式监听 data/anr 这个目录;2)定时采样方式(隔一段时间就保存一下现场)。

但是这里需要注意,如果没有权限就拿不到 trace 文件,这里并没有解决方案。

6、Net

有个 gradle 的 plugin,通过这个 gradle ,在编译 APK 的时候,读取所有的 class 文件,如果发现 Class 文件是 Okhttp 的时候,在方法里面,拿到 interceptors 字段,添加自己的 Interceptor ,这样就完成了 用 ASM 写入一段字节码,这样就可以采集 Okhttp 信息。

7、Activity

Activity 的启动常规方式不好采集,ArgusAPM 采用 Hook 的方式,这里采用的是 Hook Instrumentation 这个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static void hookInstrumentation() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException {
Log.e(TAG, " hookInstrumentation: ");
Class<?> c = Class.forName("android.app.ActivityThread");
Method currentActivityThread = c.getDeclaredMethod("currentActivityThread");
boolean acc = currentActivityThread.isAccessible();
if (!acc) {
currentActivityThread.setAccessible(true);
}
Object o = currentActivityThread.invoke(null);
if (!acc) {
currentActivityThread.setAccessible(acc);
}
Field f = c.getDeclaredField("mInstrumentation");
acc = f.isAccessible();
if (!acc) {
f.setAccessible(true);
}
Instrumentation currentInstrumentation = (Instrumentation) f.get(o);
Instrumentation ins = new ApmInstrumentation(currentInstrumentation);
f.set(o, ins);
if (!acc) {
f.setAccessible(acc);
}
}

hook 当前currentActivityThread 的 mInstrumentation。每次执行 Activity 的任何生命周期都会先调用到 Instrumentation 。在后续可以通过 Hook 方式将自定义的 Instrumentation 代理系统原来的 Instrumentation,这样,就能统计 Activity 中的每个 onXXX 方法执行的耗时,而没有嵌入任何代码。

8、Appstart

ArgusAPM 中采用的是 application 的启动到第一个 Activity 的创建结束,因为已经 Hook 了 Instrumentation ,因此在 Instrumentation 的 callApplicationOnCreate 方法执行时,就记录下 Application 启动的时间,然后 callActivityOnCreate 记录下第一个 Activity 的启动即可获得冷启动的启动时间。

总结

除了net 和 Activity 之外,其他的采集并不难,只是细节会非常多,所以需要很精细化的控制,降低对 App 的影响。

以上内容参考自简书上的博客csdn上的博客

关于线上监控,还可以参考爱奇艺的 xCrash框架,介绍如下:

xCarash源码解析

xCrash 整体分为两部分: 运行于崩溃的App进程楼内的部分、以及独立进程的部分(称为dumper)

  • App 进程内的部分分为 Java 部分 和 Native 部分,前者主要用于 Java 层的崩溃捕捉,后者用于在Native 负责信号捕捉
  • Dumper 独立进程是纯 Native 实现,主要用于负责凤奎进程中线程相关数据的收集和控制。

Java层的 Exception 捕捉

在 Java 层主要是还是通过 Java 自身提供的方法来捕捉,只需要自己自定义 UncaughtExceptionHandler 即可,在 xCrash 中定义了一个 JavaCrashHandler 类专门用来干这个事情,之后,在里面实现了注册捕捉:

1
Thread.setDefaultUncaughtExceptionHandler(this);

针对 ANR 的捕捉

ANR 捕捉主要是用 AnrHandler 实现,主要就是针对 /data/anr 路径的监听,但是,这只适合低版本的手机,在高版本(Android 版本 5.0 以上)的手机上,应用已经访问不到 /data/anr 目录了。xCrash 是怎么实现呢?实际上它捕获了 SIGQUIT 信号,其原理是:Android App 发生 ANR 时, AMS 会向 App 发送 SIGQUIT 信号!

当接在 Native 层收到 SIGQUIT 信号时,就开始 dump 现场信息。

Native 层的 Exception 捕捉

在Native 崩溃发生时,生成 tombstone 文件(与官方的格式兼容),方便查看

以上内容参考自简书的博客、以及爱奇艺技术产品团队

为什么 ViewPager 没有滑动冲突

可能你会有疑问,在 ViewPager 时代,ViewPager 嵌套 ViewPager 并没有出现过滑动冲突。但为什么在升级了 ViewPager2 之后就出现了滑动冲突呢?

既然如此,我们看下 ViewPager 的 onInterceptTouchEvent 方法(为了方便阅读,代码做了删减):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
      @Override
2 public boolean onInterceptTouchEvent(MotionEvent ev) {
3
4 final int action = ev.getAction() & MotionEvent.ACTION_MASK;
5 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
6 // 在事件取消或者抬起手指后重置状态
7 resetTouch();
8 return false;
9 }
10
11 switch (action) {
12 case MotionEvent.ACTION_MOVE: {
13 // 这里判断在水平方向上的滑动距离大于竖直方向的2倍,则认为是有效的切换页面的滑动
14 if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
15 mIsBeingDragged = true;
16 // 禁止Parent View拦截事件,即事件要能够传递到ViewPager
17 requestParentDisallowInterceptTouchEvent(true);
18 setScrollState(SCROLL_STATE_DRAGGING);
19 } else if (yDiff > mTouchSlop) {
20 mIsUnableToDrag = true;
21 }
22 break;
23 }
24
25 case MotionEvent.ACTION_DOWN: {
26 if (mScrollState == SCROLL_STATE_SETTLING
27 && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
28 // 在Down事件中禁止Parent View拦截事件,是为了事件序列能够传递到ViewPager
29 requestParentDisallowInterceptTouchEvent(true);
30 setScrollState(SCROLL_STATE_DRAGGING);
31 } else {
32 completeScroll(false);
33 mIsBeingDragged = false;
34 }
35 break;
36 }
37
38 case MotionEvent.ACTION_POINTER_UP:
39 onSecondaryPointerUp(ev);
40 break;
41 }
42 return mIsBeingDragged;
43 }

可以看到,首先在 ACTION_DOWN 的时候通过 requestParentDisallowInterceptTouchEvent(true) 禁止 Parent View 拦截事件,以便后续的事件还能传到 ViewPager 中来;在 ACTION_MOVE 的时候也会有 水平方向上的滑动距离大于竖直方向的2倍 条件来判断是否需要禁止 Parent View 拦截事件。

所以 ViewPager 时我们只管用,无需担心滑动冲突的问题。再看下 ViewPager2 的 onInterceptTouchEvent 方法:

1
2
3
4
5
6
7
8
9
1    private class RecyclerViewImpl extends RecyclerView {
2
3 .... // 省略部分代码
4
5 @Override
6 public boolean onInterceptTouchEvent(MotionEvent ev) {
7 return isUserInputEnabled() && super.onInterceptTouchEvent(ev);
8 }
9 }

它并没有为我们做冲突处理!为什么呢?因为 ViewPager2 被声明成 final 的,并不能继承,假如它像 ViewPager 一样官方给处理了滑动冲突,那么如果有特殊要求的情况下,官方的冲突处理可能会妨碍我们自己写的冲突处理,所以全权交给开发者自己处理了。

滑动冲突的处理方案

在处理滑动冲突之前先了解处理滑动冲突的两种方案。

外部拦截法

所谓的 “外部拦截法” 这个 “外部” 是指 出现滑动冲突的这两个布局的外层。因为,一个事件序列是由 Parent View 先获取的,如果 Parent View 不拦截事件才会交给子 View 去处理,既然外部先获知事件,那外层 View 根据情况来决定是否要拦截事件就行了。大概思路如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
 1     public boolean onInterceptTouchEvent(MotionEvent event) {
2 boolean intercepted = false;
3 int x = (int) event.getX();
4 int y = (int) event.getY();
5 switch (event.getAction()) {
6 case MotionEvent.ACTION_DOWN: {
7 intercepted = false;
8 break;
9 }
10 case MotionEvent.ACTION_MOVE: {
11 if (needIntercept) { // 这里根据需求判断是否需要拦截
12 intercepted = true;
13 } else {
14 intercepted = false;
15 }
16 break;
17 }
18 case MotionEvent.ACTION_UP: {
19 intercepted = false;
20 break;
21 }
22 default:
23 break;
24 }
25 mLastXIntercept = x;
26 mLastYIntercept = y;
27 return intercepted;
28 }

首先也是在 ACTION_DOWN 中不做拦截,其次是在 ACTION_MOVE 中根据需要拦截。

内部拦截法

所谓“内部拦截法”指的是对内部的View 做文章,让内部 View 决定是不是拦截事件。因为 Google 官方提供了 requestDisallowInterceptTouchEvent 方法,它接收一个 Boolean 值,意思是是否要禁止父 ViewGroup 拦截当前事件,如果是 true 的话,父 ViewGroup 就无法对事件进行拦截,看下具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 1     public boolean dispatchTouchEvent(MotionEvent event) {
2 int x = (int) event.getX();
3 int y = (int) event.getY();
4
5 switch (event.getAction()) {
6 case MotionEvent.ACTION_DOWN: {
7 // 禁止parent拦截down事件
8 parent.requestDisallowInterceptTouchEvent(true);
9 break;
10 }
11 case MotionEvent.ACTION_MOVE: {
12 int deltaX = x - mLastX;
13 int deltaY = y - mLastY;
14 if (disallowParentInterceptTouchEvent) { // 根据需求条件来决定是否让Parent View拦截事件。
15 parent.requestDisallowInterceptTouchEvent(false);
16 }
17 break;
18 }
19 case MotionEvent.ACTION_UP: {
20 break;
21 }
22 default:
23 break;
24 }
25
26 mLastX = x;
27 mLastY = y;
28 return super.dispatchTouchEvent(event);
29 }

在 dispatchTouchEvent 的 ACTION_DOWN 和 ACTION_MOVE 行为中,分别执行相应动作来判断是否允许 Parent View 拦截自己的事件。

处理 ViewPager2 的滑动冲突

在解决冲突之前,我们首先要确定下存在哪些需要拦截哪些不需要拦截的边界条件,来分析下:

  • 如果设置了 userInputEnable=false ,那么ViewPager2不应该拦截任何事件;
  • 如果只有一个Item,那么ViewPager2也不应该拦截事件;
  • 如果是多个Item,且当前是第一个页面,那么只能拦截向左的滑动事件,向右的滑动事件就不应该由ViewPager2拦截了;
  • 如果是多个Item,且当前是最后一个页面,那么只能拦截向右的滑动事件,向左的滑动事件不应该由当前的ViewPager2拦截;
  • 如果是多个Item,且是中间页面,那么无论向左还是向右的事件都应该由ViewPager2拦截;
  • 最后,由于ViewPager2是支持竖直滑动的,那么竖直滑动也应该考虑以上条件。

分析完成后,我们看下应该使用哪种方案来处理滑动冲突,很明显,我们应该使用 内部拦截法,但是,由于 ViewPager2 被设置为了 final ,我们无法通过继承方式来处理。

所以,我们需要在 ViewPager2 外部包裹一层自定义的 Layout,在它里面实现事件拦截逻辑,用它来实现内部拦截!看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
  1class ViewPager2Container @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : RelativeLayout(context, attrs, defStyleAttr) {
2
3 private var mViewPager2: ViewPager2? = null
4 private var disallowParentInterceptDownEvent = true
5 private var startX = 0
6 private var startY = 0
7
8 override fun onFinishInflate() {
9 super.onFinishInflate()
10 for (i in 0 until childCount) {
11 val childView = getChildAt(i)
12 if (childView is ViewPager2) {
13 mViewPager2 = childView
14 break
15 }
16 }
17 if (mViewPager2 == null) {
18 throw IllegalStateException("The root child of ViewPager2Container must contains a ViewPager2")
19 }
20 }
21
22 override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
23 val doNotNeedIntercept = (!mViewPager2!!.isUserInputEnabled
24 || (mViewPager2?.adapter != null
25 && mViewPager2?.adapter!!.itemCount <= 1))
26 if (doNotNeedIntercept) {
27 return super.onInterceptTouchEvent(ev)
28 }
29 when (ev.action) {
30 MotionEvent.ACTION_DOWN -> {
31 startX = ev.x.toInt()
32 startY = ev.y.toInt()
33 parent.requestDisallowInterceptTouchEvent(!disallowParentInterceptDownEvent)
34 }
35 MotionEvent.ACTION_MOVE -> {
36 val endX = ev.x.toInt()
37 val endY = ev.y.toInt()
38 val disX = abs(endX - startX)
39 val disY = abs(endY - startY)
40 if (mViewPager2!!.orientation == ViewPager2.ORIENTATION_VERTICAL) {
41 onVerticalActionMove(endY, disX, disY)
42 } else if (mViewPager2!!.orientation == ViewPager2.ORIENTATION_HORIZONTAL) {
43 onHorizontalActionMove(endX, disX, disY)
44 }
45 }
46 MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> parent.requestDisallowInterceptTouchEvent(false)
47 }
48 return super.onInterceptTouchEvent(ev)
49 }
50
51 private fun onHorizontalActionMove(endX: Int, disX: Int, disY: Int) {
52 if (mViewPager2?.adapter == null) {
53 return
54 }
55 if (disX > disY) {
56 val currentItem = mViewPager2?.currentItem
57 val itemCount = mViewPager2?.adapter!!.itemCount
58 if (currentItem == 0 && endX - startX > 0) {
59 parent.requestDisallowInterceptTouchEvent(false)
60 } else {
61 parent.requestDisallowInterceptTouchEvent(currentItem != itemCount - 1
62 || endX - startX >= 0)
63 }
64 } else if (disY > disX) {
65 parent.requestDisallowInterceptTouchEvent(false)
66 }
67 }
68
69 private fun onVerticalActionMove(endY: Int, disX: Int, disY: Int) {
70 if (mViewPager2?.adapter == null) {
71 return
72 }
73 val currentItem = mViewPager2?.currentItem
74 val itemCount = mViewPager2?.adapter!!.itemCount
75 if (disY > disX) {
76 if (currentItem == 0 && endY - startY > 0) {
77 parent.requestDisallowInterceptTouchEvent(false)
78 } else {
79 parent.requestDisallowInterceptTouchEvent(currentItem != itemCount - 1
80 || endY - startY >= 0)
81 }
82 } else if (disX > disY) {
83 parent.requestDisallowInterceptTouchEvent(false)
84 }
85 }
86
87 /**
88 * 设置是否允许在当前View的{@link MotionEvent#ACTION_DOWN}事件中禁止父View对事件的拦截,该方法
89 * 用于解决CoordinatorLayout+CollapsingToolbarLayout在嵌套ViewPager2Container时引起的滑动冲突问题。
90 *
91 * 设置是否允许在ViewPager2Container的{@link MotionEvent#ACTION_DOWN}事件中禁止父View对事件的拦截,该方法
92 * 用于解决CoordinatorLayout+CollapsingToolbarLayout在嵌套ViewPager2Container时引起的滑动冲突问题。
93 *
94 * @param disallowParentInterceptDownEvent 是否允许ViewPager2Container在{@link MotionEvent#ACTION_DOWN}事件中禁止父View拦截事件,默认值为false
95 * true 不允许ViewPager2Container在{@link MotionEvent#ACTION_DOWN}时间中禁止父View的时间拦截,
96 * 设置disallowIntercept为true可以解决CoordinatorLayout+CollapsingToolbarLayout的滑动冲突
97 * false 允许ViewPager2Container在{@link MotionEvent#ACTION_DOWN}时间中禁止父View的时间拦截,
98 */
99 fun disallowParentInterceptDownEvent(disallowParentInterceptDownEvent: Boolean) {
100 this.disallowParentInterceptDownEvent = disallowParentInterceptDownEvent
101 }
102}

代码已经很清楚了,但是主要注意一下: 在onFinishInflate中我们通过循环,遍历自定义 Layout 的所有子 View ,如果没有找到 ViewPager2 就抛出异常

以上内容参考自刘望舒的公众号,如果链接失效,可以查看原文赌一包辣条的博客

注: 本文的EventBus版本为 3.0

Subscrib 注解

自 3.0 以来,EventBus 使用 @Subscrib 注解来标记订阅事件的方法,方法命名随意。并不用像以前那样指定方法的命名:

1
2
@Subscribe
public void testEventBus(Object obj){ }

这个注解的定义很有个性,可以看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Documented
@Retention(RetentionPolicy.RUNTIME) // 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在
@Target({ElementType.METHOD}) // 作用在方法上
public @interface Subscribe {

// 指定事件订阅方法所在的线程模式,也就是决定订阅方法是在哪个线程,默认是POSTING模式
ThreadMode threadMode() default ThreadMode.POSTING;

// 是否支持粘性事件
boolean sticky() default false;

// 优先级,如果指定了优先级,则若干方法接收同一事件时,优先级高的方法会先接收到。
int priority() default 0;
}

其中有几种线程模式,有以下几种:

  • ThreadMode.MAIN:如在主线程(UI线程)发送事件,则直接在主线程处理事件;如果在子线程发送事件,则先将事件入队列,然后通过 Handler 切换到主线程,依次处理事件。

  • ThreadMode.ASYNC:与ThreadMode.MAIN_ORDERED相反,无论在哪个线程发送事件,都将事件加入到队列中,然后通过线程池执行事件

  • ThreadMode.POSTING:默认的线程模式,在哪个线程发送事件就在对应线程处理事件,避免了线程切换,效率高。

  • ThreadMode.MAIN_ORDERED:无论在哪个线程发送事件,都将事件加入到队列中,然后通过Handler切换到主线程,依次处理事件。

  • ThreadMode.BACKGROUND:与ThreadMode.MAIN相反,如果在子线程发送事件,则直接在子线程处理事件;如果在主线程上发送事件,则先将事件入队列,然后通过线程池处理事件。

注册

注册过程很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
EventBus.getDefault().register(this);

//注册源码
public void register(Object subscriber) {
Class<?> subscriberClass = subscriber.getClass(); // 获取传入的要注册类的字节码文件
List<SubscriberMethod> subscriberMethods =
subscriberMethodFinder.findSubscriberMethods(subscriberClass);

synchronized (this) {

// 遍历订阅方法封装类的集合
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}

根据源码可以直到,这个方法就做了两件事情:

  • 根据传入的参数object ,获取其 Class ,然后通过这个 Class 获取所有的方法(当然,首先看有没有缓存),查看经过 @Subscrib 修饰的方法,即这个类中所有的订阅方法(会做封装),生成一个list
  • 遍历上述生成的 list ,给 2 个 Map 填充数据:subscriptionsByEventType以 event (订阅方法的参数)的类型(Class)为key,value 为订阅方法list(CopyOnWriteArrayList);typesBySubscriber 以register时传入的对象为key ,value 为 这个对象所有订阅方法所订阅的事件。
1
2
3
4
5
6
//EventBus中变量的声明

//可以根据event(订阅方法中的参数)类型 获取所有订阅方法
private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
//根据注册对象,获取这个对象上所有的订阅方法
private final Map<Object, List<Class<?>>> typesBySubscriber;

反注册

反注册也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
EventBus.getDefault().unregister(this);

public synchronized void unregister(Object subscriber) {

List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);

// 如果集合不为null
if (subscribedTypes != null) {

// 遍历集合,获取订阅事件的类型
for (Class<?> eventType : subscribedTypes) {

unsubscribeByEventType(subscriber, eventType);
}
typesBySubscriber.remove(subscriber);
} else {
logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());
}
}

刚才在注册时候,说了其中一个 map 为 typesBySubscriber,它以注册对象为 key ,value 为这个对象中所有注册方法所注册的event 类型的列表。所以在反注册的时候,

  1. 首先通过传入的对象,获取 注册的 event 的列表
  2. 遍历这个列表,获取 event 的类型,然后通过这个类型在 subscriptionsByEventType 查找(经过封装的)订阅方法,根据封装在里面的 注册对象 是否是当前 unregister 传入的对象来判断,如果是当前传入的对象,就移除这个经过封装的订阅方法

post 发布事件

post的使用也很简单:

1
EventBus.getDefault().post(new Object());

注册的时候,说了有一个map 为 subscriptionsByEventType ,以 event 的类型为key ,存储了所有订阅了这中 event 的(经过封装的)方法。post 的时候,根据post发送的事件类型(post方法的参数的 Class )从 subscriptionsByEventType 这个集合中获取到所有的订阅方法。之后依次通过反射调用这些方法:

1
2
3
4
5
6
7
8
9
10
void invokeSubscriber(Subscription subscription, Object event) {

try {
subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
} catch (InvocationTargetException e) {
handleSubscriberException(subscription, event, e.getCause());
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unexpected exception", e);
}
}

其中, subscription.subscriberMethod.method 是 Method 类型的,用过反射的话,就知道它可以直接 invoke ,它的定义是这样的:

1
public native Object invoke(Object obj, Object... args)

第一个参数 obj 指的是用 obj 这个实例来调用这个方法(因为一个类可能会有多个实例,非静态方法需要指定一个实例来调用这个方法),后面的 args 就是方法需要传入的参数。

所以,在 invoke 的时候,subscription.subscriber 我们应该很容易知道是 register 时传入的那个 Object !由此,我们一个消息就形成了闭环。

最后问题,支持跨进程吗?

不支持跨进程,因为单例。经过上述分析,我们知道 EventBus 的注册、反注册、post 都是通过:EventBus.getDefault() 实现,我们看下它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class EventBus {

static volatile EventBus defaultInstance;

/** Convenience singleton for apps using a process-wide EventBus instance. */
public static EventBus getDefault() {
EventBus instance = defaultInstance;
if (instance == null) {
synchronized (EventBus.class) {
instance = EventBus.defaultInstance;
if (instance == null) {
instance = EventBus.defaultInstance = new EventBus();
}
}
}
return instance;
}

所以我们都是基于一个 static 类型的 defaultInstance去做一系列操作,由于跨进程后,静态会失效,所以,EventBus 并不能跨进程

以上参考自掘金刘洋巴金的博客,如果链接变得不可访问,可以参看这篇文章的Copy

自定义属性与自定义 Style

我们一般会自定义一个View,比如 MyCustomView,然后再设置自定义属性:在 res/values 目录下,创建一个 attrs.xml ,在其中编写 declare-styleable :

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyCustomView">
<attr name="header" format="reference"/>
<attr name="age">
<flag name="child" value="10"/>
<flag name="young" value="18"/>
</attr>
</declare-styleable>
</resources>

这个过程有2点需要注意:

  • declare-styleable 旁边的name属性,这个属性的取值对应所定义的类名。因为我们自定义的类名叫做 MyCustomView ,所以这里的name 取值也是 MyCustomView
  • 自定义属性值可以组合使用,比如 表示既可以自定义 color 值(比如#ff0000),也可以利用 @color/xxx 来引用 color.xml 中已有的值

当然,我们有时候也需要使用常量来表示,比如上述的 flag,相当于代码中的常量,比如 young 就表示数字 18。

在xml中使用自定义属性

要记得以 xmlns 导入自定义的属性集:

xmlns:attrstest=”http://schemas.android.com/apk/res-auto"

在代码中获取自定义的属性主要使用 TypedArray

declare-styleable 标签其他属性的用法

暂略

测量与布局

ViewGroup 的绘制流程

View 和ViewGroup 基本相同,只不过ViewGroup 不仅要绘制自己,还要绘制其子控件,而View 只需要绘制自己即可,所以就以 ViewGroup 来讲解。

绘制流程分为 3 个步骤,测量(onMeasure)、布局(onLayout)、绘制(onDraw),需要注意的一点是:onMeasure 用于测量当前控件的大小,为正式布局提供建议(只是提供建议,至于用不用,需要看 onLayout 函数)。

onMeasure 函数与 MeasureSpec

测量过程通过 measure() 函数实现,是 View 树自顶向下的遍历,每个 View 在循环过程中将尺寸细节往下传递,当测量过程完成后,所有的 View 都存储了自己的尺寸。

并且,布局过程 layout 也是自顶向下实现的,在这个过程中,每个父 View 负责通过计算好的尺寸放置它的子 View。

onMeasure 函数

该函数有2个int类型的参数 widthMeasureSpec 和 heightMeasureSpec ,这两个参数都是父View传递过来给当前 View 的一个建议值

MeasureSpec 组成

虽然上述参数是int 类型的,但是它们是由 mode + size 两部分组成的。它们转换成二进制后,前2位表示模式(mode),后30位表示数值(size)。模式主要有3种:

  • UNSPECIFIED(mode位:00):父元素不对子元素施加任何约束,子元素可以得到任意想要的大小
  • EXACTLY(mode位:01):父元素决定子元素的确切大小,子元素将被限定在给定的便捷里而忽略它本身大小
  • AT_MOST(mode位:10):子元素至多能达到指定的大小值

由以上特性,我们就可以很简单地直到模式和数值的提取:MODE_MASK,它的二进制表示中,前2位是1,其余30位都是 0,这样,我们只需要将widthMeasureSpec (或 heightMeasureSpec) 位与(&) MODE_MASK 就可以得到 mode 值,将 他们 & ~MODE_MASK 即可得到数值

mode 的用处

参数 widthMeasureSpec 和 heightMeasureSpec 各自都有对应的mode,而这个mode 来自 XML 定义,简单来说xml 布局和 mode 有如下关系:

  • Wrap_content -> MeasureSpec.AT_MOST
  • match_parent -> MeasureSpec.EXACTLY
  • 具体值 -> MeasureSpec.EXACTLY
1
2
3
<com.example.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"/>

比如上述例子中,FlowLayout 在 onMeasure() 函数中传值时,widthMeasureSpec 的mode 是 MeasureSpec.EXACTLY ,即父窗口宽度值;heightMeasureSpec 的mode是 MeasureSpec.AT_MOST ,即值不确定。一定要注意的是:当模式是 MeasureSpec.EXACTLY 时,就不必设定我们计算的值了,因为这个大小是用户指定的,我们不应该改。但当模式是 MeasureSpec.AT_MOST 时,就需要将大小设定为我们计算的数值,因为用户使用的是 wrap_content,没有设置具体值

1
2
3
4
5
6
7
8
9
10
11
12
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {
//假设width 与 height 使我们计算得到控件应该占的宽和高,省略计算过程
int width = ...;
int height = ...;

//假设 measureWidth 与 measureHeight 是获取到的后30位的value
int measureWidth = ...;
int measureHeight = ...;

//在 onMeasure 函数中,最终我们必须要通过 setMeasuredDimension 来设置
setMeasuredDimension((widthMode == MeasureSpec.EXACTLY)?measureWidth: width, (heightMode == MeasureSpec.EXACTLY)? measureHeight: height,)
}

onLayout 函数

前面说了 ,onLayout 实现了所有子View的布局,注意,是所有子View。那它自己的布局怎么办?这个后续说。其实 onLayout 是个抽象函数,也就是所有继承 ViewGroup 的类都要自己去实现这个函数,LinearLayout 和 RelativeLayout 都是如此,均重写了。

简单示例(自己加的章节)

用一个简单示例说明 onMeasure 与 onLayout 的具体使用,比如要做如下效果图:

简单效果

这个效果图需要关注2点:(1)、三个TextView 竖直排列 (2)、背景Layout宽度是 match_parent ,高度是 wrap_content 。首先看下布局文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<com.example.myapplication.MyLinLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#ff00ff">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ff0000"
android:text="第一个VIEW" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#00ff00"
android:text="第二个VIEW" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#0000ff"
android:text="第三个VIEW" />

</com.example.myapplication.MyLinLayout>

自定义的 MyLinLayout 的宽高分别为 match_parent 和 wrap_content。

MyLinLayout 重写 onMeasure() 函数

前面提到 onMeasure 的作用就是根据 container 内部的子控件计算自己的宽和高 ,然后通过 setMeasuredDimension 方法设置进去。先看看完整的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);

int height = 0;
int width = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
//测量子控件
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
//获取子控件的宽高
int childHeight = child.getMeasuredHeight();
int childWidth = child.getMeasuredWidth();
//得到最大宽度,并且累加高度
height += childHeight;
width = Math.max(childWidth, width);
setMeasuredDimension((measureWidthMode == MeasureSpec.EXACTLY) ? measureWidth : width,
(measureHeightMode == MeasureSpec.EXACTLY) ? measureHeight : height);
}

步骤如下:

  1. 获取从父类传过来的建议宽高,提取mode 和value
  2. 测量所有的子 View ,之后就能获取所有子View 的测量宽高
  3. 根据所有子View 的宽高,来获得自己的计算宽高,这个值计算的其实就是自己宽和高都被设置为 wrap_content 情况下的值,因为 Exactly 的时候,并不建议我们去改value 的。
  4. 因为是竖直排列,所以container 的高度应该是各个子View的高度和;宽度应该是各个子View 最大的宽度
  5. 最后,根据当前用户设置的mode来判断是否需要将这个计算宽高设置进去,用它来实现当前container 所在的位置

由于我们在上面的xml 布局文件中,将 MyLinLayout 的宽度设置为 match_parent ,高度为 wrap_content ,所以在onMeasure 里面,

int measureWidthMode 应该是MeasureSpec.EXACTLY; measureHeightMode 为 MeasureSpec.AT_MOST,换句话说,width 使用的是从父类传过来的 measureWidth ,高度是我们自己计算的 height,即实际情况应该等价于:

1
setMeasuredDimension(measureWidth, height);

MyLinLayout 重写 onLayout 函数

在这一部分是根据自己的医院把 container 内部的各个控件排列起来,先看完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
int childHeight = child.getMeasuredHeight();
int childWidth = child.getMeasuredWidth();
//设置子View的位置
child.layout(0, top, childWidth, top + childHeight);
top += childHeight;
}
}

代码讲完,再讲一个非常容易混淆的问题:

getMeasuredWidth 与 getWidth

这两个值大部分时候是相同的,但是含义根本不一样,二者区别主要体现在:

  • getMeasuredWidth 函数在 measure 过程结束后就可以获取到宽度值,而getWidth 需要在 layout 完成后才能获取到高度值!
  • getMeasuredWidth 获取的值是通过 setMeasuredDimension 函数来设置的;而 getWidth 函数值则是通过 layout(left,top,right,bottom) 函数来进行设置的

看完两个函数后,我们也明白了 onMeasure 阶段提供的测量结果只是为布局提供建议的,最终要看 onLayout 函数。因为我们在 child.layout 的时候,直接通过 0 + child.measuredWidth 来计算right 的值,所以我 getMeasuredWidth 与 getWidth 函数返回的值就是一样的了

疑问:container 自己什么时候被布局

这其实要追溯到 View 的 layout 里面:

1
2
3
4
5
6
public void layout(int l, int t, int r, int b) {
...省略代码
boolean changed = setFrame(l, t, r, b);
...省略代码
onLayout(changed, l, t, r, b);
}

setFrame 设置的是自己的位置,结束后才调用 onLayout 。此时,measure 和 layout 都结束了,但是我们还没考虑 margin。

获取子控件margin

如果要自定义 ViewGroup 支持子控件的 layout_margin 参数,则自定义的 ViewGroup 类必须重写 generateLayoutParams() 函数,并且在该函数返回一个 ViewGroup.MarginLayoutParams 派生类对象!

为了验证,我们可以在之前的 MyLinLayout 例子基础上,为 TextView 添加 margin ,但是我们能看到,压根就没有起作用,代码就先略了。这是为什么呢?因为测量和布局都是我们自己实现的,我们在 onLayout() 函数中没有根据 margin 来布局。

需要注意的是,如果我们在 onLayout() 函数中根据 margin 来布局,那么 onMeasure() 函数中计算 container 的大小时,也要加上 layout_margin 参数,否则导致 container 太小而控件显示不全

实例展示

之前说的需要重写 generateLayoutParams() 函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}

@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}

@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}

至于为什么要这么写,稍后再讲。

重写 onMeasure() 函数

实现 FlowLayout 容器

先略

当measure 完成之后,尺寸才会存到 View 属性中,我们才能获取到 View 的属性

java作为解释型语言,因其高度抽象的特性导致其很容易被反编译,容易被反编译,自然有防止反编译的措施存在。常见的防反编译的技术有以下几种。

隔离Java程序

最简单的方法就是让用户不能够访问到 Java Class 程序,这种方法是最根本的方法。具体方法有很多,比如:

  • 将关键的Java Class 放在服务端,客户端通过访问接口来获得服务,而不是直接访问 Class 文件,这样黑客就没法反编译 Class 文件。

但是有很多应用场景不适合这种保护方式,比如单机运行的程序 就无法格力Java 程序。

对 Class 文件进行加密

为了防止Class 文件被直接反编译,许多开发人员将一些关键的 Class 文件进行加密,例如,注册码、序列号管理等相关的类。

在使用这些类之前,程序寿险需要对这些类进行解密,然后再将这些类装在到 JVM 中,这些类的加密可以由硬件完成,也可以使用软件完成

转换成 Native Code

Native Code 往往难以被反编译,开发人员可以选择将整个应用程序转换成 Native Code,也可以选择将关键模块转换为 Native Code。

当然,这么做的同时,也牺牲了Java 的跨平台特性。如果仅仅转换关键部分,Java 程序在使用这些模块时,需要使用 JNI 技术调用。

为了保证Native 代码不被修改和替代,通常需要对这些代码进行数字签名。在使用前,往往需要先对 Native Code 进行认证。

代码混淆

代码混淆是对 Class 文件进行重新组织和处理,功能虽然还保证,但是很难被反编译,即使反编译后也非常难懂。

以上内容参考自如何防止你的java jar被反编译

信息传递(自己起的,感觉书起的章节很乱)

Android中提供了很多不同的信息传递方式,本节主要衡量每种传递方式的效率和使用场景。

在最基础的组件化架构中,组件层中的模块是相互独立的,并不存在依赖,没有依赖就没法传递消息,那该如何传递消息呢?我们需要第三方协助,也就是基础层(BaseModule),如下图所示:

基础组件化通信

从图可以看出,Base module 就是跨越组件化层级通信的关键,也是模块间交流的基础

方式1、本地广播

本地广播与全局广播比较:

  • 本地广播只能动态注册,全局广播可以动态和静态注册
  • 本地广播只局限于当前App(严谨说是当前进程),全局广播可以跨进程
  • 本地广播使用Handler 实现,就在当前进程传播,因此效率比全局广播高

但是,在用于组件间通信时,本地广播将一切全交给系统负责了,无法干预传输途中的任何步骤

方式2、事件总线

事件总线主要有 EventBus 和 RxBus。

事件总线通过记录对象、使用监听者模式来通知对象各种事件。工作机制如下图:

事件总线工作示意

其中,EventBus 是一款针对Android优化的发布/订阅事件总线,主要功能是替代 Intent、Handler、BroadCast,在Fragment、Activity、Service、线程之间传递消息,优点是开销小,代码更优雅,发送和接收者解耦;缺点是依附的对象销毁时一定要记得取消订阅,否则由于强引用会导致内存泄漏,并且,每个事件都必须自定义一个事件类,造成事件类太多

注意: EventBus 2.x 使用的是 运行时注解,很大程度上是依赖于反射规则的,采用反射的方式对整个注册的类的所有方法进行扫描来完成注册;而Eventbus 3.x 使用的是 编译时注解,在编译的时候,就会将相应操作编译成 .class 文件,在编译时就进行操作这样运行时的速度就会快很多。

RxBus 是基于RxJava 衍生而来的,只要引入了 RxJava 和 R小Android 就能很方便地使用 RxBus ,它的实现很有意思,采用静态内部类的单例,由于内部静态类只会被加载一次,所以实现方式是线程安全的(可以与volatile + double check 对比着看):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RxBus {
private final Subject bus;

public RxBus() {
bus = new SerializedSubject<>(PublishSubject.create());
}

public static RxBus getInstance() {
return RxBusHolder.mInstance;
}

static class RxBusHolder {
private static RxBus mInstance = new RxBus();
}
}

组件化事件总线考量

通信事件都要放到公共的Base模块中,Base模块也需要依赖于事件总线框架,信息组件都都需要放在Base模块中,我们看一下总线传递的流程:

总线传递流程

组件化要求功能模块独立,应该尽量少影响 App module 和 Base module ,其中 Base module 尽量做得通用,不受其他模块影响。而如果上述事件总线放在 Base module 中,每个模块增删时,都需要添加或者删除Base module 的事件,而增删事件会让其他代码索引到这些事件的代码时造成错误,这样会破坏组件化设计的规则。

这就是目前组件化通信会遇到的瓶颈。两种比较适合现阶段组件化通信的方式:

  • ModuleBus: 能传递一些基础类型数据,不需要在 Base module 添加额外的类,所以不会影响 Base模块的架构,但也无法动态移除信息接收端代码,而自定义的事件信息模型还是需要添加到 Base module
  • 组件化架构的 ModularizationArchitecture 库。每个功能模块都需要使用注解建立 Action 时间,每个 Action 完成一个事件动作,没有用到反射,参数通过 HashMap<Stirng,String>传递,无法传递对象

但是,如果一定要使用 EventBus 或者 RxBus事件总线,这里提供一种架构方案,最大限度地解耦:

EventBus正确使用使用

其中xxBus独立为一个module ,Base module 依赖 xxBus 对事件通信的解耦,抽离事件到 xxBus 事件总线模块,以后添加事件的Event 的实体都需要在上面创建。

组件间跳转

在组件化中,两个功能模块是不存在直接依赖的,其依赖规则是通过 Base module 间接依赖的

Activity 跳转,直观的跳转就是startActivity 发送一个包装好的 intent去实现。但是如果Activity 在其他 moudle ,则无法索引到 Activity 类,这时候我们会很自然想到使用 intent 包装隐式 Action 实现。隐式跳转的方法:

1
2
3
4
5
6
7
8
9
//方法1 通过AndroidManifest中声明的action来启动
Intent intent = new Intent("material.com.settings");
startActivity(intent);

//方法2 包名 + 类名的方式
Intent intent = new Intent();
intent.setClassName("模块包名", "Activity路径");
intent.setComponent(new ComponentName("模块包名","Activity路径"));
startActivity(intent);

但是第2种方式会产生崩溃,提示Activity并没有在AndroidManifest中注册,可是明明注册了啊?这里真正需要理解的是, setClassName 和 setComponent 函数第一个参数的真正含义,它们是 App 的包名而不是所在的 module 的包名 !这在第1章的时候已经聊过了,当最终合成 AndroidManifest 后,module的包名压根就不存在了有空需要自己去验证下)。

由于隐式跳转有可能找不到目标Activity 而导致崩溃,所以,我们应该首先判断intent是否能够正常跳转。

此外,还有对安全问题的考虑,因为其他App也能通过隐式的 Intent 来启动 Activity (隐式跳转是原生的,作用范围是整个Android系统),为了确保只有自己的 App 能启动组件,需要设置 exported = false。

ARouter 路由跳转

ARouter 使用 AOP 切面编程可以进行控制跳转的过滤。在 Application 中进行 init 之后,我们就对跳转目标做处理:

1
2
3
4
5
6
7
8
9
10
11
12
//声明
@Route(path="gank_web/1")
public class WebActivity extends BaseActivity {}

//使用
ARouter.getInstance().build("gank_web/1")
.withString("url",url)
.navigation();

//获取传递的参数
Intent intent = getIntent();
String url = intent.getStringExtra("url");

整一个过程还是非常优雅的。

组件化最佳路由

既然已经存在隐式跳转,为什么我们还要选择路由呢?在组件化架构中,假如移除一些功能 module 和跳转关系,则跳转无法成功,此时,如果要做一些提示,将迁入更多的判断机制代码。而使用路由机制,可以统一对这些索引不到的 module 页面进行提前拦截和做出提示。

路由除了跳转,另一个重要作用就是拦截,比如可以在跳转前进行登录状态验证,路由表的引入,也不需要在 AndroidManifest 中声明隐式跳转。

路由的选择:现在开源软件中有不少路由结构,比如: ActivityRouter、天猫统跳协议、ARouter、DeepLinkDispatch、OkDeepLink 等。如果你的项目没有引入 RxJava,那么 ARouter 的介入成本低,是首选;如果接入了,那 OkDeepLink 可以 兼容 RxJava ,可以做考虑。当然,OkDeepLink 会在 Intent 中加入 FLAG_ACTIVITY_NEW_TASK 标识,那么创建每个Activity 都会创建新的任务栈来装载 !这样无法做到标准的压栈。

空类索引

如果不想使用第三方路由,可以采用空类索引的方式实现跳转,它的原理就是:使用空类来欺骗编译。具体的步骤为(个人理解,不一定对):

  1. 在各个 module 编写好,但还没实现跨 module 跳转之前
  2. 将所有 module 打包,就会生成 apk 了,只是还暂时不能跳转而已
  3. 通过某种手段(人工或者工具)解压 apk ,并读取 AndroidManifest.xml 文件中的四大组件信息,生成一个 jar 包
  4. 这个 jar 包里面的内容是空的四大组件,比如,AndroidManifest 中有个 com.example.ActivityA ,那么在 jar 包中也会声明(同路径、同类名)一个 com.example.ActivityA,并且继承 Activity
  5. 之后,将这个 jar 包以 provided 形式(只是引入,不会编译进去)到各个 module 中
  6. 此后,各个 module 便可以直接以普通的显式 Intent 来 startActivity 了!

动态创建

动态创建也是为了解耦

动态创建 Fragment

如果在单Activity + 多Fragment情景中,可以使用动态创建 Fragment (反射的方式),之后添加到 ViewPager 里面,这种方式用于模块间的解耦是非常合适的。因为普通方式使用 Fragment 的话,需要强引用 Fragment,而这些Fragment 可能不在当前使用的 module 中,而且因为ARouter 也支持这种 Fragment 方式,所以直接使用 ARouter 是不错的。当然,由于我们是通过反射方式创建实例的,因此需要防止Fragment所在的 module 被移除之后产生 Exception,并且反射也会消耗性能。

动态配置Application

前面介绍了 Application 产生的替换原则。如果某些功能模块需要做一些初始化操作,则只能强引用到主 module 的 Application 中,是否有方法可以降低耦合呢?

第一种方案:通过主 module 获取各个 module 的初始化文件,然后通过反射初始化的 Java 文件来调用初始化方法:

  1. Base module 中定义接口 BaseAppInit,里面有 init() 方法

    1
    2
    3
    public interface BaseAppInit {
    boolean init(Application app)
    }
  2. 在 module 中使用 BaseAppInit ,实现它:

    1
    2
    3
    4
    5
    6
    7
    public class NewsInit implements BaseAppInit {
    @Override
    public boolean init(Application app) {
    //do something
    return false;
    }
    }
  3. 在 PageConfig 中添加配置

    1
    2
    3
    4
    5
    private static final String NEWS_INIT = "material.com.news.api.NewsInit";

    public static String[] initModules = {
    NEWS_INIT
    };
  4. 在主 module 的 Application 中实现初始化方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public void initModules() {
    for (String init: PageConfig.initModules) {
    try {
    Class<?> clazz = Class.forName(init);
    BaseAppInit moduleInit = (BaseAppInit)clazz.newInstance();
    moduleInit.init(this);
    } catch (Exception e) {
    e.printStack();
    }
    }
    }
  5. 在 Application的 onCreate 中调用 上述的 initModules 方法即可

第二种方案:在 Base module 中创建 BaseApplication ,之后主 Module 中的 Application 继承 BaseApplication 即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//在 Base Module 中创建
public class BaseAppLogic {
protected BaseApplication mApplication;

public void setApplication(BaseApplication application){
mApplication = application;
}

public void onCreate() {}
public void onLowMemory() {}
...//其余一些 Application 中重要的回调
}


//Base Module 中的 BaseApplication
public abstract class BaseApplication extends Application {

@Override
public void onCreate() {
initLogic();
logicCreate();
}

@Override
public void onLowMemory(){
logicLowMemory();
}

//供主module 调用
protected abstract void logicCreate();

//供主module 调用
protected abstract void logicLowMemory();


}

唉,第二次了还是没看懂这个方案,先不写了(以上内容代码没写完的)。我个人觉得可以参考 ARouter 的思路,给子 module 自定义的 Application 加编译时注解,在编译的时候把这些 Application 找出来,生成新的类,然后就可以反射调用子module的 Application 方法了。

数据存储

存储方式主要5种,网络存储、File I/O 、SQLite、ContentProvider、SharedPreference,根据 安全、效率、量级 三个维度去决定使用哪种方式:

数据存储考虑维度

组件化存储

Android 原生的存储体系是全局的,在组件化开发中,五中原生的存储方式是完全通用的。文中介绍了 greeDAO 这个关系映射的数据库框架,greeDAO 是目前众多ORM(对象关系映射)数据库中最稳定、速度最快、编写体验最好的框架,并且支持数据加密,RxJava,它能通过对象的方式去操作关系型数据库,但是它的底层还是 SQLite ,它的原理是将一个实体对象转换成意向数据,然后保存到SQLite。也正因为基于SQLite ,所以不能存储图片这样的大文件。关于数据库在组件化的应用,实体类放在本身的module 是无法传递的,需要放在一个统一的 module 中来管理这些类的产生和引用,其greenDao 需要在 Base module 中引入,编写时注解生成的对象也应该在 Base module 中,这样全部的模块才能引用到这个数据:

关系型数据库简单架构

当然,更好的设计,文中也建议与事件总线一样,如下图:

关系型数据库架构

权限管理

组件化权限

通过查看 AndroidManifest 文件,可以看到各个 module 中的权限申请,最终会被合并到完整 AndroidManifest 中。这时候,我们有两种权限放置方案:

  1. 将 normal 级别的权限申请都放到 Base module 中,然后在各个 module 中分别申请 dangerous 权限,这样分配的好处在于:当添加或者移除一个模块时,隐私权限的申请也跟随移除,做到最大限度地解耦
  2. 还有人提议,将权限全部转交给每个 module 中,达到最大程度的解耦,这样做的缺点在于:会增加AndroidManifest 的合并检测的耗时

当项目需要适配到 Android 6.0 以上的动态权限申请时,需要在 Base module 中添加自己封装的一套权限申请工具,其他组件层的 module 都能使用这套工具,书中推荐选择 AndPermission 。

动态权限框架

AndPermission 使用简单,并且最大程度适配国内各大厂商的 ROM。

路由拦截

当调用其他模块的功能时,就是路由拦截器起作用的时候了,将路由拦截器和权限申请结合在一起,前面介绍的 ARouter 是跳转钱是会遍历 Interceptor 的,因此我们可以设置拦截器来实现:

1
2
3
4
5
6
7
8
9
10
public class SettingsIntercptor implements IInterceptor {
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
//通过 postcard 中的跳转地址过滤
if (postcard.getPath.equals("/gank_setting/1")) {
//权限申请处理

}
}
}

在上述过程可能涉及到弹出 Dialog ,那么我们首先要获取当前的 Activity 。我们可以通过在 Base module 中的 BaseApplication 中做 registerActivityLifecycle 来保存栈顶的 Activity,有两种方案:

  1. 在onCreate 的时候持有这个 Activity ,但是这可能引起内存泄漏
  2. 我们在 resume 的时候持有 这个activity ,由于 下一个 Activity 的 resume 肯定比当前 Activity 的 destroy 要先执行,所以可以肯定静态持有栈顶 Activity 不会导致内存泄漏(细品就知道了)。

静态常量

在 Application module 中查看 R.java 文件:

1
2
3
4
5
6
7
public final class R {
public static final class anim {
public static final int abc_fade_in = 0x7f050000;
public static final int abc_fade_out = 0x7f050001;
public static final int abc_fade_from_bottom = 0x7f050002;
}
}

但是,在 Lib module 中查看 R.java 文件:

1
2
3
4
5
6
7
public final class R {
public static final class anim {
public static int abc_fade_in = 0x7f050000;
public static int abc_fade_out = 0x7f050001;
public static int abc_fade_from_bottom = 0x7f050002;
}
}

仔细观察会发现,在 Lib module 中的静态变量没有被赋予 final 属性。在第1章提及,各个 module 会生成 aar 文件,并且被引用到 Application module 中,最终合并成 apk 文件,当各个次级 module 在 Application module 中被解压后,在编译时 R.java 会被重新解压到 build/generated/source/r/debug(release)/包名/R.java 中。

合并后的 R.java 中 的id 属性会被添加 final 修饰符。(这是我自己理解的,需要验证

组件化的静态变量

在 Lib module 中,R.java 文件没有了 final 关键字会导致什么问题呢?

这就会导致凡是规定必须采用常量的地方都无法直接使用 R.java 中的变量,包括 switch-case 和注解。为此,我们只能抛弃 switch-case 而只能使用 if-else 来实现,if 里面不需要常量。

这个知识点是自己加的>)不同的 module 之间无法保证 R.java 中变量对应的数值不同,但是我们可以保证 R.java 的值不同,为了避免R.java 中资源的冲突,不同的 module 中,我们资源命名最好加前缀加以区分,比如登录module ,资源以 login_ 开头,比如社区module ,资源可以以 comm_ 开头等等。

R2.java 的秘密

ButterKnife ,一个专注于 Android View 的注入框架,可以大量减少 findviewById 和 setOnClickListener 操作的第三方库。当使用注入View 绑定时:

1
2
@BindView(R2.id.submit)
public Toolbar mToolbar;

编译成 class 后,R的值会替换为常量:

1
2
@BindView(2131492966)
public Toolbar mToolbar;

注解中只能使用常量,如果不是常量就会报错,那么 ButterKnife 是如何解决的呢?它的原理是使用替换的方法,将 R.java 文件复制一份,命名为 R2.java ,然后给 R2.java 的变量加上 final 修饰符,在相关的地方直接使用 R2 资源!这一点,可以从 ButterKnife Gradle 中的 ButterKnifePlugin 源码中找到答案,源码略。最终生成的 R2 与 R 文件在同一个目录。

当然,ButterKnife 中处理后代码还是会用 findViewById ,用的是 R 的,而不是 R2, 但是 onClick 的时候,用的还是 R2,因为我们 view.getId() 返回的是 R 中的id ,而 R2 是 R 的副本,所以是一致的,不会有问题。

值得注意的是,library 使用 R2 的方式时,会出现 library 和 Application 切换 R 文件资源的引用问题,这里全部使用 R2 的方式生成引用资源 id,则不会出现此问题。

资源冲突

组件化的资源汇合

全部功能都依赖 Base module ,但是 Application module 最终还是得将功能 module 的 aar 文件汇总后,才能开始编译,那会不会出现多个 Base module 呢,不会,我们可以通过 gradle 命令查看 module 的依赖树:

./gradlew module_name: dependencies

则会展示依赖树,有些传递依赖标记了 * ,表示这个依赖被忽略了,因为有其他定级依赖中也依赖了这个传递的依赖。

AndroidManifest 冲突问题

前面说了,AndroidManifest 中 Application 的 app:name 冲突时,需要使用 “tools:replace=android:name” 声明可替换

包冲突

包冲突可以先检查依赖报告,用以下命令查看依赖目录树:

./gradlew module_name: dependencies

有冲突可以使用 exclude 解决:

1
2
3
compile('com.facebook.fresco:fresco:0.10.0') {
exclude group: 'com.android.support', module:'support-v4'
}

资源名冲突

因为无法保证不同的module 中资源名称不同,那么Gradle 就会合并相同命名的资源,并且后编译的模块会覆盖之前编译的模块中的资源字段中的内容。所以,一般在一开始命名的时候,不同的 module 加上不同的前缀即可解决。只能一点可以采用 gradle 命名提示机制,resourcePrefix字段:

1
2
3
android {
resourcePrefix "组件名_"
}

组件化混淆

混淆基础

混淆包括了代码压缩,代码混淆以及资源压缩等优化过程。AS 中的 ProGuard 是一个压缩、优化和混淆Java 字节码的工具,可以删除无用类、字段、方法和属性,还可以删除无用注释,最大限度优化字节码文件。

不能混淆的情况有以下:

  • 反射中使用的元素
  • 最好不让一些Bean 对象混淆
  • 四大组件要在AndroidManifest中声明,混淆后类名发生改变,因此不要混淆
  • 注解不要混淆,注解一般要用到反射
  • 不能混淆枚举红的 value 和 valueOf ,因为这两个方法是静态添加到代码中运行的,也会被反射使用
  • JNI 调用 java 的方法,需要通过类名和方法名构成地址形成
  • java 使用 Native 方法,Native 是C/C++ 编写的,方法是无法一同混淆的
  • JS 调用 java的方法(-keepattributes *JavascriptInterface*)
  • webview中 Javascript 的调用方法不能混淆
  • 第三方库建议使用自身混淆规则
  • Parcelable 的子类和 Creator 的静态成员变量不混淆

资源混淆

proguard 可以混淆代码,其实资源名也是能混淆的,混淆后变为 R.string.a 之类的,它有3种方案:

资源混淆方案

书中推荐使用 微信的 AndResGuard 混淆机制,它的工作流程如下:

资源混淆方案

组件化混淆

重点是保证只混淆一次:

  • 第一种方案:只在 Application module 中设置混淆,其他module 都关闭混淆,所有的规则都放在 Application 的module 中

    缺点:当某些模块移除之后,需要手动移除混淆规则,虽然理论上混淆规则多了不会崩溃或者编译不过,但是会对编译效率造成影响

  • 第二种方案:命令将 所有的 module 中的 proguard-rule.pro 文件合成,然后覆盖 Application module 中的混淆文件

    有合成操作,也会影响编译效率

  • 第三种方案: 将 proguard-rule.pro 文件打进 aar

    Library module 自身拥有将 proguard-rule.pro 文件打包到 aar 中的设置,如添加一个 consumerProguardFiles 属性:

    1
    2
    3
    defaultConfig {
    consumerProguardFiles 'proguard-fules.pro'
    }

开源库中可以依赖此标志来指定库的混淆方式,consumerProguardFiles 属性会将 *.pro 文件打包进aar ,混淆时会自动使用次混淆配置文件。不过,以 consumerProguardFiles 形式添加 混淆文件具有以下特性:

  1. proguard.txt 文件会在aar文件中
  2. proguard 配置会在混淆时使用
  3. 此配置只针对aar
  4. 此配置只针对 库文件有效,对应用程序无效

当 Application module 将全部代码汇总混淆的时候, Library module 会被打包为 release.aar ,然后被引用汇总,通过 proguard.txt 规则各自混淆,保证只混淆一次。

第三种方案可以最大限度地解耦混淆解耦工作。

多渠道打包

可以使用几种方式打渠道包:

  • 使用Python 打包,推荐 AndroidMultiChannelBuildTool

  • 美团批量打包工具 Walle

    其原理是修改 V2 内容区,V2签名以一组 ID-value 的形式保存在这个区块中,可以自定义一组 ID-value 并写入到这个区域

  • 在apk文件后面添加 zip comment,推荐 packer-ng-plugin,它也提供了python 和 gradle 两种打包方式

    Apk 的本质是一个带签名信息的 zip 文件,符合zip文件的格式规范,不过 V2会校验包实际大小了,因此不能添加 comment 了

  • 使用官方的方式打包

    重点说下官方打包,包含有几个步骤:

    1. 在 AndroidManifest 文件中假如渠道区分标识,写入一个 meta 标签

      1
      <meta-data android:name="channel" android:value="${channel}"/>
  1. 在App目录的build.gradle 中配置 productFlavors

    1
    2
    3
    4
    5
    6
    7
    8
    9
    productFlavors {
    qihu360{}
    baidu {}
    //...省略其他渠道

    productFlavors.all {
    flavor -> flavor.manifestPlaceholders = [channel: name]
    }
    }
  2. 在 AS 的 Build -> Generate signed apk 中选择设置渠道

    当然如果要一次性打出全部的渠道,只需要 执行 .gradlew build 即可,就可以打出所有的 Release 和 Debug 包

多渠道模块设置

有个时候,我们的App可能要打包成 管理员端、普通用户端 ,等等此类需求是比较棘手的,不同的版本依赖的module不一样,这时候怎么弄呢?我们可以使用原生的 Gradle 来配置构建,下面演示一个用户版本和管理版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//下面的代码都在根目录的 build.gradle 中

productFlavors {
//用户版
client {
manifestPlaceholders = [
channel: "10086", //渠道号
verNum: "1", //版本号
app_name: "Gank" //App名
]
}

//服务版
server {
manifestPlaceholders = [
channel: "10087", //渠道号
verNum: "1", //版本号
app_name: "Gank服务版" //App名
]
}
}

dependencies {
clientCompile project(':settings') //引入客户版特定module
clientCompile project(':submit')//我怀疑书上打错了,应该这里只是 compile 吧?是客户版与服务版公用的?
serverCompile project(':server_settings') //引入服务版特定module
}

上面通过 productFlavors 属性设置多渠道,而 manifestPlaceholders 设置不同渠道中的不同属性,这些属性需要在 AndroidManifest 中声明才能使用在 dependencies 中通过设置 xxxCompile 来配置不同渠道需要引用的 module 文件。接下来,我们要在 App module 的 AndroidManifest 文件中声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
<application
android:allowBackup="true"
<!--app引用名-->
android:label="${app_name}"
<!--标记可替代-->
tools:replace="label">

<!--版本号声明-->
<meta-data android:name="verNum" android:value="${verNum}"/>

<!--渠道号声明-->
<meta-data android:name="channel" android:value="${channel}"/>
</application>

其中,android:label 属性用于更改签名,${xxx} 会自动引用 manifestPlaceholders 对应的 key 值, tools:replace 属性在以前的 Application 替代中提到,最后替换的属性名需要添加 tools:replace ,这里提示编译器需要替换的 label 属性。

声明 meta-data 用于某些额外自定义的属性,这些属性都可以通过代码读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//获取metadata的方法,当然,要防止异常,这里省略了
public static Object getMetaData(Context context, String metaName){
String pkgName = context.getPackageName();
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(pkgName, PackageManager.GET_META_DATA);
Object obj = appInfo.metaData.get(metaName);

return obj;
}


//获取 channel
public static int getChannelNum (Context context){
Object obj = getMetaData(context);
return (int)obj;
}

//之后,在路由跳转的时候,可以根据 channel 来做跳转拦截,代码略

以上shi值调用,至于需要某个类调用,则可以直接将路径以值的形式来传递(同样在meta-data中),然后解析处meta-data,最后用反射方式就能完成对象的创建,之后就能调用了,代码就略了。

你知道组件化吗

在项目开发中,一般将公用的代码提取出来用于制作基础库 Base Module ,将某些单独的功能封装到 Library module 中,根据业务来划分 module,组内每个人分别开发各自的模块,如下图所示:

项目原始架构

但是,随着时间推移,扩展了一些业务模块之后,模块间互相调用的情况就越来越多,结构可能就会很混乱了,并且,耦合可能会非常严重。这时候,新的规划规则出现了,这就是组件化、模块化 以及 插件化

  • 组件:指的是单一功能的组件,如视频组件(VideoSDK)、支付组件(PaySDK)等,每个组件都能单独抽出来制作成SDK
  • 模块:指的是独立的业务模块,如首页模块(HomeModule)、直播模块(LiveModule)等,模块相对组件来说粒度更大,它可能包含多种不同的组件

组件化和模块化都是为了代码重用和业务解耦,区别在于模块化是业务导向,组件化是功能导向。模块化和组件化的缺点在于:旧项目需要重新适配组件化。当项目的越来越大,方法数可能就会超过 65535,此时有两种选择: MultiDex 分包解决以及 插件化方法解决

基础组件化架构

我们用一个非常基础的组件化架构图来解释组件化基础:

基础的组件化架构

其中,基础层包含一些基础库和对基础库的封装(如图片加载、网络加载、数据存储等)、组件层包含一些简单的业务(如登录、图片浏览等)、应用层用于统筹全部组件,并输出生成App。虽然这个架构简陋,但是已经包含了组件化的内涵。

依赖

通过Android Studio ,我们有3中基本的依赖,如下代码:

1
2
3
4
5
dependencies {
compile fileTree(include: ['*.jar,*.aar'], dir: 'libs')
compile project(':base')
annotationProcessor 'com.alibba:arouter-compiler:1.1.1'
}

这 3 种依赖分别是(我自己给命名的) jar dependency、module依赖、仓库索引依赖。需要注意的是:

  • 读入自身目录使用的是 : fileTree 字段
  • 读入其他资源 module 使用的是: project 字段,而 “:base”中冒号表示文件目录与自己相同层级

聚合和解耦

最好能考虑插拔,不要移除某个业务项目多数模块就不能工作了。聚合和解耦是项目架构的基础

重新认识 AndroidManifest

manifest 字面意思就是货单、旅客名单,AndroidManifest 就是 Android 项目的声明配置文件。我们知道,这里面放的是 Android四大组件以及 自定义的 Application

如果项目中有多个 module,每个moudle都配有一份 manifest ,那么最终生成一个 App 时只会存在一个 AndroidManifest,因为这些 Androidmanifest 会合并,解决冲突后成一份。我们能在: app/build/intermediates/manifests/full/debug/Androidmanifest.xml 中找到这个最终合成的 AndroidManifest 文件。intermediates 目录是用来包含 App 生成过程中产生的“中间文件”

AndroidManifest属性汇总

当编译主 module 时,会将那些功能 module 重新编译,然后将成果(aar) 放到主 module 的 intermediates 目录中。

子module成果放入主module

可以看到,会在 app/build/intermediates/exploded-aar 中引用其最终生成的 aar 文件,exploded-aar 还包含了其他第三方仓库引用到的库

aar 文件解压能看到目录中一般包含 aidl、assets 等资源, classes.jar 是每个module 的真正代码,res包含功能 module 的资源,而每个 module 都有自己的 AndroidManifest ,即使 module 没有四大组件,在 AndroidManifest 中也依然带有 application 标识,甚至还会帮我们补全 use-sdk 信息。

AndroidManifest 属性变更

module 的 manifest 文件在合入的时候,最主要的差别在于,activity 声明的时候,name 属性不再是缩进,而是完整地址,当然了,剩下的其他四大组件也会一样,为什么会这样呢?因为 AndroidManifest 会引用多个 module 中的文件,需要知道具体路径,不然在编译器打包时招不到具体路径。如下所示:

1
2
3
4
5
<!--在module中写法-->
<activity android:name=".MainActivity"/>

<!--合并后-->
<activity android:name="com.example.MainActivity"/>

此外,还会补全一些属性,比如 icon 和 theme (如果没有在编写中指定)。权限最终也会被合并。当然也会有 Application 的合并(如果module中也注册了Application 的话)。

多个module中声明的相同权限,最终只会有一份,关于主题的声明,Activity 的主题都会引用自身module 声明的 主题,不声明当然就是使用默认主题了,主moudle 中声明 的 theme 将默认为整个 App 的 UI 主题风格。

注册Application

关于shareUid(题目自己加的)

通过声明 Shared User id,拥有同一个User id 的多个 App 可以配置成运行正在同一个进程中,所以默认可以互相访问任意数据。需要注意的是 如果只是在功能module 中声明 shareUid,那么最终并不会被何如最终的 AndroidManifest 中,只有主 module 的声明的 sharedUserId 才会最终打包到 AndroidManifest

你所不知道的 Application

看一下 Application 中比较重要的方法:

  • onTerminate:当终止应用程序时回调,但是不保证一定调用,比如,当程序被内核终止以便为其他应用释放空间时,就不会有提醒和这个回调
  • onLowMemory: 当后台程序已经终止且此时资源还是匮乏时执行该回调,一般应该在这里释放一些不必要的资源
  • onConfigurationChanged:配置改变时触发,例如手机屏幕旋转
  • registerActivityLifecycleCallbacks() :用于监听Activity 的生命周期,可以利用方法获取在栈顶的 Activity 对象,用于弹dialog 等

组件化 Application

这个小节把“注册Application”的内容一起合并进来了。注,这里说的 Application 是指 AndroidManifest 中的。

如果主工程创建一个 Application,Library 里面也声明了,那么在merge 的时候可能会出错,因为如果 Library 中定义了与 主项目 相同的属性(如android:icon 和 android:theme),此时就合并失败,并且提示可以用 tools:replace=”android:name” 来声明Application 是可被替换的。例如,我们在 application 标签下添加:

Tools:replace=”android:icon, android:theme” (多个属性的话,使用 “,” 分开)

App 最终只会允许声明一个 Application 到 AndroidManifest 中,如果存在多个 Application 的情况可以参考以下 Application 的替换规则如下:

  • 主module有,功能module 没有,那就用主的
  • 其中一个功能module有,主module没有,则用功能module的
  • 如果功能module中有多个Application,那么在解决冲突后,最终载入到后编译的module中
  • 若主module 有,功能module也有,解决冲突后,最后编译的主 module 的 Application 会在 AndroidManifest 里面。

  • 优化:闪屏、极速版、viewStub、IdleHandler加载Webview、classes.dex只保留App启动所需要的代码、避开峰值,滑动时不加载图片。线程优化-采用线程池、不加载大的sp,监控方式:视频录制

  • 内存泄漏,比如 EventBus没有反注册、Activity被静态持有

  • 衡量指标: 快开慢开比:比如2秒快开比,5秒快开比;90% 用户启动时间。冷启动,从3.5秒左右降低到1秒左右,内存从经常性的 380M 左右降低到 330M 的水平,优化前88%左右,93%的收集数据显示1秒以内打开;主要集中在网络超时、网络无连接两种异常,其中网络超时占了40%左右

  • Serializable 的反序列默认是不会执行构造函数的
  • SQLite 默认支持多进程并发操作,它通过文件锁来控制多进程的并发,但是SQLite 的锁粒度并没有非常细,针对的是整个DB文件,简单来说,多进程可以同时获取 SHARED 锁来读取数据,但是只有一个进程可以获取 EXCLUSIVE 锁来写数据库
  • 网络请求: DNS解析(本地或服务查询)、连接(握手、TLS、慢启动、重定向)、发送数据包(延迟、丢包)、接收(IO、解析)、关闭
  • 网络性能监控:360的 ArgusAPM插桩技术、以及 TraceNetTrafficMonitor :使用Aspect 切面功能
  • 软件绘制使用的是 Skia 库,画笔:Skia或者OpenGL;画纸:Surface;画板:Graphic Buffer
  • 硬件绘制与软件绘制最核心的区别是硬件绘制通过GPU完成 Graphic Buffer内容的绘制
  • Surface 对应2个Buffer,一个前台的,一个后台的,Surface与Buffer之间通过 匿名共享内存交换数据的。可以采用 Tracer for OpenGL ES 逐帧分析性能
  • Android的多渠道打包其中的一种思路就是这样,在 apk 尾巴上追加几个字节,来标记apk的渠道,apk启动时,从apk尾巴上读取这个渠道值。不过,后来google发现了这个漏洞,在新版本系统中,系统安装apk时,会检查apk实际大小,二者不相等就会报错安装失败
  • UI渲染用 gfxinfo 监测性能,当然也可以用 Systrace,怎么操作?
  • SharedPreference 缺点:跨进程不安全、加载慢(异步加载,可能主线程会等待)、卡顿(onPause会强制写入磁盘)、Json转义
  • ContentProvider 的原理:通过 Binder 传输匿名共享内存的文件描述符给数据获取方(Client内部有一个 CursorWindow 对象,发送请求时,把这个CursorWindow 类型对象传过去,这个对象暂时为空,Server 收到请求,搜集数据,填充到这个CursorWindow 对象中,Client 读取内部这个CursorWindow 对象,获取数据)。适合传输大量数据,传小数据不一定划算
  • 静态代理就是对于每种情形都编写一个Proxy,在调用的时候,使用真实的target的来调用方法;动态代理就是根据系统提供的Proxy.newProxyInstance,它的原理是通过 Method.invok ,避免创建多个Proxy
  • 动态广播先于静态广播接收到
  • assets目录存放的文件,最终还是通过 getResources 去open打开,所以,资源文件都可以归结到 Resource 里面
  • 资源插件化解决:创建AssertManager对象,调用 addAssertPath 将所有的插件路径都添加进去,之后根据AssetManager对象创建 Resources 和 Theme 对象。 重写 Activity 的 getAsset、getResources 、和 getTheme 方法(第8、15章)。
  • 资源插件化问题解决:修改AAPT以及打包gradle文件,为插件资源指定不同前缀。插件使用宿主的资源,就固定id;宿主通过provider 以 aar形式引入到 插件项目中
  • 动态广播插件化:动态广播,只需要保证宿主APP能加载到动态广播类就行,所以,无序特别处理
  • Service、ContentProvider、广播这些通用方案都是本地站位,因为数量不多
  • ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。(事件分发机制)
  • getX 是获取事件到屏幕的距离;getRawX 是获取事件距离控件顶边的距离
  • View的滑动方法: layout、LayoutParams、属性动画、ObjectAnimator、offsetLeftAndRight、scrollTo、scrollBy、Scroller(computeScroll)
  • Zygote 启动SystemServer,后者再启动各种服务,比如 AMS、WMS
  • React 只关注MVC框架中的View层,为减少直接操作DOM,它使用虚拟DOM来差异化更新 DOM,实现数据单向流动。而传统的HTML 页面更新元素是全量更新
  • RN 的UI层变化, 就映射到 虚拟DOM,计算出diff后转成json发送给Native生成页面元素。编写的 RN 代码最终会打包成 main.bundle.js 文件供App 加载,此文件可以存在App本地或者服务器上更新。我个人的理解是,生成虚拟DOM之后的.js文件发到Android或者ios中,由本地进行差异对比生成json,本地再根据json更改view。
  • B想从A中获取数据,然后呢,它就mmap 一块内存,这块内存对应到物理内存上,但是Binder驱动在内核空间中也指向一块虚拟内存K,这块内存与B那块内存在物理上是一样的。这时候,只需要A将通过 copy_from_user 将数据复制到 K 中,即完成了数据的一次复制。同步回去博客
  • 直接内存,NIO : 在Native 分配堆外内存,在Java对中通过 DirectBuffer 作为引用
  • GC Root: 虚拟机栈引用、本地方法栈引用、方法区静态、方法区中常量
  • 类加载过程: 加载、校验、准备、解析(将常量池的符号引用搞成直接引用)、初始化、使用、卸载
  • 锁优化:自旋、锁消除、锁粗化、轻量级锁、偏向锁
  • 轻量级锁,使用CAS操作尝试将对象的 MarkWord 改成轻量级锁标志,如果成功,则拥有锁;否则,判断锁是否指向当前线程,如果是,则直接进入。否则,就等待(个人觉得应该是CAS重试)。如果有两个以上线程在争用这个锁,就升级为重量级锁,后面的线程阻塞。
  • 偏向锁:偏向锁在轻量级锁基础上连 CAS 都不做。在对象第一次被线程获取后,把线程id写在 MarkWord 中(这个当然是CAS操作),当另一个线程尝试获取的时候,偏向模式就结束(指的是这个CAS操作不做了),恢复到未锁定或者轻量级锁状态,后续就是轻量级锁规则了
  • 拥塞控制方法: 慢启动、拥塞避免(增速慢,不再翻番)、快速恢复(拥塞窗口减半,再做拥塞避免)
  • TCP慢可能的原因:握手、捎带确认算法、拥塞控制算法
  • 数字签名就是加了密的摘要。证书包含的内容: 证书格式、对象名称、对象的公钥、有效期、颁发者、签名算法等
  • lateinit 主要用于 var 声明的变量,然而它不能用于基本数据类型,如 Int、Long 等,我们需要使用Integet这种包装类作为替代
  • Data class 的特点是: 默认给写了getter、setter、equals、copy(浅拷贝)、toString 等方法
  • Kotlin 中的 Int 类型等同于 int
  • Kotlin 中的 Int? 等同于 Integer !
  • 一般来说,Kotlin 属于 无栈协程,它依靠对协程体本身编译生成的状态机的状态流转实现,变量的保存也是通过闭包语法实现
  • adb shell -》 run as -》 cd /data/data/app_webview,非Root情况下 获取webview的Cookies ,使用 sqlite3打开即可
  • MeasureSpec 的含义是:父View传递给当前 View 的一个建议值。UNSPECIFIED 父不约束子,子View可以取任意尺寸。Exactly(match_parent、具体数值): 父为子指定特定的尺寸,子必须在这个尺寸之内 ;AT_MOST(wrap_content):父为子指定一个最大的尺寸,子要在这里面
  • 当子具体数值时:EXACTLY,大小为数值;当子为 match_parent: 为父容器的mode,父的剩余空间;当子为wrap_content: mode 为 AT_MOST ,不超过父容器
  • 内部拦截手势冲突时,在setOntouchListener 中 disallowParent设置为 true,在合适的时候,再设置为false。
  • 在WebviewClient 的 shouldInterceptRequest ,自行重新组装 WebResourceResponse
  • 安全努力:app签名校验、https防抓包、本地广播、sp加密
  • 自己写AIDL,会生成Stub 接口,在用于数据传递的Service (在主进程)中 自己写 binder实现 Stub 接口,实现其中的方法,在onBind 方法中返回这个 binder 。将数据传给webview: webview.loadUrl(“javascript:”)
  • handler处理流程:首先判断msg.callback 、其次是Handler的mCallback,最后才是交给 Handler 的 handleMessage 处理
  • epoll: 以前是将文件描述符数组发给内核,内核遍历;现在是 内核保存了一份文件描述符,只需要穿进去改动的部分。以前内核轮询来发现是否可以读写,现在是通过异步IO事件唤醒而不是轮询;最后,现在系统是仅将有IO事件的文件描述符返回给用户。
  • ViewRootImpl 的 scheduleTraversals ,会往MessageQueue 中插入同步屏障 msg,之后在Vsync的时候,就会发送异步消息。之后在unscheduleTraversals中移除
  • ANR: 输入5s,广播:前台10,后台60;服务:前台20,后台200;ContentProvider 的publish 超时 10s
  • 内存分配流程: 分配-> GC无soft -> 增长到最大 -> GC 有 soft ,增长大最大 -> OOM
  • 挂堆、标记、放堆、挂起线程、

想让消息提前执行?

可重入的原理

启动模式兼容?无论singleTop还是singleTask,再回到这个Activity时,并不会触发它的onCreate,而是会触发它的onNewIntent(其实这里说的bug我并没有明白,等测试后再说)。为此,我们需要在MockClass2 中,拦截onNewIntent方法,把占位 StubActivity 替换回插件Activity,代码如下:

写个自定义容器,在里面横着放子View,当一行放不下一个View的时候,自动换行

RenderThread?

Binder原理?两种签名方式?

systrace + 函数插桩怎么操作

HardwarLayout、SoftWareLayout

RN布局常用的方法

Android 中 js 桥的原理