Android 面试之必问性能优化

对于Android开发者来讲,懂得基本的应用开发技能每每是不够,由于无论是工做仍是面试,都须要开发者懂得大量的性能优化,这对提高应用的体验是很是重要的。对于Android开发来讲,性能优化主要围绕以下方面展开:启动优化、渲染优化、内存优化、网络优化、卡顿检测与优化、耗电优化、安装包体积优化、安全问题等。html

1,启动优化

一个应用的启动快慢是可以直接影响用户的使用体验的,若是启动较慢可能会致使用户卸载放弃该应用程序。前端

1.1 冷启动、热启动和温启动的优化

1.1.1 概念

对于Android应用程序来讲,根据启动方式能够分为冷启动,热启动和温启动三种。java

  • 冷启动:系统不存在App进程(如APP首次启动或APP被彻底杀死)时启动App称为冷启动。
  • 热启动:按了Home键或其它状况app被切换到后台,再次启动App的过程。
  • 温启动:温启动包含了冷启动的一些操做,不过App进程依然存在,这表明着它比热启动有更多的开销。

能够看到,热启动是启动最快的,温启动则是介于冷启动和热启动之间的一种启动方式。下而冷启动则是最慢的,由于它会涉及不少进程的建立,下面是冷启动相关的任务流程:
在这里插入图片描述python

1.1.2 视觉优化

在冷启动模式下,系统会启动三个任务:android

  • 加载并启动应用程序。
  • 启动后当即显示应用程序空白的启动窗口。
  • 建立应用程序进程。

一旦系统建立应用程序进程,应用程序进程就会进入下一阶段,并完成以下的一些事情。web

  • 建立app对象
  • 启动主线程(main thread)
  • 建立应用入口的Activity对象
  • 填充加载布局View
  • 在屏幕上执行View的绘制过程.measure -> layout -> draw

应用程序进程完成第一次绘制后,系统进程会交换当前显示的背景窗口,将其替换为主活动。此时,用户能够开始使用该应用程序了。由于App应用进程的建立过程是由手机的软硬件决定的,因此咱们只能在这个建立过程当中进行一些视觉优化。面试

1.1.3 启动主题优化

在冷启动的时候,当应用程序进程被建立后,就须要设置启动窗口的主题。目前,大部分的 应用在启动会都会先进入一个闪屏页(LaunchActivity) 来展现应用信息,若是在 Application 初始化了其它第三方的服务,就会出现启动的白屏问题。算法

为了更顺滑无缝衔接咱们的闪屏页,能够在启动 Activity 的 Theme中设置闪屏页图片,这样启动窗口的图片就会是闪屏页图片,而不是白屏。shell

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowBackground">@drawable/lunch</item>  //闪屏页图片
        <item name="android:windowFullscreen">true</item>
        <item name="android:windowDrawsSystemBarBackgrounds">false</item>
    </style>

1.2 代码方面的优化

设置主题的方式只能应用在要求不是很高的场景,而且这种优化治标不治本,关键还在于代码的优化。为了进行优化,咱们须要掌握一些基本的数据。编程

1.2.1 冷启动耗时统计

ADB命令方式
在Android Studio的Terminal中输入如下命令能够查看页面的启动的时间,命令以下:

adb shell am start  -W packagename/[packagename].首屏Activity

执行完成以后,会在控制台输出以下的信息:

Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.optimize.performance/.MainActivity }
Status: ok
Activity: com.optimize.performance/.MainActivity
ThisTime: 563
TotalTime: 563
WaitTime: 575
Complete

在上面的日志中有三个字段信息,即ThisTime、TotalTime和WaitTime。

  • ThisTime:最后一个Activity启动耗时
  • TotalTime:全部Activity启动耗时
  • WaitTime:AMS启动Activity的总耗时

日志方式
埋点方式是另外一种统计线上时间的方式,这种方式经过记录启动时的时间和结束的时间,而后取两者差值便可。首先,须要定义一个统计时间的工具类:

class LaunchRecord {
​
    companion object {
​
        private var sStart: Long = 0

        fun startRecord() {
            sStart = System.currentTimeMillis()
        }
​
        fun endRecord() {
            endRecord("")
        }
​
        fun endRecord(postion: String) {
            val cost = System.currentTimeMillis() - sStart
            println("===$postion===$cost")
        }
    }
}

启动时埋点咱们直接在Application的attachBaseContext中进行打点。那么启动结束应该在哪里打点呢?结束埋点建议是在页面数据展现出来进行埋点。可使用以下方法:

class MainActivity : AppCompatActivity() {
​
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
​
        mTextView.viewTreeObserver.addOnDrawListener {
            LaunchRecord.endRecord("onDraw")
        }
​
    }
​
    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        LaunchRecord.endRecord("onWindowFocusChanged")
    }
}

1.2.2 优化检测工具

在作启动优化的时候,能够借助三方工具来帮助咱们理清各个阶段的方法或者线程、CPU的执行耗时等状况。这里主要介绍如下TraceView和SysTrace两款工具。

TraceView

TraceView是以图形的形式展现执行时间、调用栈等信息,信息比较全面,包含全部线程,以下图所示。
在这里插入图片描述
使用TraceView检测生成生成的结果会放在Andrid/data/packagename/files路径下。由于Traceview收集的信息比较全面,因此会致使运行开销严重,总体APP的运行会变慢,所以咱们没法区分是否是Traceview影响了咱们的启动时间。

