Android小知识点笔记

FP函数式编程

函数式编程(functional programming),是一种编程范式,它将计算机运算视为函数运算 ,并且避免使用程序状态以及不可变对象。

即一种把程序看作数学方程式的编程风格,并避免可变状态和副作用

FP核心思想强调:

  1. 声明式代码 —— 程序员应该关心是什么,让编译器和运行环境去关心怎样做。

  2. 明确性 —— 代码应该尽可能的明显。尤其是要隔离副作用避免意外。要明确定义数据流和错误处理,要避免 GOTO 语句和异常,因为它们会将应用置于意外的状态。

  3. 并发 —— 因为纯函数的概念,大多数函数式代码默认都是并行的。由于CPU运行速度没有像以前那样逐年加快((详见 摩尔定律)), 普遍看来这个特点导致函数式编程渐受欢迎。以及我们也必须利用多核架构的优点,让代码尽量的可并行。

  4. 高阶函数 —— 和其他的基本语言元素一样,函数是一等公民。你可以像使用 string 和 int 一样的去传递函数。

  5. 不变性 —— 变量一经初始化将不能修改。一经创建,永不改变。如果需要改变,需要创建新的。这是明确性和避免副作用之外的另一方面。如果你知道一个变量不能改变,当你使用时会对它的状态更有信心。

声明式、明确性和可并发的代码,难道不是更易推导以及从设计上就避免了意外吗?不可变性和纯粹性是帮助我们写出安全的并发代码的强力组合。

参考链接:https://blog.csdn.net/u010144805/article/details/81416355

λ表达式

Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中)。

lambda 表达式的语法格式如下:

(parameters) -> expression

(parameters) ->{ statements; }

以下是lambda表达式的重要特征:

  • 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
  • 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号。
  • 可选的大括号:如果主体包含了一个语句,就不需要使用大括号。
  • 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定明表达式返回了一个数值。

λ表达式主要用于精简广泛使用的内部匿名类,各种回调,比如事件响应器、传入Thread类的Runnable等。Java8有一个短期目标和一个长期目标。短期目标是:配合“集合类批处理操作”的内部迭代和并行处理;长期目标是将Java向函数式编程语言这个方向引导(并不是要完全变成一门函数式编程语言,只是让它有更多的函数式编程语言的特性)。

Java8为集合类引入了另一个重要概念:流(stream)。一个流通常以一个集合类实例为其数据源,然后在其上定义各种操作。流的API设计使用了管道(pipelines)模式。对流的一次操作会返回另一个流。你可能会觉得List 被迭代了好多次,map,filter,distinct都分别是一次循环,效率会不好。实际并非如此。这些返回另一个Stream的方法都是“懒(lazy)”的,而最后返回最终结果的方法则是“急(eager)”的。在遇到eager方法之前,lazy的方法不会执行。

内部类总是持有一个其外部类对象的引用。而λ表达式呢,除非在它内部用到了其外部类(包围类)对象的方法或者成员,否则它就不持有这个对象的引用。在Java8以前,如果要在内部类访问外部对象的一个本地变量,那么这个变量必须声明为final才行。在Java8中,这种限制被去掉了,代之以一个新的概念,“effectively final”。它的意思是你可以声明为final,也可以不声明final但是按照final来用,也就是一次赋值永不改变。

任何一个λ表达式都可以代表某个函数接口的唯一方法的匿名描述符。我们也可以使用某个类的某个具体方法来代表这个描述符,叫做方法引用。

匿名内部类

外部类创建匿名内部类后,有可能匿名内部类还在使用,而外部类实例(或者创建内部类的方法)已经被回收了。如果此时匿名内部类用到了外部类的成员变量,那么就会出现匿名内部类要去访问一个不存在的变量的这种荒唐情况,为了延长局部变量的生命周期,于是匿名内部类使用的局部变量会被复制一份,从而使得局部变量的生命周期看起来变长了。但是这样又会引出另一个问题:数据一致性的问题!为了保证局部变量和 内部类中复制品 的数据一致性,于是要求内部类使用的局部变量是final的

