当前位置:网站首页>再见篇:App专项技术优化
再见篇:App专项技术优化
2022-08-02 14:06:00 【一叶难遮天】
目录
写在前面
时间过的真快啊,不知不觉中对性能优化的学习已经进入尾声了,随之学习记录的笔记也进入了尾声。虽然是学完了,但是感觉还是一脸懵逼有木有,所以啊,后面还是要抽出时间来巩固复习,对这一系列的优化进行复盘总结。上一篇说的是Android的稳定性优化——《带你揭秘Android稳定性优化》,有需要的朋友可以看一下,今天主要是来说一下Android中的专项技术优化,这也是这个系列专栏的最后一篇,内容不算多,写完咱们就算是告一段落了!
一、列表页卡顿优化
列表页就是我们平常使用的ListView或者RecyclerView等控件展示的界面,这个基本上所有的项目中都能看到,算是Android中比较基础常用的控件了,承载了我们许许多多的业务功能,使用起来也是比较简单的,但是也是比较容易出问题的,比方说列表页的滑动卡顿问题。
1.1、常规方案
- convertView复用、使用ViewHolder:convertView的复用每个条目创建出的布局都会保存下来,当我们在下次展示的时候直接使用即可,同时还会使用ViewHolder,ViewHolder就是维护View引用的类,每个布局的控件通过findViewById之后会被保存下来,下次需要刷新的时候也是直接使用,绕过了再次findViewById的过程
- 耗时任务异步处理:如果在getView()等方法中有明显的耗时操作,可以采用异步的方式来处理
1.2、其它方案
上面这些手段都是针对列表页优化会使用的常规方案,对于一般的列表页其实已经够用了。但是对于大型项目或者是较为复杂的列表页还是会有卡顿发生,尤其是低端手机。下面就来说一下列表页优化的其它方案:
①、布局相关
前面的布局优化部分介绍过,布局文件的加载可能会很耗时,因为布局文件的加载是一个IO的过程,同时还伴随着反射,因此列表页布局的加载也是有可能导致列表页滑动的卡顿,因此可以采用以下方案:
- 减少布局层级、避免过度绘制
- 异步inflate或者X2C
这两种方式都可以起到优化布局文件加载慢的效果,具体的优化手段在这里就不说了,在前面有过专门的一篇介绍布局优化的文章,有需要的可以去翻看一下具体的使用方式:《你想知道的布局优化都在这里了》
②、图片相关
- 避免过大尺寸:GC频繁、内存抖动
- 滑动时取消加载:监听滑动事件
在内存优化的时候说了过高的内存占用可能会导致GC频繁、内存抖动,如果发生的是阻塞式GC,主线程被影响的时间就会更长,因此在列表页一定要避免过大的图片尺寸,同时需要注意避免加载出的图片宽高大于控件的宽高。为了让列表页的滑动更加流畅,还可以监听列表页的事件,在滑动的过程中取消图片的加载以及显示,只有用户停止的时候才来加载需要显示的图片。
内存优化详解:《你的应用内存优化了吗?》
③、线程优化
- 使用线程池收敛线程,降低线程优先级
- 避免UI线程时间片被抢占
错误的异步方式很可能会导致卡顿加剧,对于Android App来说,它所能创建出的线程数也不是无限多的,而且即使是创建出了很多的线程,对于Java这种抢占式的调度模型,只会抢走原本属于主线程的CPU时间片,导致主线程执行的更慢,因此在列表页中要做耗时操作的话,可以考虑使用线程池,同时将线程池中的线程优先级设置成后台优先级,这样就不会出现原本属于主线程的CPU时间片被异步任务抢走了。
线程优化详解:《Android线程优化你了解多少》
④、TextView优化
有人可能会问:为什么要来优化TextView,平时不都是直接使用setText()方法的,难道它也很耗时吗?是的,对于复杂文本原生的TextView也会导致卡顿。
- 原因:面对复杂文本性能不佳
- 绘制原理:BoringLayout 单行显示、StaticLayout 多行显示、DynamicLayout 可编辑的文本展示
- 展示类使用StaticLayout即可,性能优于DynamicLayout
- 异步创建StaticLayout
接下来我们根据上面的思想在Demo工程中来简单的实现一下,自定义CustomTextView类,使用它实现展示文本的功能,并且它的性能要超越TextView:
@Deprecated
public class CustomTextView extends View {
private String mText = "简单实践StaticLayout展示文本";
private TextPaint mTextPaint;
private StaticLayout mStaticLayout;
public CustomTextView(Context context) {
super(context);
initLabelView();
}
public CustomTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initLabelView();
}
private void initLabelView() {
mTextPaint = new TextPaint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(16 * getResources().getDisplayMetrics().density);
mTextPaint.setColor(Color.BLACK);
final int width = (int) mTextPaint.measureText(mText);
//使用异步来创建StaticLayout性能更高
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
mStaticLayout = new StaticLayout(mText, mTextPaint, (int) width, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0, false);
postInvalidate(); //触发对控件的重绘
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(mStaticLayout != null){
canvas.save();
canvas.translate(getPaddingLeft(), getPaddingTop());
mStaticLayout.draw(canvas);
canvas.restore();
}
}
}
这里我们只是简单的实践了一下,如果有兴趣的可以研究一下如何自定义一个功能更加强大的TextView,因为我们平时使用的TextView里面其实还配置了很多的属性,如果没有时间研究的可以直接使用FaceBook提供的这个库来方便简单的对TextView进行优化:TextLayoutBuilder:https://github.com/facebook/TextLayoutBuilder
⑤、其它优化
- SysTrace跟踪
- 注意字符串拼接
对于优化是无止境的,最好是根据SysTrace来跟踪具体的卡顿原因,来做针对性的优化。这里提一点我们开发中容易忽略的地方——字符串拼接,注意不要使用“+”这种字符串相加的拼接操作,而是应该使用StringBuilder通过append()方法拼接,尤其是针对字符串频繁操作的情况。
//方式一
String oldStr = bean.getTitle()+bean.getSuperChapterName();
//方式二
StringBuilder builder =new StringBuilder();
builder.append(bean.getTitle()).append(bean.getSuperChapterName());
String newStr = builder.toString();
二、存储优化
2.1、常规方案
- 确保IO操作发生在非主线程
- Hook或者AOP辅助
一般团队对存储的优化就是将IO操作放到异步任务当中,之前的文章介绍过其实可以通过Hook或者AOP的方式来做辅助的判断,这个思路实际上就是将耗时操作放到异步任务中避免卡顿主线程,其实只是一个粗粒度的优化而已。
2.2、其它方案
①、SharedPreferences相关
- 加载慢:初始化加载整个文件
- 全量写入:单次改动都会导致整体写入
- 卡顿:补偿策略导致
在实际项目中针对数据存储我们肯定会用到SharedPreferences,它是Android中的一种非常简单易用的轻量级存储方式,可以用来存储一些简单的数据类型。但是它也有一些缺点:
首先它在创建的时候它会将整个文件全部都加载到内存中,如果你的文件比较大那么加载一定会很耗时,虽然它的加载是在异步线程中做的,但是取值的方法会等待这个异步线程执行完成,这也就出现了UI线程等待异步线程的情况,所以在启动优化部分介绍了会提前使用异步线程来加载SP文件,有兴趣的可以去翻翻看启动优化那篇文章——《Android启动优化你真的了解吗?》。
第二个缺点是全量写入,就是不管你是使用commit()还是apply()方法去改动其中一个条目的数据,它会把全部内容都写入到文件中,即便是你多次写入同一个文件它也不会将多次修改合并成一次,每次写入都是全量写入,性能较差。
第三个缺点就是卡顿,Android其实提供了对SP的一种异步的apply()机制,但是这种机制可能会在程序崩溃或者其它一些异常情况的时候导致数据丢失,所以当应用收到系统广播或者被调用到onPause()等一些时机系统会强制将所有SharedPreferences对象保存在硬盘中,如果一直没有保存完成那么主线程就会一直被阻塞,这样就很容易会造成卡顿更甚者ANR。对SP来说它的设计初衷只是存储少量的数据,所以实际使用时不要用它来存储大量数据,并且它也不支持跨进程通信,这一点相信大家都听说过。
解决方案:使用微信开源的可替代方案MMKV,无论是性能还是安全性都要优于SP的XML
SharedPreferences替代者MMKV
- mmap和文件锁保证数据完整
- 增量写入、使用Protocol Buffer
- 支持从SharedPreferences中迁移数据
- GitHub地址:https://github.com/Tencent/MMKV
下面在Demo中简单的使用一下这个库,它的使用方式和SP有点像:
首先添加依赖:
//mmkv
implementation 'com.tencent:mmkv-static:1.2.1'
其次进行初始化:
MMKV.initialize(this);
存储数据:
MMKV.defaultMMKV().encode("key", "value");
String value = MMKV.defaultMMKV().decodeString("key");
LogUtils.i("MMKV存储的值为--->" + value);
结果:
②、日志存储优化
- 大量服务需要日志库支持
- 对于性能的要求:不影响性能、日志不丢失、安全
常规实现
- 每产生一个日志,写一遍到磁盘中:这种方案每次操作都去写一遍,如果操作非常频繁,肯定会因为频繁的IO操作影响程序性能,导致程序卡顿,但是这种方案的优点就是不会出现丢日志的情况
- 开辟一个内存buffer,先存buffer,再存文件:这种方案可以有效减少IO操作,保证程序的流畅性,但是这种方案存在一个致命的缺陷,就是内存中的日志不稳定,很可能会导致丢日志
解决方案:mmap
- 内存映射文件
- 优势:高性能、不丢失
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
详细了解的话可以参考这篇文章:https://www.cnblogs.com/huxiao-tee/p/4660352.html
针对这种思路在业界的优秀实现:微信的XLog和美团的Logan,如果你有高频日志的使用场景可以考虑这两个库,地址如下:
③、其它优化
- 常用数据的缓存,避免多次读取:对于项目中比较大的数据可以在读取之后直接缓存起来,避免多次读取造成的浪费
- 合理选择缓冲区Buffer大小:对于文件流的使用要合理的选择缓存区buffer的大小,一般设置4-8KB之间即可,buffer太小会导致IO操作次数增加,太大会导致申请时间变长
三、WebView异常监控
WebView可以说是一个很重要的控件,因为它承载了H5页面的展示功能,但是在Android中WebView出现的问题也是非常多的,尤其是性能和适配方面。关于性能的优化在业界也已经有了比较好的实践,比如容器预热和资源预加载等方式,具体的可以参考腾讯的VasSonic项目,它是一个轻量级的高性能Hybrid框架,具体的项目地址如下:
VasSonic: A Lightweight And High-performance Hybrid Framework
这一部分重点来说一下关于WebView的异常监控,对于用户来说,使用WebView加载H5页面,出现的最严重的情况就是页面打开后是一个空白页,所以这里要做的就是监控用户的白屏率,对于白屏出现的原因总结起来就一句话:
- WebView版本及对接业务方众多
解决思路
- 监控屏幕是否白屏,白屏则WebView有问题
- 确认白屏:所有像素一致则认为白屏
这里给出一个白屏检测的工具类,代码如下所示,isBlank()方法为true则认定为白屏:
//WebView白屏检测
public class BlankDetect {
/**
* 判断Bitmap是否都是一个颜色
* @param bitmap
* @return
*/
public static boolean isBlank(View view) {
Bitmap bitmap = getBitmapFromView(view);
if (bitmap == null) {
return true;
}
int width = bitmap.getWidth();
int height = bitmap.getHeight();
if (width > 0 && height > 0) {
int originPix = bitmap.getPixel(0, 0);
int[] target = new int[width];
Arrays.fill(target, originPix);
int[] source = new int[width];
boolean isWhiteScreen = true;
for (int col = 0; col < height; col++) {
bitmap.getPixels(source, 0, width, 0, col, width, 1);
if (!Arrays.equals(target, source)) {
isWhiteScreen = false;
break;
}
}
return isWhiteScreen;
}
return false;
}
/**
* 从View获取转换到的Bitmap
* @param view
* @return
*/
private static Bitmap getBitmapFromView(View view){
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
if (Build.VERSION.SDK_INT >= 11) {
view.measure(View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(view.getHeight(), View.MeasureSpec.EXACTLY));
view.layout((int) view.getX(), (int) view.getY(), (int) view.getX() + view.getMeasuredWidth(), (int) view.getY() + view.getMeasuredHeight());
} else {
view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
}
view.draw(canvas);
return bitmap;
}
}
OK,以上就是今天的主要内容了,内容不多,很容易消化吧,到这里关于性能优化的内容就要跟大家说再见了,后面有需要补充的再更新吧!
天也不早了,人也都走了,咱们江湖再见! 晚安各位!
边栏推荐
猜你喜欢
随机推荐
安装使用——百家CMS微商城说明文档(2)
C语言初级—从键盘接收一个整形并逆序输出
Camera Hal(Hal3)层修改Preview流
初识c语言指针
浮点数的运算方法
C语言——断言assert的使用
芝诺悖论的理解
【c】大学生在校学习c语言常见代码
spark写sql的方式
spark(standalone,yarn)
OpenCart迁移到其他服务器
Flink时间和窗口
Flask framework in-depth two
【c】小游戏---扫雷雏形
数据乱码问题—更改mysql字符编码
原码、补码、反码
c语言用scanf出错不安全的解决办法
liunx下mysql遇到的简单问题
VS Code无法安装插件之Unable to install because, the extension '' compatible with current version
Flask request application context source code analysis