SysTrace
Systrace是结合Android内核数据,生成HTML报告,从报告中咱们能够看到各个线程的执行时间以及方法耗时和CPU执行时间等。

在这里插入图片描述
再API 18以上版本,能够直接使用TraceCompat来抓取数据,由于这是兼容的API。

开始:TraceCompat.beginSection("tag ")
结束:TraceCompat.endSection()

而后,执行以下脚本。

python systrace.py -b 32768 -t 10 -a packagename -o outputfile.html sched gfx view wm am app

这里能够你们普及下各个字端的含义:

  • b: 收集数据的大小
  • t:时间
  • a:监听的应用包名
  • o: 生成文件的名称

Systrace开销较小,属于轻量级的工具,而且能够直观反映CPU的利用率。

2,UI渲染优化

Android系统每隔16ms就会从新绘制一次Activity,所以,咱们的应用必须在16ms内完成屏幕刷新的所有逻辑操做,每一帧只能停留16ms,不然就会出现掉帧现象。Android应用卡顿与否与UI渲染有直接的关系。

2.1CPU、GPU

对于大多数手机的屏幕刷新频率是60hz,也就是若是在1000/60=16.67ms内没有把这一帧的任务执行完毕,就会发生丢帧的现象,丢帧是形成界面卡顿的直接缘由,渲染操做一般依赖于两个核心组件:CPU与GPU。CPU负责包括Measure,Layout等计算操做,GPU负责Rasterization(栅格化)操做。

所谓栅格化,就是将矢量图形转换为位图的过程,手机上显示是按照一个个像素来显示的,好比将一个Button、TextView等组件拆分红一个个像素显示到手机屏幕上。而UI渲染优化的目的就是减轻CPU、GPU的压力,除去没必要要的操做,保证每帧16ms之内处理完全部的CPU与GPU的计算、绘制、渲染等等操做,使UI顺滑、流畅的显示出来。

2.2 过分绘制

UI渲染优化的第一步就是找到Overdraw(过分绘制),即描述的是屏幕上的某个像素在同一帧的时间内被绘制了屡次。在重叠的UI布局中,若是不可见的UI也在作绘制的操做或者后一个控件将前一个控件遮挡,会致使某些像素区域被绘制了屡次,从而增长了CPU、GPU的压力。

那么如何找出布局中Overdraw的地方呢?很简单,就是打开手机里开发者选项,而后将调试GPU过分绘制的开关打开便可,而后就能够看到应用的布局是否被Overdraw,以下图所示。
在这里插入图片描述
蓝色、淡绿、淡红、深红表明了4种不一样程度的Overdraw状况,1x、2x、3x和4x分别表示同一像素上同一帧的时间内被绘制了屡次,1x就表示一次(最理想状况),4x表示4次(最差的状况),而咱们须要消除的就是3x和4x。

2.3 解决自定义View的OverDraw

咱们知道,自定义View的时候有时会重写onDraw方法,可是Android系统是没法检测onDraw里面具体会执行什么操做,从而系统没法为咱们作一些优化。这样对编程人员要求就高了,若是View有大量重叠的地方就会形成CPU、GPU资源的浪费,此时咱们可使用canvas.clipRect()来帮助系统识别那些可见的区域。

这个方法能够指定一块矩形区域,只有在这个区域内才会被绘制,其余的区域会被忽视。下面咱们经过谷歌提供的一个小的Demo进一步说明OverDraw的使用。
在这里插入图片描述
在下面的代码中,DroidCard类封装的是卡片的信息,代码以下。

public class DroidCard {

public int x;//左侧绘制起点
public int width;
public int height;
public Bitmap bitmap;

public DroidCard(Resources res,int resId,int x){
this.bitmap = BitmapFactory.decodeResource(res,resId);
this.x = x;
this.width = this.bitmap.getWidth();
this.height = this.bitmap.getHeight();
 }
}

自定义View的代码以下:

public class DroidCardsView extends View {
//图片与图片之间的间距
private int mCardSpacing = 150;
//图片与左侧距离的记录
private int mCardLeft = 10;

private List<DroidCard> mDroidCards = new ArrayList<DroidCard>();

private Paint paint = new Paint();

public DroidCardsView(Context context) {
super(context);
initCards();
}

public DroidCardsView(Context context, AttributeSet attrs) {
super(context, attrs);
initCards();
}
/**
* 初始化卡片集合
*/
protected void initCards(){
Resources res = getResources();
mDroidCards.add(new DroidCard(res,R.drawable.alex,mCardLeft));

mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res,R.drawable.claire,mCardLeft));

mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res,R.drawable.kathryn,mCardLeft));
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (DroidCard c : mDroidCards){
drawDroidCard(canvas, c);
}
invalidate();
}

/**
* 绘制DroidCard
*/
private void drawDroidCard(Canvas canvas, DroidCard c) {
canvas.drawBitmap(c.bitmap,c.x,0f,paint);
}
}

而后,咱们运行代码,打开手机的overdraw开关,效果以下:
在这里插入图片描述
能够看到,淡红色区域明显被绘制了三次,是由于图片的重叠形成的。那怎么解决这种问题呢?其实,分析能够发现,最下面的图片只须要绘制三分之一便可,保证最下面两张图片只须要回执其三分之一最上面图片彻底绘制出来就可。优化后的代码以下:

