关于android:Android-面试之必问性能优化

5次阅读

共计 21009 个字符,预计需要花费 53 分钟才能阅读完成。

对于 Android 开发者来说,懂得根本的利用开发技能往往是不够,因为不论是工作还是面试,都须要开发者懂得大量的性能优化,这对晋升利用的体验是十分重要的。对于 Android 开发来说,性能优化次要围绕如下方面开展:启动优化、渲染优化、内存优化、网络优化、卡顿检测与优化、耗电优化、安装包体积优化、平安问题等。

1,启动优化

一个利用的启动快慢是可能间接影响用户的应用体验的,如果启动较慢可能会导致用户卸载放弃该应用程序。

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

1.1.1 概念

对于 Android 应用程序来说,依据启动形式能够分为冷启动,热启动和温启动三种。

  • 冷启动:零碎不存在 App 过程(如 APP 首次启动或 APP 被齐全杀死)时启动 App 称为冷启动。
  • 热启动:按了 Home 键或其它状况 app 被切换到后盾,再次启动 App 的过程。
  • 温启动:温启动蕴含了冷启动的一些操作,不过 App 过程仍然存在,这代表着它比热启动有更多的开销。

能够看到,热启动是启动最快的,温启动则是介于冷启动和热启动之间的一种启动形式。下而冷启动则是最慢的,因为它会波及很多过程的创立,上面是冷启动相干的工作流程:

1.1.2 视觉优化

在冷启动模式下,零碎会启动三个工作:

  • 加载并启动应用程序。
  • 启动后立刻显示应用程序空白的启动窗口。
  • 创立应用程序过程。

一旦零碎创立应用程序过程,应用程序过程就会进入下一阶段,并实现如下的一些事件。

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

应用程序过程实现第一次绘制后,零碎过程会替换以后显示的背景窗口,将其替换为主流动。此时,用户能够开始应用该应用程序了。因为 App 利用过程的创立过程是由手机的软硬件决定的,所以咱们只能在这个创立过程中进行一些视觉优化。

1.1.3 启动主题优化

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

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

    <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 的累赘,造成性能的重大卡顿。

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 的内存治理模型也做了根本的介绍。

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 Manager wakeup 唤醒过多
  • 频繁应用部分唤醒锁
  • 后盾网络使用量过高
  • 后盾 WiFi Scans 过多

在应用了一段时间之后,我发现它并不是那么好用。以 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 的优化还包含存储优化、多线程优化以及奔溃解决等方面。

视频:
Android 性能优化大厂实战篇:内存优化 / 启动优化 / 耗电优化 /apk 瘦身 / 屏幕适配
备战 2022Android 面试:BAT 一线大厂高频面试题具体解析

原文: https://juejin.cn/post/6970613873040687141

正文完
 0