String & StringBuffer & StringBuilder

1、长度是否可变
String 是被 final 修饰的,他的长度是不可变的,就算调用 String 的concat 方法,那也是把字符串拼接起来并重新创建一个对象,把拼接后的 String 的值赋给新创建的对象
StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象,StringBuffer 与 StringBuilder 中的方法和功能完全是等价的。调用StringBuffer 的 append 方法,来改变 StringBuffer 的长度,并且,相比较于 StringBuffer,String 一旦发生长度变化,是非常耗费内存的!

2、执行效率
三者在执行速度方面的比较:StringBuilder > StringBuffer > String

3、应用场景
如果要操作少量的数据用 = String
单线程操作字符串缓冲区 下操作大量数据 = StringBuilder
多线程操作字符串缓冲区 下操作大量数据 = StringBuffer

StringBuffer 中的方法大都采用了 synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是线程不安全的。

Java中Vector和ArrayList的区别

首先看这两类都实现List接口,而List接口一共有三个实现类,分别是ArrayList、Vector和LinkedList。List用于存放多个元素,能够维护元素的次序,并且允许元素的重复。3个具体实现类的相关区别如下:

  1. ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要讲已经有数组的数据复制到新的存储空间中。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。
  2. Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。
  3. LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

View绘制与事件

绘制

onMeasure()、onLayout():
measure是测量的意思,那么onMeasure()方法顾名思义就是用于测量视图的大小的。View系统的绘制流程会从ViewRoot的performTraversals()方法中开始,在其内部调用View的measure()方法。
getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

onDraw()步骤:
1.第一步是从第9行代码开始的,这一步的作用是对视图的背景进行绘制
2.第三步是在第34行执行的,这一步的作用是对视图的内容进行绘制
3.第四步的作用是对当前视图的所有子视图进行绘制
4.第六步的作用是对视图的滚动条进行绘制。其实不管是Button也好,TextView也好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。
部分源代码如下:

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
public void draw(Canvas canvas) {
if (ViewDebug.TRACE_HIERARCHY) {
ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
}
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
final Drawable background = mBGDrawable;
if (background != null) {
final int scrollX = mScrollX;
final int scrollY = mScrollY;
if (mBackgroundSizeChanged) {
background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
mBackgroundSizeChanged = false;
}
if ((scrollX | scrollY) == 0) {
background.draw(canvas);
} else {
canvas.translate(scrollX, scrollY);
background.draw(canvas);
canvas.translate(-scrollX, -scrollY);
}
}
}
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
// we're done...
return;
}
}

重绘

调用视图的setVisibility()、setEnabled()、setSelected()等方法时都会导致视图重绘,而如果我们想要手动地强制让视图进行重绘,可以调用invalidate()方法来实现。当然了,setVisibility()、setEnabled()、setSelected()等方法的内部其实也是通过调用invalidate()方法来实现的。

invalidate()方法中,当ViewParent不等于空的时候就会一直循环下去。在这个while循环当中会不断地获取当前布局的父布局,并调用它的invalidateChildInParent()方法,在ViewGroup的invalidateChildInParent()方法中主要是来计算需要重绘的矩形区域,当循环到最外层的根布局后,就会调用ViewRoot的invalidateChildInParent()方法了, 最终会调用到performTraversals()方法。
invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measure和layout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()了。

参考:https://blog.csdn.net/guolin_blog/article/details/17045157

事件传递

Android中触摸事件传递过程中最重要的是dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()方法。这里记录一下他们的处理过程,以供记忆:
1.dispatchTouchEvent是处理触摸事件分发,事件(多数情况)是从Activity的dispatchTouchEvent开始的。执行super.dispatchTouchEvent(ev),事件向下分发。dispatchTouchEvent()返回true,后续事件(ACTION_MOVE、ACTION_UP)会再传递,如果返回false,dispatchTouchEvent()就接收不到ACTION_UP、ACTION_MOVE。
2.onInterceptTouchEvent是ViewGroup提供的方法,默认返回false,返回true表示拦截。
3.onTouchEvent是View中提供的方法,ViewGroup也有这个方法,view中不提供onInterceptTouchEvent。view中默认返回true,表示消费了这个事件。