public class DroidCardsView extends View {

//图片与图片之间的间距
private int mCardSpacing = 150;
//图片与左侧距离的记录
private int mCardLeft = 10;

private List<DroidCard> mDroidCards = new ArrayList<DroidCard>();

private Paint paint = new Paint();

public DroidCardsView(Context context) {
super(context);
initCards();
}

public DroidCardsView(Context context, AttributeSet attrs) {
super(context, attrs);
initCards();
}
/**
* 初始化卡片集合
*/
protected void initCards(){
Resources res = getResources();
mDroidCards.add(new DroidCard(res, R.drawable.alex,mCardLeft));

mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res, R.drawable.claire,mCardLeft));

mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res, R.drawable.kathryn,mCardLeft));
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < mDroidCards.size() - 1; i++){
drawDroidCard(canvas, mDroidCards,i);
}
drawLastDroidCard(canvas,mDroidCards.get(mDroidCards.size()-1));
invalidate();
}

/**
* 绘制最后一个DroidCard
* @param canvas
* @param c
*/
private void drawLastDroidCard(Canvas canvas,DroidCard c) {
canvas.drawBitmap(c.bitmap,c.x,0f,paint);
}

/**
* 绘制DroidCard
* @param canvas
* @param mDroidCards
* @param i
*/
private void drawDroidCard(Canvas canvas,List<DroidCard> mDroidCards,int i) {
DroidCard c = mDroidCards.get(i);
canvas.save();
canvas.clipRect((float)c.x,0f,(float)(mDroidCards.get(i+1).x),(float)c.height);
canvas.drawBitmap(c.bitmap,c.x,0f,paint);
canvas.restore();
 }
}

在上面的代码中,咱们使用Canvas的clipRect方法,绘制以前裁剪出一个区域,这样绘制的时候只在这区域内绘制,超出部分不会绘制出来。从新运行上面的代码,效果以下图所示。
在这里插入图片描述

2.4 Hierarchy Viewer

Hierarchy Viewer 是 Android Device Monitor 中内置的一种工具,可以让开发者测量布局层次结构中每一个视图的布局速度,以及帮助开发者查找视图层次结构致使的性能瓶颈。Hierarchy Viewer能够经过红、黄、绿三种不一样的颜色来区分布局的Measure、Layout、Executive的相对性能表现状况。

打开

  1. 将设备链接到计算机。若是设备上显示对话框提示您容许 USB 调试吗?,请点按肯定。
  2. 在 Android Studio 中打开您的项目,在您的设备上构建并运行项目。
  3. 启动 Android Device Monitor。Android Studio 可能会显示 Disable adb integration 对话框,由于一次只能有一个进程能够经过 adb 链接到设备,而且 Android Device Monitor 正在请求链接。所以,请点击 Yes。
  4. 在菜单栏中,依次选择 Window > Open Perspective,而后点击 Hierarchy View。
  5. 在左侧的 Windows 标签中双击应用的软件包名称。这会使用应用的视图层次结构填充相关窗格。

在这里插入图片描述
提高布局性能的关键点是尽可能保持布局层级的扁平化,避免出现重复的嵌套布局。若是咱们写的布局层级比较深会严重增长CPU的负担,形成性能的严重卡顿,关于Hierarchy Viewer的使用能够参考:使用 Hierarchy Viewer 分析布局

2.5 内存抖动

在咱们优化过view的树形结构和overdraw以后,可能仍是感受本身的app有卡顿和丢帧,或者滑动慢等问题,咱们就要查看一下是否存在内存抖动状况了。所谓内存抖动,指的是内存频繁建立和GC形成的UI线程被频繁阻塞的现象。

Android有自动管理内存的机制,可是对内存的不恰当使用仍然容易引发严重的性能问题。在同一帧里面建立过多的对象是件须要特别引发注意的事情,在同一帧里建立大量对象可能引发GC的不停操做,执行GC操做的时候,全部线程的任何操做都会须要暂停,直到GC操做完成。大量不停的GC操做则会显著占用帧间隔时间。若是在帧间隔时间里面作了过多的GC操做,那么就会形成页面卡顿。
在这里插入图片描述
在Android开发中,致使GC频繁操做有两个主要缘由:

  • 内存抖动,所谓内存抖动就是短期产生大量对象又在短期内立刻释放。
  • 短期产生大量对象超出阈值,内存不够,一样会触发GC操做。

Android的内存抖动可使用Android Studio的Profiler进行检测。
在这里插入图片描述
而后,点击record记录内存信息,查找发生内存抖动位置,固然也可直接经过Jump to Source定位到代码位置。
在这里插入图片描述

为了不发生内存抖动,咱们须要避免在for循环里面分配对象占用内存,须要尝试把对象的建立移到循环体以外,自定义View中的onDraw方法也须要引发注意,每次屏幕发生绘制以及动画执行过程当中,onDraw方法都会被调用到,避免在onDraw方法里面执行复杂的操做,避免建立对象。对于那些没法避免须要建立对象的状况,咱们能够考虑对象池模型,经过对象池来解决频繁建立与销毁的问题,可是这里须要注意结束使用以后,须要手动释放对象池中的对象。

