对于Android开发者来讲,懂得基本的应用开发技能每每是不够,由于无论是工做仍是面试,都须要开发者懂得大量的性能优化,这对提高应用的体验是很是重要的。对于Android开发来讲,性能优化主要围绕以下方面展开:启动优化、渲染优化、内存优化、网络优化、卡顿检测与优化、耗电优化、安装包体积优化、安全问题等。html
一个应用的启动快慢是可以直接影响用户的使用体验的,若是启动较慢可能会致使用户卸载放弃该应用程序。前端
对于Android应用程序来讲,根据启动方式能够分为冷启动,热启动和温启动三种。java
能够看到,热启动是启动最快的,温启动则是介于冷启动和热启动之间的一种启动方式。下而冷启动则是最慢的,由于它会涉及不少进程的建立,下面是冷启动相关的任务流程:python
在冷启动模式下,系统会启动三个任务:android
一旦系统建立应用程序进程,应用程序进程就会进入下一阶段,并完成以下的一些事情。web
应用程序进程完成第一次绘制后,系统进程会交换当前显示的背景窗口,将其替换为主活动。此时,用户能够开始使用该应用程序了。由于App应用进程的建立过程是由手机的软硬件决定的,因此咱们只能在这个建立过程当中进行一些视觉优化。面试
在冷启动的时候,当应用程序进程被建立后,就须要设置启动窗口的主题。目前,大部分的 应用在启动会都会先进入一个闪屏页(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>
设置主题的方式只能应用在要求不是很高的场景,而且这种优化治标不治本,关键还在于代码的优化。为了进行优化,咱们须要掌握一些基本的数据。编程
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。
日志方式
埋点方式是另外一种统计线上时间的方式,这种方式经过记录启动时的时间和结束的时间,而后取两者差值便可。首先,须要定义一个统计时间的工具类:
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") } }
在作启动优化的时候,能够借助三方工具来帮助咱们理清各个阶段的方法或者线程、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
这里能够你们普及下各个字端的含义:
Systrace开销较小,属于轻量级的工具,而且能够直观反映CPU的利用率。
Android系统每隔16ms就会从新绘制一次Activity,所以,咱们的应用必须在16ms内完成屏幕刷新的所有逻辑操做,每一帧只能停留16ms,不然就会出现掉帧现象。Android应用卡顿与否与UI渲染有直接的关系。
对于大多数手机的屏幕刷新频率是60hz,也就是若是在1000/60=16.67ms内没有把这一帧的任务执行完毕,就会发生丢帧的现象,丢帧是形成界面卡顿的直接缘由,渲染操做一般依赖于两个核心组件:CPU与GPU。CPU负责包括Measure,Layout等计算操做,GPU负责Rasterization(栅格化)操做。
所谓栅格化,就是将矢量图形转换为位图的过程,手机上显示是按照一个个像素来显示的,好比将一个Button、TextView等组件拆分红一个个像素显示到手机屏幕上。而UI渲染优化的目的就是减轻CPU、GPU的压力,除去没必要要的操做,保证每帧16ms之内处理完全部的CPU与GPU的计算、绘制、渲染等等操做,使UI顺滑、流畅的显示出来。
UI渲染优化的第一步就是找到Overdraw(过分绘制),即描述的是屏幕上的某个像素在同一帧的时间内被绘制了屡次。在重叠的UI布局中,若是不可见的UI也在作绘制的操做或者后一个控件将前一个控件遮挡,会致使某些像素区域被绘制了屡次,从而增长了CPU、GPU的压力。
那么如何找出布局中Overdraw的地方呢?很简单,就是打开手机里开发者选项,而后将调试GPU过分绘制的开关打开便可,而后就能够看到应用的布局是否被Overdraw,以下图所示。
蓝色、淡绿、淡红、深红表明了4种不一样程度的Overdraw状况,1x、2x、3x和4x分别表示同一像素上同一帧的时间内被绘制了屡次,1x就表示一次(最理想状况),4x表示4次(最差的状况),而咱们须要消除的就是3x和4x。
咱们知道,自定义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方法,绘制以前裁剪出一个区域,这样绘制的时候只在这区域内绘制,超出部分不会绘制出来。从新运行上面的代码,效果以下图所示。
Hierarchy Viewer 是 Android Device Monitor 中内置的一种工具,可以让开发者测量布局层次结构中每一个视图的布局速度,以及帮助开发者查找视图层次结构致使的性能瓶颈。Hierarchy Viewer能够经过红、黄、绿三种不一样的颜色来区分布局的Measure、Layout、Executive的相对性能表现状况。
打开
提高布局性能的关键点是尽可能保持布局层级的扁平化,避免出现重复的嵌套布局。若是咱们写的布局层级比较深会严重增长CPU的负担,形成性能的严重卡顿,关于Hierarchy Viewer的使用能够参考:使用 Hierarchy Viewer 分析布局。
在咱们优化过view的树形结构和overdraw以后,可能仍是感受本身的app有卡顿和丢帧,或者滑动慢等问题,咱们就要查看一下是否存在内存抖动状况了。所谓内存抖动,指的是内存频繁建立和GC形成的UI线程被频繁阻塞的现象。
Android有自动管理内存的机制,可是对内存的不恰当使用仍然容易引发严重的性能问题。在同一帧里面建立过多的对象是件须要特别引发注意的事情,在同一帧里建立大量对象可能引发GC的不停操做,执行GC操做的时候,全部线程的任何操做都会须要暂停,直到GC操做完成。大量不停的GC操做则会显著占用帧间隔时间。若是在帧间隔时间里面作了过多的GC操做,那么就会形成页面卡顿。
在Android开发中,致使GC频繁操做有两个主要缘由:
Android的内存抖动可使用Android Studio的Profiler进行检测。
而后,点击record记录内存信息,查找发生内存抖动位置,固然也可直接经过Jump to Source定位到代码位置。
为了不发生内存抖动,咱们须要避免在for循环里面分配对象占用内存,须要尝试把对象的建立移到循环体以外,自定义View中的onDraw方法也须要引发注意,每次屏幕发生绘制以及动画执行过程当中,onDraw方法都会被调用到,避免在onDraw方法里面执行复杂的操做,避免建立对象。对于那些没法避免须要建立对象的状况,咱们能够考虑对象池模型,经过对象池来解决频繁建立与销毁的问题,可是这里须要注意结束使用以后,须要手动释放对象池中的对象。
在前面Java基础环节,咱们对Java的内存管理模型也作了基本的介绍,参考连接:Android 面试之必问Java基础
在Java的内存模型中,将内存区域划分为方法区、堆、程序计数器、本地方法栈、虚拟机栈五个区域,以下图。
方法区
堆
虚拟机栈
本地方法栈
程序计数器
标记清除算法
标记清除算法主要分为有两个阶段,首先标记出须要回收的对象,而后咋标记完成后统一回收全部标记的对象;
缺点:
复制算法
将可用内存按空间分为大小相同的两小块,每次只使用其中的一块,等这块内存使用完了将还存活的对象复制到另外一块内存上,而后将这块内存区域对象总体清除掉。每次对整个半区进行内存回收,不会致使碎片问题,实现简单且效率高效。
缺点:
须要将内存缩小为原来的一半,空间代价过高。
标记整理算法
标记整理算法标记过程和标记清除算法同样,但清除过程并非对可回收对象直接清理,而是将全部存活对象像一端移动,而后集中清理到端边界之外的内存。
分代回收算法
当代虚拟机垃圾回收算法都采用分代收集算法来收集,根据对象存活周期不一样将内存划分为新生代和老年代,再根据每一个年代的特色采用最合适的回收算法。
所谓内存泄露,指的是内存中存在的没有用的确没法回收的对象。表现的现象是会致使内存抖动,可用内存减小,进而致使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分析工具解决。
在Android开发中,常常会遇到加载大图致使内存泄露的问题,对于这种场景,有一个通用的解决方案,即便用ARTHook对不合理图片进行检测。咱们知道,获取Bitmap占用的内存主要有两种方式:
*
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()); } });
方案一
在特定场景中获取当前占用内存大小,若是当前内存大小超过系统最大内存80%,对当前内存进行一次Dump(Debug.dumpHprofData()),选择合适时间将hprof文件进行上传,而后经过MAT工具手动分析该文件。
缺点:
方案二
将LeakCannary带到线上,添加预设怀疑点,对怀疑点进行内存泄露监控,发现内存泄露回传到服务端。
缺点:
改造主要涉及如下几点:
完成的改造步骤以下:
App的网络链接对于用户来讲, 影响不少, 且多数状况下都很直观, 直接影响用户对这个App的使用体验. 其中较为重要的几点:
流量 :App的流量消耗对用户来讲是比较敏感的, 毕竟流量是花钱的嘛. 如今大部分人的手机上都有安装流量监控的工具App, 用来监控App的流量使用. 若是咱们的App这方面没有控制好, 会给用户很差的使用体验。
电量 :电量相对于用户来讲, 没有那么明显. 通常用户可能不会太注意. 可是如电量优化中的那样, 网络链接(radio)是对电量影响很大的一个因素. 因此咱们也要加以注意。
用户等待 :也就是用户体验, 良好的用户体验, 才是咱们留住用户的第一步. 若是App请求等待时间长, 会给用户网络卡, 应用反应慢的感受, 若是有对比, 有替代品, 咱们的App极可能就会被用户无情抛弃。
网络分析能够借助的工具备Monitor、代理工具等。
Android Studio内置的Monitor工具提供了一个Network Monitor,能够帮助开发者进行网络分析,下面是一个典型的Network Monitor示意图。
Network Monitor实时跟踪选定应用的数据请求状况。 咱们能够连上手机,选定调试应用进程, 而后在App上操做咱们须要分析的页面请求。
网络代理工具备两个做用,一个是截获网络请求响应包, 分析网络请求;另外一个设置代理网络, 移动App开发中通常用来作不一样网络环境的测试, 例如Wifi/4G/3G/弱网等。
如今,可使用的代理工具备不少, 诸如Wireshark, Fiddler, Charles等。
对于网络优化来讲,主要从两个方面进行着手进行优化:
基于上面的方案,能够获得如下一些常见的解决方案:
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,依据网络状况获取不一样分辨率的图片
咱们使用淘宝或者京东的时候,会看到应用会根据网络状况,获取不一样分辨率的图片,避免流量的浪费以及提高用户的体验。
适当的使用缓存, 不只可让咱们的应用看起来更快, 也能避免一些没必要要的流量消耗,带来更好的用户体验。
1,打包网络请求
当接口设计不能知足咱们的业务需求时。例如,可能一个界面须要请求多个接口,或是网络良好,处于Wifi状态下时咱们想获取更多的数据等。这时就能够打包一些网络请求, 例如请求列表的同时, 获取Header点击率较高的的item项的详情数据。
2,监听设备状态
为了提高用户体验,咱们能够对设备的使用状态进行监听,而后再结合JobScheduler来执行网络请求.。比方说Splash闪屏广告图片, 咱们能够在链接到Wifi时下载缓存到本地; 新闻类的App能够在充电,Wifi状态下作离线缓存。
1,弱网测试
有几种方式来模拟弱网进行测试:
Android Emulator
一般,咱们建立和启动Android模拟器能够设置网络速度和延迟,以下图所示。
而后,咱们在启动时使用的emulator命令以下。
$emulator -netdelay gprs -netspeed gsm -avd Nexus_5_API_22
2,网络代理工具
使用网络代理工具也能够模拟网络状况。以Charles为例,保持手机和PC处于同一个局域网, 在手机端wifi设置高级设置中设置代理方式为手动, 代理ip填写PC端ip地址, 端口号默认8888。
事实上,若是咱们的应用须要播放视频、须要获取 GPS 信息,亦或者是游戏应用,耗电都是比较严重的。如何判断哪些耗电是能够避免,或者是须要去优化的呢?咱们能够打开手机自带的耗电排行榜,发现“王者荣耀”使用了 7 个多小时,这时用户对“王者荣耀”的耗电是有预期的。
假设这个时候发现某个应用他根本没怎么使用,可是耗电却很是多,那么就会被系统无情的杀掉。因此耗电优化的第一个方向是优化应用的后台耗电。
知道了系统是如何计算耗电的,咱们也就能够知道应用在后台不该该作什么,例如长时间获取 WakeLock、WiFi 和蓝牙的扫描等,以及后台服务。为何说耗电优化第一个方向就是优化应用后台耗电,由于大部分厂商预装项目要求最严格的正是应用后台待机耗电。
固然前台耗电咱们不会彻底无论,可是标准会放松不少。再来看看下面这张图,若是系统对你的应用弹出这个对话框,可能对于微信来讲,用户还能够忍受,可是对其余大多数的应用来讲,可能不少用户就直接把你加入到后台限制的名单中了。
耗电优化的第二个方向是符合系统的规则,让系统认为你耗电是正常的。
而 Android P 及以上版本是经过 Android Vitals 监控后台耗电,因此咱们须要符合 Android Vitals 的规则,目前它的具体规则以下。
能够看到,Android系统目前比较关心是后台 Alarm 唤醒、后台网络、后台 WiFi 扫描以及部分长时间 WakeLock 阻止系统后台休眠,由于这些都有可能致使耗电问题。
Android Vitals 的几个关于电量的监控方案与规则,能够帮助咱们进行耗电监测。
在使用了一段时间以后,我发现它并非那么好用。以 Alarm wakeup 为例,Vitals 以每小时超过 10 次做为规则。因为这个规则没法作修改,不少时候咱们可能但愿针对不一样的系统版本作更加细致的区分。其次跟 Battery Historian 同样,咱们只能拿到 wakeup 的标记的组件,拿不到申请的堆栈,也拿不到当时手机是否在充电、剩余电量等信息。 下图是wakeup拿到的信息。
对于网络、WiFi scans 以及 WakeLock 也是如此。虽然 Vitals 帮助咱们缩小了排查的范围,可是依然没办法确认问题的具体缘由。
前面说过,Android Vitals并非那么好用,并且对于国内的应用来讲其实也根本没法使用。那咱们的耗电监控系统应该监控哪些内容,又应该如何作呢?首先,咱们看一下耗电监控具体应该怎么作呢?
明确了咱们须要监控什么以及具体的规则以后,接下来咱们来看一下电量监控的技术方案。这里首先来看一下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 信息、应用先后台时间等辅助信息上传到后台便可。
使用 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 只是提供了一系列的基础类,在实际使用时开发者仍然须要修改大量的源码。
如今市面上的App,小则几十M,大则上百M。安装包越小,下载时省流量,用户好的体验,下载更快,安装更快。那么对于安装包,咱们能够从哪些方面着手进行优化呢?
1,清理无用资源
在android打包过程当中,若是代码有涉及资源和代码的引用,那么就会打包到App中,为了防止将这些废弃的代码和资源打包到App中,咱们须要及时地清理这些无用的代码和资源来减少App的体积。清理的方法是,依次点击android Studio的【Refactor】->【Remove unused Resource】,以下图所示。
2,使用Lint工具
Lint工具仍是颇有用的,它给咱们须要优化的点:
3,开启shrinkResources去除无用资源
在build.gradle 里面配置shrinkResources true,在打包的时候会自动清除掉无用的资源,但通过实验发现打出的包并不会,而是会把部分无用资源用更小的东西代替掉。注意,这里的“无用”是指调用图片的全部父级函数最终是废弃代码,而shrinkResources true 只能去除没有任何父函数调用的状况。
android { buildTypes { release { shrinkResources true } } }
除此以外,大部分应用其实并不须要支持几十种语言的国际化支持,还能够删除语言支持文件。
在android开发中,内置的图片是不少的,这些图片占用了大量的体积,所以为了缩小包的体积,咱们能够对资源进行压缩。经常使用的方法有:
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" } }
在前端开发中,动态加载资源能够有效减少apk的体积。除此以外,只提供对主流架构的支持,好比arm,对于mips和x86架构能够考虑不支持,这样能够大大减少APK的体积。
固然,除了上面提到的场景的优化场景外,Android App的优化还包括存储优化、多线程优化以及奔溃处理等方面。