事件传递流程如下:
1.ACTION_DOWN首先会传递到onInterceptTouchEvent()方法
2.如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return false,那么后续的move, up等事件将继续会先传递给该ViewGroup,之后才和down事件一样传递给最终的目标view的onTouchEvent()处理。
3.如果该ViewGroup的onInterceptTouchEvent()在接收到down事件处理完成之后return true,那么后续的move, up等事件将不再传递给onInterceptTouchEvent(),而是和down事件一样传递给该ViewGroup的onTouchEvent()处理,注意,目标view将接收不到任何事件。
4.如果最终需要处理事件的view的onTouchEvent()返回了false,那么该事件将被传递至其上一层次的view的onTouchEvent()处理。
5.如果最终需要处理事件的view的onTouchEvent()返回了true,那么后续事件将可以继续传递给该view的onTouchEvent()处理。


当触摸事件ACTION_DOWN发生之后,先调用Activity中的dispatchTouchEvent函数进行处理,紧接着ACTION_DOWN事件传递给ViewGroup中的dispatchTouchEvent函数,接着viewGroup中的dispatchTouchEvent中的ACTION_DOWN事件传递到调用ViewGroup中的onInterceptTouchEvent函数,此函数负责拦截ACTION_DOWN事件。由于viewGroup下还包含子View,所以默认返回值为false,即不拦截此ACTION_DOWN事件。如果返回false,则ACTION_DOWN事件继续传递给其子view。由于子view不是viewGroup的控件,所以ACTION_DOWN事件接着传递到onTouchEvent进行处理事件。此时消息的传递基本上结束。从上可以分析,motionEvent事件的传递是采用隧道方式传递。隧道方式,即从根元素依次往下传递直到最内层子元素或在中间某一元素中由于某一条件停止传递。

参考:https://blog.csdn.net/qiushuiqifei/article/details/9918527

Android属性动画优化

android的三种动画:

  • View Animation(视图动画,平移、缩放、透明等)View的属性没有改变,其位置与大小都不变
  • Drawable Animation(帧动画)
  • Property Animation(属性动画)对象自己的属性会被真的改变,而且属性动画不止用于View,还可以用于任何对象。

现在项目的动画问题最主要出在动画部分临时变量多,GC触发频繁,内存泄漏。属性动画优化思路:

  1. 硬件加速
    在开始动画时调用View.setLayerType(View.LAYER_TYPE_HARDWARE, null)
    运行动画
    动画结束时调用View.setLayerType(View.LAYER_TYPE_NONE, null).
  2. 减少临时变量,使用PropertyValuesHolder(可以用在多属性动画同时工作管理) ,一个view同时发生多种属性效果时,建议这种写法。
  3. 使用Keyframe
    Keyframe是PropertyValuesHolder的成员,用来管理每一个关键帧的出现时间。一个view的单个属性先后发生一系列变化时,建议使用Keyframe达到效果。
    总的来说就是:ObjectAnimator把属性值的更新委托给PropertyValuesHolder执行,PropertyValuesHolder再把关键帧的时序性计算委托给Keyframe。
    最后,不同的view再用不同的ObjectAnimator管理。
  4. 内存泄漏: animator.setRepeatCount(ValueAnimator.INFINITE)及时cancel()
  5. 动画卡顿,可以考虑使用自定义控件实现,如果一个自定义不行,那就是两个

ART和Dalvik

Android4.4版本以前是Dalvik虚拟机,4.4版本开始引入ART虚拟机(Android Runtime)。在4.4版本上,两种运行时环境共存,可以相互切换,但是在5.0版本以后,Dalvik虚拟机则被彻底的丢弃,全部采用ART。

JIT,Just-in-time,即时编译,边运行边编译;AOT,Ahead Of Time,提前编译,指运行前编译。区别:这两种编译方式的主要区别在于是否在“运行时”进行编译

ART