3,内存优化

3.1 内存管理

在前面Java基础环节,咱们对Java的内存管理模型也作了基本的介绍,参考连接:Android 面试之必问Java基础

3.1.1 内存区域

在Java的内存模型中,将内存区域划分为方法区、堆、程序计数器、本地方法栈、虚拟机栈五个区域,以下图。
在这里插入图片描述
方法区

  • 线程共享区域,用于存储类信息、静态变量、常量、即时编译器编译出来的代码数据。
  • 没法知足内存分配需求时会发生OOM。

  • 线程共享区域,是JAVA虚拟机管理的内存中最大的一块,在虚拟机启动时建立。
  • 存放对象实例,几乎全部的对象实例都在堆上分配,GC管理的主要区域。

虚拟机栈

  • 线程私有区域,每一个java方法在执行的时候会建立一个栈帧用于存储局部变量表、操做数栈、动态连接、方法出口等信息。方法从执行开始到结束过程就是栈帧在虚拟机栈中入栈出栈过程。
  • 局部变量表存放编译期可知的基本数据类型、对象引用、returnAddress类型。所需的内存空间会在编译期间完成分配,进入一个方法时在帧中局部变量表的空间是彻底肯定的,不须要运行时改变。
  • 若线程申请的栈深度大于虚拟机容许的最大深度,会抛出SatckOverFlowError错误。
  • 虚拟机动态扩展时,若没法申请到足够内存,会抛出OutOfMemoryError错误。

本地方法栈

  • 为虚拟机中Native方法服务,对本地方法栈中使用的语言、数据结构、使用方式没有强制规定,虚拟机可自有实现。
  • 占用的内存区大小是不固定的,可根据须要动态扩展。

程序计数器

  • 一块较小的内存空间,线程私有,存储当前线程执行的字节码行号指示器。
  • 字节码解释器经过改变这个计数器的值来选取下一条须要执行的字节码指令:分支、循环、跳转等。
  • 每一个线程都有一个独立的程序计数器
  • 惟一一个在java虚拟机中不会OOM的区域

3.1.2 垃圾回收

标记清除算法
标记清除算法主要分为有两个阶段,首先标记出须要回收的对象,而后咋标记完成后统一回收全部标记的对象;
缺点:

  • 效率问题:标记和清除两个过程效率都不高。
  • 空间问题:标记清除以后会致使不少不连续的内存碎片,会致使须要分配大对象时没法找到足够的连续空间而不得不触发GC的问题。

复制算法
将可用内存按空间分为大小相同的两小块,每次只使用其中的一块,等这块内存使用完了将还存活的对象复制到另外一块内存上,而后将这块内存区域对象总体清除掉。每次对整个半区进行内存回收,不会致使碎片问题,实现简单且效率高效。
缺点:
须要将内存缩小为原来的一半,空间代价过高。

标记整理算法
标记整理算法标记过程和标记清除算法同样,但清除过程并非对可回收对象直接清理,而是将全部存活对象像一端移动,而后集中清理到端边界之外的内存。

分代回收算法
当代虚拟机垃圾回收算法都采用分代收集算法来收集,根据对象存活周期不一样将内存划分为新生代和老年代,再根据每一个年代的特色采用最合适的回收算法。

  • 新生代存活对象较少,每次垃圾回收都有大量对象死去,通常采用复制算法,只须要付出复制少许存活对象的成本就能够实现垃圾回收;
  • 老年代存活对象较多,没有额外空间进行分配担保,就必须采用标记清除算法和标记整理算法进行回收;

3.2 内存泄漏

所谓内存泄露,指的是内存中存在的没有用的确没法回收的对象。表现的现象是会致使内存抖动,可用内存减小,进而致使GC频繁、卡顿、OOM。

下面是一段模拟内存泄漏的代码:

/**
 * 模拟内存泄露的Activity
 */
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memoryleak);
        ImageView imageView = findViewById(R.id.iv_memoryleak);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.splash);
        imageView.setImageBitmap(bitmap);
        
        // 添加静态类引用
        CallBackManager.addCallBack(this);
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
//        CallBackManager.removeCallBack(this);
    }
    @Override
    public void dpOperate() {
        // do sth
    }

当咱们使用Memory Profiler工具查看内存曲线,发现内存在不断的上升,以下图所示。
在这里插入图片描述
若是想分析定位具体发生内存泄露位置,咱们能够借助MAT工具。首先,使用MAT工具生成hprof文件,点击dump将当前内存信息转成hprof文件,须要对生成的文件转换成MAT可读取文件。执行一下转换命令便可完成转换,生成的文件位于Android/sdk/platorm-tools路径下。

hprof-conv 刚刚生成的hprof文件 memory-mat.hprof

使用mat打开刚刚转换的hprof文件,而后使用Android Studio打开hprof文件,以下图所示。
在这里插入图片描述
而后点击面板的【Historygram】,搜索MemoryLeakActivity,便可查看对应的泄漏文件的相关信息。
在这里插入图片描述
而后,查看全部引用对象,并获得相关的引用链,以下图。
在这里插入图片描述
在这里插入图片描述
能够看到GC Roots是CallBackManager
在这里插入图片描述
因此,咱们在Activity销毁时将CallBackManager引用移除便可。

@Override
protected void onDestroy() {
    super.onDestroy();
    CallBackManager.removeCallBack(this);
}

固然,上面只是一个MAT分析工具使用的示例,其余的内存泄露均可以借助MAT分析工具解决。

3.3 大图内存优化

在Android开发中,常常会遇到加载大图致使内存泄露的问题,对于这种场景,有一个通用的解决方案,即便用ARTHook对不合理图片进行检测。咱们知道,获取Bitmap占用的内存主要有两种方式:

  • 经过getByteCount方法,可是须要在运行时获取
  • width * height * 一个像素所占内存 * 图片所在资源目录压缩比

经过ARTHook方法能够优雅的获取不合理图片,侵入性低,可是由于兼容性问题通常在线下使用。使用ARTHook须要安装如下依赖:

implementation 'me.weishu:epic:0.3.6'

而后自定义实现Hook方法,以下所示。

public class CheckBitmapHook extends XC_MethodHook {
    @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        ImageView imageView = (ImageView)param.thisObject;
        checkBitmap(imageView,imageView.getDrawable());
    }
    private static void checkBitmap(Object o,Drawable drawable) {
        if(drawable instanceof BitmapDrawable && o instanceof View) {
            final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
            if(bitmap != null) {
                final View view = (View)o;
                int width = view.getWidth();
                int height = view.getHeight();
                if(width > 0 && height > 0) {
                    if(bitmap.getWidth() > (width <<1) && bitmap.getHeight() > (height << 1)) {
                        warn(bitmap.getWidth(),bitmap.getHeight(),width,height,
                                new RuntimeException("Bitmap size is too large"));
                    }
                } else {
                    final Throwable stacktrace = new RuntimeException();
                    view.getViewTreeObserver().addOnPreDrawListener(
                            new ViewTreeObserver.OnPreDrawListener() {
                                @Override public boolean onPreDraw() {
                                    int w = view.getWidth();
                                    int h = view.getHeight();
                                    if(w > 0 && h > 0) {
                                        if (bitmap.getWidth() >= (w << 1)
                                                && bitmap.getHeight() >= (h << 1)) {
                                            warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stacktrace);
                                        }
                                        view.getViewTreeObserver().removeOnPreDrawListener(this);
                                    }
                                    return true;
                                }
                            });
                }
            }
        }
    }
    private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
        String warnInfo = new StringBuilder("Bitmap size too large: ")
                .append("\n real size: (").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
                .append("\n desired size: (").append(viewWidth).append(',').append(viewHeight).append(')')
                .append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
                .toString();
        LogUtils.i(warnInfo);

最后,在Application初始化时注入Hook。

DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
    @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        DexposedBridge.findAndHookMethod(ImageView.class,"setImageBitmap", Bitmap.class,
                new CheckBitmapHook());
    }
});

3.4 线上监控

3.4.1 常规方案

方案一
在特定场景中获取当前占用内存大小,若是当前内存大小超过系统最大内存80%,对当前内存进行一次Dump(Debug.dumpHprofData()),选择合适时间将hprof文件进行上传,而后经过MAT工具手动分析该文件。

缺点:

  • Dump文件比较大,和用户使用时间、对象树正相关。
  • 文件较大致使上传失败率较高,分析困难。

方案二
将LeakCannary带到线上,添加预设怀疑点,对怀疑点进行内存泄露监控,发现内存泄露回传到服务端。

缺点:

  • 通用性较低,须要预设怀疑点,对没有预设怀疑点的地方监控不到。
  • LeakCanary分析比较耗时、耗内存,有可能会发生OOM。

3.4.2 LeakCannary改造

改造主要涉及如下几点:

  • 将须要预设怀疑点改成自动寻找怀疑点,自动将前内存中所占内存较大的对象类中设置怀疑点。
  • LeakCanary分析泄露链路比较慢,改造为只分析Retain size大的对象。
  • 分析过程会OOM,是由于LeakCannary分析时会将分析对象所有加载到内存当中,咱们能够记录下分析对象的个数和占用大小,对分析对象进行裁剪,不所有加载到内存当中。

完成的改造步骤以下:

  1. 监控常规指标:待机内存、重点模块占用内存、OOM率
  2. 监控APP一个生命周期内和重点模块界面的生命周期内的GC次数、GC时间等
  3. 将定制的LeakCanary带到线上,自动化分析线上的内存泄露

4,网络优化

4.1 网络优化的影响

App的网络链接对于用户来讲, 影响不少, 且多数状况下都很直观, 直接影响用户对这个App的使用体验. 其中较为重要的几点:
流量 :App的流量消耗对用户来讲是比较敏感的, 毕竟流量是花钱的嘛. 如今大部分人的手机上都有安装流量监控的工具App, 用来监控App的流量使用. 若是咱们的App这方面没有控制好, 会给用户很差的使用体验。
电量 :电量相对于用户来讲, 没有那么明显. 通常用户可能不会太注意. 可是如电量优化中的那样, 网络链接(radio)是对电量影响很大的一个因素. 因此咱们也要加以注意。
用户等待 :也就是用户体验, 良好的用户体验, 才是咱们留住用户的第一步. 若是App请求等待时间长, 会给用户网络卡, 应用反应慢的感受, 若是有对比, 有替代品, 咱们的App极可能就会被用户无情抛弃。

4.2 网络分析工具