ART 是一种执行效率更高且更省电的运行机制,执行的是本地机器码,这些本地机器码是从dex字节码转换而来。ART采用的是AOT(Ahead-Of-Time)编译,应用在第一次安装的时候,字节码就会预先编译成机器码存储在本地。在App运行时,ART模式就较Dalvik模式少了解释字节码的过程,所以App的运行效率会有所提高,占用内存也会相应减少。

Dalvik

Dalvik 虚拟机采用的是JIT(Just-In-Time)编译模式,意思为即时编译,我们知道apk被安装到手机中时,对应目录会有dex或odex和apk文件,apk文件存储的是资源文件,而dex或odex(经过优化后的dex文件内部存储class文件:加快软件的启动速度,odex可预先提取、应用保护)内部存储class文件,每次运行app时虚拟机会将dex文件解释翻译成机器码,这样才算是本地可执行代码,之后被系统运行。

Dalvik虚拟机可以看做是一个Java VM,他负责解释dex文件为机器码,如果我们不做处理的话,每次执行代码,都需要Dalvik将dex代码翻译为微处理器指令,然后交给系统处理,这样效率不高。为了解决这个问题,Google在2.2版本添加了JIT编译器,当App运行时,每当遇到一个新类,JIT编译器就会对这个类进行编译,经过编译后的代码,会被优化成相当精简的原生型指令码(即native code),这样在下次执行到相同逻辑的时候,速度就会更快。JIT代表运行时编译策略,也可以理解成一种运行时编译器,是为了加快Dalvik虚拟机解释dex速度提出的一种技术方案,来缓存频繁使用的本地机器码。

两者的区别

Dalvik每次都要编译再运行,Art只会安装时启动编译
Art占用空间比Dalvik大(原生代码占用的存储空间更大),就是用“空间换时间”
Art减少编译,减少了CPU使用频率,使用明显改善电池续航
Art应用启动更快、运行更快、体验更流畅、触感反馈更及时

dexopt 与 dex2oat 的区别

通过上图可以很明显的看出 dexopt 与 dex2oat 的区别,前者针对 Dalvik 虚拟机,后者针对 Art 虚拟机。

  • dexopt 是对 dex 文件 进行 verification 和 optimization 的操作,其对 dex 文件的优化结果变成了 odex 文件,这个文件和 dex 文件很像,只是使用了一些优化操作码(譬如优化调用虚拟指令等)。

  • dex2oat 是对 dex 文件的 AOT 提前编译操作,其需要一个 dex 文件,然后对其进行编译,结果是一个本地可执行的 ELF 文件,可以直接被本地处理器执行。

除此之外在上图还可以看到 Dalvik 虚拟机中有使用 JIT 编译器,也就是说其也能将程序运行的热点 java 字节码编译成本地 code 执行,所以其与 Art 虚拟机还是有区别的。Art 虚拟机的 dex2oat 是提前编译所有 dex 字节码,而 Dalvik 虚拟机只编译使用启发式检测中最频繁执行的热点字节码。

CLASS_ISPREVERIFIED

  1. 在apk安装的时候,虚拟机会将dex优化成odex后才拿去执行。在这个过程中会对所有class一个校验。
  2. 校验方式:假设A该类在它的static方法,private方法,构造函数,override方法中直接引用到B类。如果A类和B类在同一个dex中,那么A类就会被打上CLASS_ISPREVERIFIED标记
  3. 被打上这个标记的类不能引用其他dex中的类,否则就会报错
  4. 在我们的Demo中,MainActivity和Cat本身是在同一个dex中的,所以MainActivity被打上了CLASS_ISPREVERIFIED。而我们修复bug的时候却引用了另外一个dex的Cat.class,所以这里就报错了
  5. 而普通分包方案则不会出现这个错误,因为引用和被引用的两个类一开始就不在同一个dex中,所以校验的时候并不会被打上CLASS_ISPREVERIFIED
  6. 补充一下第二条:A类如果还引用了一个C类,而C类在其他dex中,那么A类并不会被打上标记。换句话说,只要在static方法,构造方法,private方法,override方法中直接引用了其他dex中的类,那么这个类就不会被打上CLASS_ISPREVERIFIED标记