网络分析能够借助的工具备Monitor、代理工具等。

4.2.1 Network Monitor

Android Studio内置的Monitor工具提供了一个Network Monitor,能够帮助开发者进行网络分析,下面是一个典型的Network Monitor示意图。
在这里插入图片描述

  • Rx --- R(ecive) 表示下行流量,即下载接收。
  • Tx --- T(ransmit) 表示上行流量,即上传发送。

Network Monitor实时跟踪选定应用的数据请求状况。 咱们能够连上手机,选定调试应用进程, 而后在App上操做咱们须要分析的页面请求。

4.2.2 代理工具

网络代理工具备两个做用,一个是截获网络请求响应包, 分析网络请求;另外一个设置代理网络, 移动App开发中通常用来作不一样网络环境的测试, 例如Wifi/4G/3G/弱网等。

如今,可使用的代理工具备不少, 诸如Wireshark, Fiddler, Charles等。

4.3 网络优化方案

对于网络优化来讲,主要从两个方面进行着手进行优化:

  1. 减小活跃时间:减小网络数据获取的频次,从而就减小了radio的电量消耗以及控制电量使用。
  2. 压缩数据包的大小:压缩数据包能够减小流量消耗,也可让每次请求更快, 。

基于上面的方案,能够获得如下一些常见的解决方案:

4.3.1 接口设计

1,API设计
App与服务器之间的API设计要考虑网络请求的频次,资源的状态等。以便App能够以较少的请求来完成业务需求和界面的展现。

例如, 注册登陆. 正常会有两个API, 注册和登陆, 可是设计API时咱们应该给注册接口包含一个隐式的登陆. 来避免App在注册后还得请求一次登陆接口。

2,使用Gzip压缩

使用Gzip来压缩request和response, 减小传输数据量, 从而减小流量消耗。使用Retrofit等网络请求框架进行网络请求时,默认进行了Gzip的压缩。

3,使用Protocol Buffer
之前,咱们传输数据使用的是XML, 后来使用JSON代替了XML, 很大程度上也是为了可读性和减小数据量。而在游戏开发中,为了保证数据的准确和及时性,Google推出了Protocol Buffer数据交换格式。

4,依据网络状况获取不一样分辨率的图片
咱们使用淘宝或者京东的时候,会看到应用会根据网络状况,获取不一样分辨率的图片,避免流量的浪费以及提高用户的体验。

4.3.2 合理使用网络缓存

适当的使用缓存, 不只可让咱们的应用看起来更快, 也能避免一些没必要要的流量消耗,带来更好的用户体验。

1,打包网络请求

当接口设计不能知足咱们的业务需求时。例如,可能一个界面须要请求多个接口,或是网络良好,处于Wifi状态下时咱们想获取更多的数据等。这时就能够打包一些网络请求, 例如请求列表的同时, 获取Header点击率较高的的item项的详情数据。

2,监听设备状态
为了提高用户体验,咱们能够对设备的使用状态进行监听,而后再结合JobScheduler来执行网络请求.。比方说Splash闪屏广告图片, 咱们能够在链接到Wifi时下载缓存到本地; 新闻类的App能够在充电,Wifi状态下作离线缓存。

4.3.3 弱网测试&优化

1,弱网测试
有几种方式来模拟弱网进行测试:

Android Emulator
一般,咱们建立和启动Android模拟器能够设置网络速度和延迟,以下图所示。
在这里插入图片描述
而后,咱们在启动时使用的emulator命令以下。

$emulator -netdelay gprs -netspeed gsm -avd Nexus_5_API_22

2,网络代理工具
使用网络代理工具也能够模拟网络状况。以Charles为例,保持手机和PC处于同一个局域网, 在手机端wifi设置高级设置中设置代理方式为手动, 代理ip填写PC端ip地址, 端口号默认8888。

在这里插入图片描述

5,耗电优化

事实上,若是咱们的应用须要播放视频、须要获取 GPS 信息,亦或者是游戏应用,耗电都是比较严重的。如何判断哪些耗电是能够避免,或者是须要去优化的呢?咱们能够打开手机自带的耗电排行榜,发现“王者荣耀”使用了 7 个多小时,这时用户对“王者荣耀”的耗电是有预期的。
在这里插入图片描述

5.1 优化方向

假设这个时候发现某个应用他根本没怎么使用,可是耗电却很是多,那么就会被系统无情的杀掉。因此耗电优化的第一个方向是优化应用的后台耗电。

知道了系统是如何计算耗电的,咱们也就能够知道应用在后台不该该作什么,例如长时间获取 WakeLock、WiFi 和蓝牙的扫描等,以及后台服务。为何说耗电优化第一个方向就是优化应用后台耗电,由于大部分厂商预装项目要求最严格的正是应用后台待机耗电。

在这里插入图片描述

在这里插入图片描述
固然前台耗电咱们不会彻底无论,可是标准会放松不少。再来看看下面这张图,若是系统对你的应用弹出这个对话框,可能对于微信来讲,用户还能够忍受,可是对其余大多数的应用来讲,可能不少用户就直接把你加入到后台限制的名单中了。

耗电优化的第二个方向是符合系统的规则,让系统认为你耗电是正常的。