方案:
根据上面的第六条,我们只要让所有类都引用其他dex中的某个类就可以了。在所有类的构造函数中插入这行代码(AOP):

System.out.println(AntilazyLoad.class);

这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作

UncaughtExceptionHandler

如果给一个线程设置了UncaughtExceptionHandler 这个接口:
1、这个线程中,所有未处理或者说未捕获的异常都将会由这个接口处理,也就说被这个接口给try…catch了。
2、在这个线程中抛出异常时,java虚拟机将会忽略,也就是说,java虚拟机不会让程序崩溃了。
3、如果没有设置,那么最终会调用getDefaultUncaughtExceptionHandler 获取默认的UncaughtExceptionHandler 来处理异常。

注意:
1、即使我们用这种方式捕获到了异常,保证程序不会闪退,如果是子线程出现了异常,那么还好,并不会影响UI线程的正常流程,但是如果是UI线程中出现了异常,那么程序就不会继续往下走,处于没有响应的状态,所以,我们处理异常的时候,应该给用户一个有好的提示,让程序优雅地退出。
2、Thread.setDefaultUncaughtExceptionHandler(handler)方法,如果多次调用的话,会以最后一次调用时,传递的handler为准,之前设置的handler都没用。所以,这也是如果用了第三方的统计模块后,可能会出现失灵的情况。(这种情况其实也好解决,就是只设置一个handler,以这handler为主,然后在这个handler的uncaughtException 方法中,调用其他的handler的uncaughtException 方法,保证都会收到异常信息)

参考资料:
https://blog.csdn.net/bai981002/article/details/78717069

ANR

ANR问题发生时,系统会收集ANR相关的日志信息,CPU使用情况,trace日志也就是各线程执行情况等信息,生成一个traces.txt的文件并且放在/data/anr/路径下。

ANR的发生原因,总结出三个典型场景

1、主线程被其他线程锁(占比57%):调用了thread的sleep()、wait()等方法,导致的主线程等待超时。

2、系统资源被占用(占比14%):其他进程系统资源(CPU/RAM/IO)占用率高,导致该进程无法抢占到足够的系统资源。

3、主线程耗时工作导致线程卡死(占比9%):例如大量的数据库读写,耗时的网络情况,高强度的硬件计算等。

解决ANR问题方法总体思路

1、导出ANR日志信息,根据日志信息,判断确认发生ANR的包名类名,进程号,发生时间,导致ANR原因类型等。

2、关注系统资源信息,包括ANR发生前后的CPU,内存,IO等系统资源的使用情况。

3、查看主线程状态,关注主线程是否存在耗时、死锁、等锁等问题,判断该ANR是App导致还是系统导致的。

4、结合应用日志,代码或源码等,分析ANR问题发生前,应用是否有异常,其中具体问题具体分析。

掉帧检测方案Looper

Android使用消息机制进行UI更新,UI线程有个Looper,在其loop方法中会不断取出message,调用其绑定的Handler在UI线程执行。如果在handler的dispatchMesaage方法里有耗时操作,就会发生卡顿。

Looper源码:

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
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;

// Make sure the identity of this thread is that of the local process,
// and keep track of what that identity token actually is.
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();

for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}

// This must be in a local variable, in case a UI event sets the logger
//处理消息前,打印开始日志
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}

msg.target.dispatchMessage(msg);

//处理完消息后,打印结束日志
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}

// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}

msg.recycleUnchecked();
}
}

我们可以根据消息处理前后的日志输出作为检测点,计算出消息处理的耗时,如果超出16ms,说明发生了卡顿,此时就可以把UI线程的堆栈日志打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Looper.getMainLooper().setMessageLogging(new Printer() {
private static final String START = ">>>>> Dispatching";
private static final String END = "<<<<< Finished";

@Override
public void println(String x) {
if (x.startsWith(START)) {
UiBlockLogMonitor.getInstance().startMonitor();
}
if (x.startsWith(END)) {
UiBlockLogMonitor.getInstance().stopMonitor();
}
}
});

不过,由于系统定制的原因,打印出来的日志标识不一定标准,所以可以改为判断第一次日志输出和第二次日志输出。

常见卡顿原因及解决方案

一、过度绘制,去除不必要的背景色(开发者选项 -> 调试GPU过度绘制):

  1. 设置窗口背景色为通用背景色,去除根布局背景色。
  2. 若页面背景色与通用背景色不一致,在页面渲染完成后移除窗口背景色
  3. 去除和列表背景色相同的Item背景色

二、布局视图树扁平化

  1. 移除嵌套布局
  2. 使用merge、include标签
  3. 使用性能消耗更小布局(TableLayout、ConstraintLayout)

三、减少透明色,即alpha属性的使用
将alpha设置为半透明值(0<alpha<1)会很耗性能,尤其是对大视图。所以最好短暂地使用alpha属性。通过使用半透明颜色值(#77000000)代替

四、其他

  1. 使用ViewStub标签,延迟加载不必要的视图
  2. 使用AsyncLayoutInflater异步解析视图

五、主线程耗时操作

  1. Json数据解析耗时(Cache类)
  2. 文件操作(获取所属渠道名称)
  3. Binder通信(获取系统属性(mac地址))
  4. 正则匹配(Hybird 通信)
  5. 相机操作:初始化、预览、停止预览、释放(反扫)
  6. 组件初始化(推送)
  7. 循环删除、创建View(更多页面)
  8. WebView首次初始化

设置异步线程优先级为Process.THREAD_PRIORITY_BACKGROUND,减少与主线程的竞争。

参考资料:
https://www.cnblogs.com/developer-huawei/p/13856252.html
https://blog.csdn.net/joye123/article/details/79425398

@aar

@aar的方式关闭传递依赖

// 只下载该库,其他所依赖的所有库不下载
compile 'io.reactivex.rxjava2:rxandroid:2.0.1@aar'
// 在使用@aar的前提下还能下载其他依赖库,则需要添加transitive=true的条件
compile ("io.reactivex.rxjava2:rxandroid::2.0.1@aar") {
    transitive=true
}

Groovy

Groovy语言=Java语言的扩展+众多脚本语言的语法。运行在JVM虚拟机上。Gradle项目构框架使用groovy语言实现。 基于Gradle框架为我们实现了一些项目构件框架。

Groovy是一门jvm语言,它最终是要编译成class文件然后在jvm上执行,所以Java语言的特性Groovy都支持,我们完全可以混写Java和Groovy。

在Groovy中,数据类型有:
1) Java中的基本数据类型
2) Java中的对象
3) Closure(闭包)
4) 加强的List、Map等集合类型
5) 加强的File、Stream等IO类型

类型可以显示声明,也可以用 def 来声明,用 def 声明的类型Groovy将会进行类型推断。

gradle插件

插件会扩展项目的功能,帮助我们在项目的构建的过程中做很多事情:

  • 可以添加任务到项目中,帮助完成诸如 测试、编译、打包等事情。
  • 可以添加依赖配置到项目中,通过它们来配置我们在构建过程中的依赖。
  • 可以向项目中现有的对象类型添加新的扩展属性、方法等,可以使用它们来配置优化构建。例如:android{}这个配置块就是Android Gradle插件为Project对象添加的一个扩展。
  • 可以对项目进行一些约定,比如应用Java插件后,可以约定src/main/java目录下是我们的源代码的存放地址,在编译的时候也是编译这个目录下的Java源代码文件。

这就是插件,我们只需要按照它约定的方式,使用它提供的任务、方法或者扩展,就可以对我们的项目进行构建。

因为是基于groovy开发,所有代码文件要以.groovy结尾

1.定义插件入口: implements Plugin

2.XXXTask必须要继承DefautTask,并使用@TaskAction来定义Task的入口函数。

3.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
28
29
30
31
32
33
34
35
36
37
38
39
40
//bulid.gradle
buildscript {

repositories {
google()
jcenter()
maven {
url 'file:///Users/xinyu/work/maven-lib/'
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.2'
classpath 'com.zxy.plugin:plugin:1.0.0'
}
}

//plugin gradle
apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
// gradle sdk
compile gradleApi()
// groovy sdk
compile localGroovy()
// 可以引用其它库
compile fileTree(dir: 'libs'2 include: ['*.jar'])
}