而 Android P 及以上版本是经过 Android Vitals 监控后台耗电,因此咱们须要符合 Android Vitals 的规则,目前它的具体规则以下。
在这里插入图片描述
能够看到,Android系统目前比较关心是后台 Alarm 唤醒、后台网络、后台 WiFi 扫描以及部分长时间 WakeLock 阻止系统后台休眠,由于这些都有可能致使耗电问题。

5.2 耗电监控

5.2.1 Android Vitals

Android Vitals 的几个关于电量的监控方案与规则,能够帮助咱们进行耗电监测。

在使用了一段时间以后,我发现它并非那么好用。以 Alarm wakeup 为例,Vitals 以每小时超过 10 次做为规则。因为这个规则没法作修改,不少时候咱们可能但愿针对不一样的系统版本作更加细致的区分。其次跟 Battery Historian 同样,咱们只能拿到 wakeup 的标记的组件,拿不到申请的堆栈,也拿不到当时手机是否在充电、剩余电量等信息。 下图是wakeup拿到的信息。

在这里插入图片描述
对于网络、WiFi scans 以及 WakeLock 也是如此。虽然 Vitals 帮助咱们缩小了排查的范围,可是依然没办法确认问题的具体缘由。

5.3 如何监控耗电

前面说过,Android Vitals并非那么好用,并且对于国内的应用来讲其实也根本没法使用。那咱们的耗电监控系统应该监控哪些内容,又应该如何作呢?首先,咱们看一下耗电监控具体应该怎么作呢?

  • 监控信息:简单来讲系统关心什么,咱们就监控什么,并且应该之后台耗电监控为主。相似 Alarm wakeup、WakeLock、WiFi scans、Network 都是必须的,其余的能够根据应用的实际状况。若是是地图应用,后台获取 GPS 是被容许的;若是是计步器应用,后台获取 Sensor 也没有太大问题。
  • 现场信息:监控系统但愿能够得到完整的堆栈信息,好比哪一行代码发起了 WiFi scans、哪一行代码申请了 WakeLock 等。还有当时手机是否在充电、手机的电量水平、应用前台和后台时间、CPU 状态等一些信息也能够帮助咱们排查某些问题。
  • 提炼规则:最后咱们须要将监控的内容抽象成规则,固然不一样应用监控的事项或者参数都不太同样。 因为每一个应用的具体状况都不太同样,能够用来参考的简单规则。

在这里插入图片描述
在这里插入图片描述

5.3.2 Hook方案

明确了咱们须要监控什么以及具体的规则以后,接下来咱们来看一下电量监控的技术方案。这里首先来看一下Hook 方案。Hook 方案的好处在于使用者接入很是简单,不须要去修改代码,接入的成本比较低。下面我以几个比较经常使用的规则为例,看看如何使用 Java Hook 达到监控的目的。

1,WakeLock
WakeLock 用来阻止 CPU、屏幕甚至是键盘的休眠。相似 Alarm、JobService 也会申请 WakeLock 来完成后台 CPU 操做。WakeLock 的核心控制代码都在PowerManagerService中,实现的方法很是简单,以下所示。

// 代理 PowerManagerService
ProxyHook().proxyHook(context.getSystemService(Context.POWER_SERVICE), "mService", this);

@Override
public void beforeInvoke(Method method, Object[] args) {
    // 申请 Wakelock
    if (method.getName().equals("acquireWakeLock")) {
        if (isAppBackground()) {
            // 应用后台逻辑,获取应用堆栈等等     
         } else {
            // 应用前台逻辑,获取应用堆栈等等
         }
    // 释放 Wakelock
    } else if (method.getName().equals("releaseWakeLock")) {
       // 释放的逻辑    
    }
}

2,Alarm
Alarm 用来作一些定时的重复任务,它一共有四个类型,其中ELAPSED_REALTIME_WAKEUP和RTC_WAKEUP类型都会唤醒设备。一样,Alarm 的核心控制逻辑都在AlarmManagerService中,实现以下。

// 代理 AlarmManagerService
new ProxyHook().proxyHook(context.getSystemService
(Context.ALARM_SERVICE), "mService", this);

public void beforeInvoke(Method method, Object[] args) {
    // 设置 Alarm
    if (method.getName().equals("set")) {
        // 不一样版本参数类型的适配,获取应用堆栈等等
    // 清除 Alarm
    } else if (method.getName().equals("remove")) {
        // 清除的逻辑
    }
}

除了WakeLock和Alarm外,对于后台 CPU,咱们可使用卡顿监控相关的方法;对于后台网络,一样咱们能够经过网络监控相关的方法;对于 GPS 监控,咱们能够经过 Hook 代理LOCATION_SERVICE;对于 Sensor,咱们经过 Hook SENSOR_SERVICE中的“mSensorListeners”,能够拿到部分信息。

最后,咱们将申请资源到的堆栈信息保存起来。当咱们触发某个规则上报问题的时候,能够将收集到的堆栈信息、电池是否充电、CPU 信息、应用先后台时间等辅助信息上传到后台便可。

5.3.3 插桩法

使用 Hook 方式虽然简单,可是某些规则可能不太容易找到合适的 Hook 点,并且在 Android P 以后,不少的 Hook 点都不支持了。出于兼容性考虑,我首先想到的是插桩法。以 WakeLock 为例:

public class WakelockMetrics {
    // Wakelock 申请
    public void acquire(PowerManager.WakeLock wakelock) {
        wakeLock.acquire();
        // 在这里增长 Wakelock 申请监控逻辑
    }
    // Wakelock 释放
    public void release(PowerManager.WakeLock wakelock, int flags) {
        wakelock.release();
        // 在这里增长 Wakelock 释放监控逻辑
    }
}

若是你对电量消耗又研究,那么确定知道Facebook 的耗电监控的开源库Battery-Metrics,它监控的数据很是全,包括 Alarm、WakeLock、Camera、CPU、Network 等,并且也有收集电量充电状态、电量水平等信息。不过,遗憾的是Battery-Metrics 只是提供了一系列的基础类,在实际使用时开发者仍然须要修改大量的源码。

6,安装包优化

如今市面上的App,小则几十M,大则上百M。安装包越小,下载时省流量,用户好的体验,下载更快,安装更快。那么对于安装包,咱们能够从哪些方面着手进行优化呢?

6,1 经常使用的优化策略

1,清理无用资源
在android打包过程当中,若是代码有涉及资源和代码的引用,那么就会打包到App中,为了防止将这些废弃的代码和资源打包到App中,咱们须要及时地清理这些无用的代码和资源来减少App的体积。清理的方法是,依次点击android Studio的【Refactor】->【Remove unused Resource】,以下图所示。
在这里插入图片描述
2,使用Lint工具

Lint工具仍是颇有用的,它给咱们须要优化的点:

  • 检测没有用的布局而且删除
  • 把未使用到的资源删除
  • 建议String.xml有一些没有用到的字符也删除掉

3,开启shrinkResources去除无用资源
在build.gradle 里面配置shrinkResources true,在打包的时候会自动清除掉无用的资源,但通过实验发现打出的包并不会,而是会把部分无用资源用更小的东西代替掉。注意,这里的“无用”是指调用图片的全部父级函数最终是废弃代码,而shrinkResources true 只能去除没有任何父函数调用的状况。

android {
        buildTypes {
            release {
                shrinkResources true
            }
        }
    }

除此以外,大部分应用其实并不须要支持几十种语言的国际化支持,还能够删除语言支持文件。

6.2 资源压缩

在android开发中,内置的图片是不少的,这些图片占用了大量的体积,所以为了缩小包的体积,咱们能够对资源进行压缩。经常使用的方法有:

  1. 使用压缩过的图片:使用压缩过的图片,能够有效下降App的体积。
  2. 只用一套图片:对于绝大对数APP来讲,只须要取一套设计图就足够了。
  3. 使用不带alpha值的jpg图片:对于非透明的大图,jpg将会比png的大小有显著的优点,虽然不是绝对的,可是一般会减少到一半都不止。
  4. 使用tinypng有损压缩:支持上传PNG图片到官网上压缩,而后下载保存,在保持alpha通道的状况下对PNG的压缩能够达到1/3以内,并且用肉眼基本上分辨不出压缩的损失。
  5. 使用webp格式:webp支持透明度,压缩比比,占用的体积比JPG图片更小。从Android 4.0+开始原生支持,可是不支持包含透明度,直到Android 4.2.1+才支持显示含透明度的webp,使用的时候要特别注意。
  6. 使用svg:矢量图是由点与线组成,和位图不同,它再放大也能保持清晰度,并且使用矢量图比位图设计方案能节约30~40%的空间。
  7. 对打包后的图片进行压缩:使用7zip压缩方式对图片进行压缩,能够直接使用微信开源的AndResGuard压缩方案。
apply plugin: 'AndResGuard'
    buildscript {
        dependencies {
            classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.1.7'
        }
    }
    andResGuard {
        mappingFile = null
        use7zip = true
        useSign = true
        keepRoot = false
        // add <your_application_id>.R.drawable.icon into whitelist.
        // because the launcher will get thgge icon with his name
        def packageName = <your_application_id>
                whiteList = [
        //for your icon
        packageName + ".R.drawable.icon",
                //for fabric
                packageName + ".R.string.com.crashlytics.*",
                //for umeng update
                packageName + ".R.string.umeng*",
                packageName + ".R.string.UM*",
                packageName + ".R.string.tb_*",
                packageName + ".R.layout.umeng*",
                packageName + ".R.layout.tb_*",
                packageName + ".R.drawable.umeng*",
                packageName + ".R.drawable.tb_*",
                packageName + ".R.anim.umeng*",
                packageName + ".R.color.umeng*",
                packageName + ".R.color.tb_*",
                packageName + ".R.style.*UM*",
                packageName + ".R.style.umeng*",
                packageName + ".R.id.umeng*"
        ]
        compressFilePattern = [
        "*.png",
                "*.jpg",
                "*.jpeg",
                "*.gif",
                "resources.arsc"
        ]
        sevenzip {
            artifact = 'com.tencent.mm:SevenZip:1.1.7'
            //path = "/usr/local/bin/7za"
        }
    }

6.3 资源动态加载

在前端开发中,动态加载资源能够有效减少apk的体积。除此以外,只提供对主流架构的支持,好比arm,对于mips和x86架构能够考虑不支持,这样能够大大减少APK的体积。

固然,除了上面提到的场景的优化场景外,Android App的优化还包括存储优化、多线程优化以及奔溃处理等方面。

相关文章
相关标签/搜索