uploadArchives {
repositories {
mavenDeployer {
//本地的Maven地址
repository(url: 'file:///Users/xinyu/work/maven-lib/')
}
}
}

group='com.zxy.plugin'
version='1.0.0'

参考资料:https://www.jianshu.com/p/eda0bfd692e6

AOP

AOP(Aspect Oriented Programming 面向切面编程)则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。

将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。能将同一个关注点聚焦到同一个方法中解决。

AspectJ

由于AndroidStudio默认是没有ajc编译器的,所以在Android中使用@AspectJ来编写(包括SpringAOP也是如此)。它在代码的编译期间扫描目标程序,根据切点(PointCut)匹配,将开发者编写的Aspect程序编织(Weave)到目标程序的.class文件中,对目标程序作了重构(重构单位是JoinPoint),目的就是建立目标程序与Aspect程序的连接(获得执行的对象、方法、参数等上下文信息),从而达到AOP的目的。

AspectJ是通过对目标工程的.class文件进行代码注入的方式将通知(Advise)插入到目标代码中:
第一步:根据pointCut切点规则匹配的joinPoint;
第二步:将Advise插入到目标JoinPoint中。
这样在程序运行时被重构的连接点将会回调Advise方法,就实现了AspectJ代码与目标代码之间的连接。

Gradle Transform

Gradle Transform是Android官方提供给开发者在项目构建阶段即由class到dex转换期间修改class文件的一套api。目前比较经典的应用是字节码插桩、代码注入技术。Gradle Transform更多的是提供一种可以让开发者参与项目构建的机制,而诸如修改字节码等更加具体的细节则需要开发者去实现。

Hook

代理Hook:如果我们自己创建代理对象,然后把原始对象替换为我们的代理对象,那么就可以在这个代理对象为所欲为了;修改参数,替换返回值,我们称之为Hook。

hook,又叫钩子,通常是指对一些方法进行拦截。这样当这些方法被调用时,也能够执行我们自己的代码,这也是面向切面编程的思想(AOP)。

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

大致思路:
1.找到需要Hook方法的系统类
2.利用代理模式来代理系统类的运行拦截我们需要拦截的方法
3.使用反射的方法把这个系统类替换成你的代理类

案例可参考:
https://blog.csdn.net/yulong0809/article/details/56842027

应用的启动过程

1.首先我们要启动的Activity会去ActivityManagerService中去校检是否合法

2.通过回调ActivityThread中内部类ApplicationThread的scheduleLaunchActivity去发送一个消息到ActivityThread中的内部类H中,H继承于Handler

3.然后会通过反射创建Activity对象及Application对象,并回调响应生命周期方法

这里说一点ActivityManagerService和我们应用间沟通几乎都是ActivityThread,ApplicationThread,H这几个类之间来回调用,而且不只是Activity,我们Android的四大组件几乎都用了这种模式

参考:
https://blog.csdn.net/yulong0809/article/details/58589715

Watchdog

能通过关闭FinalizerWatchdogDaemon来减少TimeoutException的触发。需要注意的是,此种方法并不是去解决问题,而是为了避免上报异常采取的一种 hack 方案,并没有真正的解决引起 finialize() 超时的问题。

// Android P 以后不能反射FinalizerWatchdogDaemon
if (Build.VERSION.SDK_INT >= 28) {
    Log.w(TAG, "stopWatchDog, do not support after Android P, just return");
    return;
}

try {
    final Class clazz = Class.forName("java.lang.Daemons$FinalizerWatchdogDaemon");
    final Field field = clazz.getDeclaredField("INSTANCE");
    field.setAccessible(true);
    final Object watchdog = field.get(null);
    try {
        final Field thread = clazz.getSuperclass().getDeclaredField("thread");
        thread.setAccessible(true);
        thread.set(watchdog, null);
    } catch (final Throwable t) {
        Log.e(TAG, "stopWatchDog, set null occur error:" + t);

        t.printStackTrace();
        try {
            // 直接调用stop方法,在Android 6.0之前会有线程安全问题
            final Method method = clazz.getSuperclass().getDeclaredMethod("stop");
            method.setAccessible(true);
            method.invoke(watchdog);
        } catch (final Throwable e) {
            Log.e(TAG, "stopWatchDog, stop occur error:" + t);
            t.printStackTrace();
        }
    }
} catch (final Throwable t) {
    Log.e(TAG, "stopWatchDog, get object occur error:" + t);
    t.printStackTrace();
}

NFC

Near Field Communication (NFC) 为一短距离无线通信技术,通常有效通讯距离为4厘米以内。NFC工作频率为13.65 兆赫兹,通信速率为106 kbit/秒到 848kbit/秒

NFC支持如下3种工作模式:
读卡器模式(Reader/writer mode)、
仿真卡模式(Card Emulation Mode)、
点对点模式(P2P mode)。

Android SDK API主要支持NFC论坛标准(Forum Standard),这种标准被称为NDEF(NFC Data Exchange Format,NFC数据交换格式),类似传感器。

线程池

线程池使用的好处:
1:对多个线程进行统一地管理,避免资源竞争中出现的问题。
2:(重点):对线程进行复用,线程在执行完任务后不会立刻销毁,而会等待另外的任务,这样就不会频繁地创建、销毁线程和调用GC。
3:JAVA提供了一套完整的ExecutorService线程池创建的api,可创建多种功能不一的线程池,使用起来很方便。

创建线程池,主要是利用ThreadPoolExecutor这个类,而这个类有几种构造方法,其中参数最多的一种构造方法如下:

1
2
3
4
5
6
7
8
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
...
}

corePoolSize: 该线程池中核心线程的数量。
maximumPoolSize:该线程池中最大线程数量。(区别于corePoolSize)
keepAliveTime:从字面上就可以理解,是非核心线程空闲时要等待下一个任务到来的时间,当任务很多,每个任务执行时间很短的情况下调大该值有助于提高线程利用率。注意:当allowCoreThreadTimeOut属性设为true时,该属性也可用于核心线程。
unit:上面时间属性的单位
workQueue:任务队列
threadFactory:线程工厂,可用于设置线程名字等等,一般无须设置该参数。

进程保活

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机制是定时进行检查。

方案:

1、利用系统广播拉活:

系统广播事件是不可控制的,只有在发生事件时才能进行拉活,无法保证进程被杀死后立即被拉活

2、利用系统Service机制拉活:

将Service中的onStartCommand()回调方法的返回值设为START_STICKY,就可以利用系统机制在Service挂掉后自动拉活。
拓展:onStartCommand()的返回值表明当Service由于系统内存不足而被系统杀掉之后,在未来的某个时间段内当系统内存足够的情况下,系统会尝试创建这个Service,一旦创建成功就又会回调onStartCommand()方法。
缺点(无法拉活的情形):Service第一次被异常杀死后会在5s内重启,第二次会在10s内重启,第三次会在20s内重启,若Service在短时间内被杀死的次数超过3次以上系统就会不惊醒拉活;进程被取得root权限的管理工具或系统工具通过强制stop时,通过Service机制无法重启进程。

3、利用JobScheduler机制拉活

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

4、利用Native进程拉活

思想:利用Linux中的fork机制创建一个Native进程,在Native进程可以监控主进程的存活,当主进程挂掉之后,Native进程可以立即对主进程进行拉活。
在Native进程中如何监听主进程被杀死:可在Native进程中通过死循环或定时器,轮询地判断主进程被杀死,但是此方案会耗时耗资源;在主线程中创建一个监控文件,并且在主进程中持有文件锁,在拉活进程启动后申请文件锁将会被阻塞,一旦成功获取到锁说明主进程挂掉了。
如何在Native进程中拉活主进程:主要通过一个am命令即可拉活。
说明:android5.0后系统对Native进程加强了管理,利用Native进程拉活的方式已失效。