App性能优化之---响应快 / 稳定性 / 省内存 / 体积小

  

在网上看到很多关于性能优化的文章,总结起来就是:快,稳,省,小。如下图所示:

接下来针对这四个方面进行讲解

                                                       

如何让 app 在运行过程过不卡顿,运行流畅,速度快,也就是说如何解决卡顿呢?我们先看看那些因素影响卡顿? 

1、ui的绘制,刷新等 
2、冷启动,热启动等 
3、页面跳转,前后台切换 
4、点击事件,滑动,系统事件

UI

Android 显示过程可以简单概括为:Android 应用程序把经过测量,布局、绘制后的 surface 缓存数据,通过 SurfaceFlinger 把数据渲染到显示屏幕上, 通过 Android 的刷新机制来刷新数据。也就是说应用层负责绘制,系统层负责渲染,通过进程间通信把应用层需要绘制的数据传递到系统层服务,系统层服务通过刷新机制把数据更新到屏幕上。

换一种方式说:Android 系统每隔 16ms 发出 VSYNC 信号,触发对 UI 进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需的 60FPS。(注:FPS 表示每秒传递的帧数。)在理想情况下,60 FPS 就感觉不到卡,这意味着每个绘制时长应该在16 ms 左右。如果某个操作花费的时间是 24ms ,系统在得到 VSYNC 信号时就无法正常进行正常渲染,这样就发生了丢帧现象。也就是延迟了,这种现象在执行动画或滑动列表比较常见,还有可能是你的 Layout 太过复杂,层叠太多的绘制单元,无法在 16ms 完成渲染,最终引起刷新不及时.

那么我们如何解决呢,主要从两点入手:布局优化,绘制优化

布局优化

1、避免ui布局优化可以先从合理使用背景色开始,比如:如果子view和父布局公用一个背景色就没有必要了。
2、减少不必要的嵌套,一般建议不超过5层
3、合理使用各种布局,尽量使用 LinearLayout 和 FrameLayout,因为 RelativeLayout 需要比较复杂,测绘也比较费时
4、合理使用 include、merge 和 ViewStub,使用include和merge增加复用,减少层级; ViewStub 按需加载。

绘制优化

我们之前说过根据 Android 系统显示的原理,View 的绘制频率保证 60fps 是最佳的,这就要求每帧绘制时间不超过16ms(16ms = 1000/60),因此要减轻 onDraw() 的负担。所以在绘制时要注意两点:

1、onDraw 中不要创建新的局部对象。
2、onDraw 方法中不要做耗时的任务。

还有就是刷新,刷新的话尽量减少不必要的刷新和尽可能减少刷新面积

启动优化

冷启动

冷启动是指安装 apk 后首次启动应用程序,或者应用程序上次结束,进程被杀死后重新打开app.


1、点击桌面App图标,Launcher进程采用Binder IPC向system_server进程发起startActivity请求;
2、system_server进程接收到请求后,向zygote进程发送创建进程的请求;
3、Zygote进程fork出新的子进程,即App进程;
4、App进程,通过Binder IPC向sytem_server进程发起attachApplication请求;
5、system_server进程在收到请求后,进行一系列准备工作后,再通过binder IPC向App进程发送scheduleLaunchActivity请求;
6、App进程的binder线程(ApplicationThread)在收到请求后,通过handler向主线程发送LAUNCH_ACTIVITY消息;
7、主线程在收到Message后,通过发射机制创建目标Activity,并回调Activity.onCreate()等方法。

到此,App便正式启动,开始进入Activity生命周期,执行完onCreate/onStart/onResume方法

热启动

热启动是用户 Back 退出应用程序,然后又重新启动,应用程序会再次执行 Activity 的 onCreate(),但会从 Bundle(savedInstanceState)获取数据,我们平时应用成勋崩溃,不也是通过该方法保存数据的吗。

针对启动方式的优化

1、Application 的创建过程中尽量少的进行耗时操作
比如:onCreate() 中进行友盟,bugly, okhttp,地图,推送等 init() 等操作。如果是必须在 onCreate 中进行的如:okhttp 等网络请求框架我们在 onCreate 中进行,其他的友盟,百度地图啥的我们可以等程序起来后再 onResume 方法中执行,bugly 等 sdk 可以异步加载。

统计SDK执行耗时,在代码之间插入Debug.startMethodTracing(filename)和Debug.stopMethodTracing()就可以追踪所包含代码的执行耗时,这些数据会写入一个.trace文件中,只要导出这个文件就可以看到详细信息了

//代码追踪开始,参数是trace文件保存在手机的全路径名  
Debug.startMethodTracing(this.getExternalFilesDir(Environment.MEDIA_MOUNTED).getPath() + "01");
        //EventBus初始化
        EventBus.getDefault().register(this);
        // 百度地图的初始化,在使用SDK各组间之前初始化context信息,传入ApplicationContext
        SDKInitializer.initialize(this);
        SDKInitializer.setCoordType(CoordType.BD09LL);
        //初始化友盟
        initUm();
        //初始化路由ARouter
        initRouter(this);
        //初始化日志收集
        initCrashReport();
        //初始化http
        initRxHttp();
        //代码追踪结束
        Debug.stopMethodTracing();

执行以上代码,然后通过将在cmd中输入adb pull [trace文件保存在手机的全路径名] 导出trace文件到电脑中,再通过Android Studio打开文件

从图中可以看到代码中每个方法的执行时间和占用时间的百分比,initRouter时间占比55.6%、initUm占比20.1%、SDKInitializer.initialize占比19.5%,主要是这个三个初始化占用了大多数时间,它们分别是路由框架ARouter,友盟和百度地图的初始化

一些常用的优化思路:

  • 考虑异步初始化第三方组件,不阻塞主线程
  • 延迟第三方的初始化时间,因为异步初始化有可能遇到初始化还没完成主线程已经用到的情况,这种情况可以考虑延时到第三方组件使用之前进行初始化
  • 数据库、IO操作、网络请求尽量不要在Application中执行,能异步初始化的就尽量异步,如果不能异步初始化就尽量延时,不要在Application中创建线程池
  • 在首页Activity中,布局尽量减少嵌套,在onCreate、onStart、onResume方法中尽量避免耗时操作

因为首页activity要用到组件ARouter,所以我考虑把它的初始化延时到SplashActivity中的使用前,经测试,百度地图不能在子线程初始化,所以考虑延时,然后友盟初的始化放在子线程

    //在Application中创建工作线程进行相关初始化
    private void initOnWorkThread() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                //将线程设置为后台,避免和主线程争抢资源
                Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
                initUm();
                initCrashReport();
            }
        }).start();
    }
 
 
        //将下面初始化延迟到首次显示Activity的onCreate方法中进行,界面完成以后才执行run方法
     
        findViewById(R.id.splash_view).post(new Runnable() {
            public void run() {
                //初始化ARouter
                ARouter.init(ComApplication.getInstance());
                // 在使用 SDK 各组间之前初始化 context 信息,传入 ApplicationContext
                SDKInitializer.initialize(ComApplication.getInstance());
                SDKInitializer.setCoordType(CoordType.BD09LL);
 
                //登录等其他操作
                ........
            }
        });

优化前后对比

优化前时间 优化后时间 时间相差 提升百分比
1560       1009    551  35.3%

优化启动速度整整提升了35.3%


2、SplashActivity设置主题背景
举个例子:使用以下几种主题,看看APP的启动速度
@android:style/Theme.NoTitleBar.Fullscreen           平均启动时间:126.8ms
@android:style/Theme.Black                                    平均启动时间:160ms
默认(根据操作系统自动选择)                                平均启动时间:174.8ms

可以得出一个结论:使用一个没有ActionBar的主题,比较快,而如果连StatusBar也去掉了,速度最快!
原因是这样的,启动一个Activity的时候,SetContentView中会加载布局文件

public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

从Activity的attach方法可以知道,mWindow的实体是PhoneWindow:
1. 初始化DecorView(对于一个window来说最顶级的view) 
2. 根据Window的属性选择合适的布局模板add到DecorView中 
3. 将我们的布局add到一个id为com.android.internal.R.id.content的ViewGroup中

如果Activity的主题设为FullScreen,无需加载ActionBar减少一层布局,有效提高启动速度。

另外将主题的Background进行处理,使得Splash在启动时根本没有加载实际的View,而仅仅是加载了主题,待Activity初始化完成以后,再渲染View,这样就避免了白屏和空屏的等待时常,让用户感觉到启动速度快

<item name="android:windowBackground">@drawable/welcome_layler_drawable</item>

通过将生成的drawable设置为background的形式最终并不会生成任何View,减小View绘制占用的时间提升启动速度!

通过实验,发现市面上很多的APP(高德地图、大众点评、百度地图、ofo小黄车等)都是采取了类似的方式,通过设置一个FullScreen主题的Activity,并设置background为和Splash布局类似的形式,能够做到点下图标的即刻,展现界面。


BlockCanary分析android卡顿

1、Service TimeOut:  未在规定时间执行完成:前台服务 20s,后台 200s
2、BroadCastQueue TimeOut: 未在规定时间内未处理完广播:前台广播 10s 内, 后台 60s 内
3、ContentProvider TimeOut:  publish 在 10s 内没有完成
4、Input Dispatching timeout:  5s 内未响应键盘输入、触摸屏幕等事件

卡顿(ANR)检测原理    源文
    从 ANR原理 可以知道,当一个事件处理时间超过阈值就会触发ANR。dispatchMessage花费的时间就是每条消息处理时间。

从Looper源码中我们发现,执行dispatchMessage前后都有一个logging打印,并且Looper提供了注册logging的方法。可以在MainThread Looper中注册一个logging,在每条消息dispatchMessage前后,都能收到一条打印记录。通过记录dispatchMessage之前的时间t1和dispatchMessage执行之后的log时间t2,totalTime = t2-t1得到该事件执行时间。


 
BlockCanary卡顿检测流程



                                                                  稳

内存溢出的几点原因: 
1、资源释放问题 
       程序代码的问题,长期保持某些资源,如Context、Cursor、IO流的引用,资源得不到释放造成内存泄露。 
2、对象内存过大问题 
       保存了多个耗用内存过大的对象(如 Bitmap、XML文件),造成内存超出限制。 
3、static关键字的使用问题 
       static是Java中的一个关键字,当用它来修饰成员变量时,那么该变量就属于该类,而不是该类的实例。所  以用static修饰的变量,它的生命周期是很长的。

4、避免OOM的第一步就是要尽量减少新分配出来的对象占用内存的大小,尽量使用更加轻量的对象
使用ArrayMap/SparseArray而不是HashMap等传统数据结构,通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效在于他们避免了对key与value的autobox自动装箱,并且避免了装箱后的解箱

如何避免OOM异常  
1、图片过大导致OOM 
   方法 1 :等比例缩小图片 
BitmapFactory.Options options = new BitmapFactory.Options(); 
 options.inSampleSize = 2;  

 方法2 :对图片采用软引用,及时地进行recyle()操作 
SoftReference<Bitmap> bitmap = new SoftReference<Bitmap>(pBitmap); 
if(bitmap != null){ 
 if(bitmap.get() != null && !bitmap.get().isRecycled()){ 
bitmap.get().recycle(); 
 bitmap = null; 
} } 

2、界面切换导致OOM 
有时候我们会发现这样的问题,横竖屏切换 N次后 OOM了。 这种问题没有固定的解决方法,我们从以下几个方面下手分析。 
 1、看看页面布局当中有没有大的图片,比如背景图之类的。 
去除xml中相关设置,改在程序中设置背景图(放在onCreate()方法中): 
 Drawable drawable = getResources().getDrawable(R.drawable.id); 
ImageView imageView = new ImageView(this); 
imageView.setBackgroundDrawable(drawable); 
在Activity destory时注意,drawable.setCallback(null); 防止Activity得不到及时的释放。 

2、使用完BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源时,在Activity中的OnDestry中及时的关闭、注销或者释放内存。 

内存泄漏出现的场景:      详细分析
      (1)、单例设计模式造成的内存泄漏
      (2)、非静态内部类创建的静态实例造成的内存泄漏
      (3)、Handler造成的内存泄漏
      (4)、线程造成的内存泄漏
      (5)、资源未关闭造成的内存泄漏

       常见的解决方案:
        1、尽量使用Application的Context而不是Activity的
        2、使用弱引用或者软引用漏
        3、手动设置null,解除引用关系漏
        4、将内部类设置为static,不隐式持有外部的实例漏
        5、在使用完BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源时,一定要在Activity中的OnDestry中及时的关闭、注销或者释放内存,

             

                                                   
 

省电

5.0之后才有像样的方案,讲实话这个优化的优先级没有前面几个那么高,但是我们也要了解一些避免耗电的坑,至于更细的耗电分析可以使用这个Battery Historian。

Battery Historian 是由Google提供的Android系统电量分析工具,从手机中导出bugreport文件上传至页面,在网页中生成详细的图表数据来展示手机上各模块电量消耗过程,最后通过App数据的分析制定出相关的电量优化的方法。

我们来谈一下怎么规避电老虎吧。

谷歌推荐使用JobScheduler,来调整任务优先级等策略来达到降低损耗的目的。JobScheduler可以避免频繁的唤醒硬件模块,造成不必要的电量消耗。避免在不合适的时间(例如低电量情况下、弱网络或者移动网络情况下的)执行过多的任务消耗电量。

具体功能:
1、可以推迟的非面向用户的任务(如定期数据库数据更新);
2、当充电时才希望执行的工作(如备份数据);
3、需要访问网络或 Wi-Fi 连接的任务(如向服务器拉取配置数据);
4、零散任务合并到一个批次去定期运行;
5、当设备空闲时启动某些任务;
6、只有当条件得到满足, 系统才会启动计划中的任务(充电、WIFI…)。

同时谷歌针对耗电优化也提出了一个懒惰第一的法则:
减少:你的应用程序可以删除冗余操作吗?例如,它是否可以缓存下载的数据而不是重复唤醒无线电以重新下载数据?
推迟:应用是否需要立即执行操作?例如,它可以等到设备充电才能将数据备份到云端吗?
合并:可以批处理工作,例如几十个应用程序是否真的有必要在不同时间打开收音机发送邮件?

省内存

主要是加载图片,动不动就 OOM,对于图片的压缩无非是:
1、图片尺寸压缩
2、图片质量压缩

Glide就是采用了 Lrucache 和 LruDiskCache 推荐使用。
Fresco 采用匿名共享内存,更加节省内存。

 
                                               小                        

小大多指应用程序apk体积要小。我们先看看一个apk文件有哪些解压后有哪些文件:

使用Analyze减少APK体积
project—>app—>bulid—>outputs—>apk—>[XXX].apk,双击[XXX].apk即可---打开Analyze
 

File                              原size           优化后    
 
assets                        13.4M               7.5M
lib                               12.6M               6.4M
res                              16.2M               8.5M
classes1.dex              7.9M                4.4M
classes2.dex              2.1M               492kb
resources.arsc           2.6M               408kb

lib:包含so文件,还有armeabi, armeabi-v7a, arm64-v8a, x86, x86_64, and mips
优化:
 ndk {
        abiFilters "armeabi", "armeabi-v7a"
    }
1、市面上主流机型都是arm架构,只保留armeabi或者armeabi-v7a
分析用户手机的cpu
舍弃so之前需要进行用户cpu型号的统计,花了几个版本统计了用户的cpu型号,然后排除了没有或少量用户才会用到的so,以达到瘦身的目的。
@NonNull
public static String getCpuName() {
    String name = getCpuName1();
    if (TextUtils.isEmpty(name)) {
        name = getCpuName2();
        if (TextUtils.isEmpty(name)) {
            name = "unknown";
        }
    }
    return name;
}

private static String getCpuName1() {
    String[] abiArr;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        abiArr = Build.SUPPORTED_ABIS;
    } else {
        abiArr = new String[]{Build.CPU_ABI, Build.CPU_ABI2};
    }

    StringBuilder abiStr = new StringBuilder();
    for (String abi : abiArr) {
        abiStr.append(abi);
        abiStr.append(',');
    }
    return abiStr.toString();
}

private static String getCpuName2() {
    try {
        FileReader e = new FileReader("/proc/cpuinfo");
        BufferedReader br = new BufferedReader(e);
        String text = br.readLine();
        String[] array = text.split(":\\s+", 2);
        e.close();
        br.close();
        return array[1];
    } catch (IOException var4) {
        var4.printStackTrace();
        return null;
    }
}

注意:
如果你用了RN,那么对于x86需要谨慎的保留,否则可能会出现用户找不到so而崩溃的情况。稍有不慎就可能会出现开机崩的情况。
so这个东西还是比较危险的,虽然可以通过统计cpu型号来降低风险,但还是推荐发布app前走一遍大量机型的云测
推荐在崩溃系统中上传用户cpu型号的信息,我们可以在第一时间知道因找不到so引起的崩溃量,至于是否需要增加so就看问题的严重程度了。


res:一些不会被编译到resources.arsc的资源文件。如drawable文件、layout文件等
优化:
1、只保留一套图,非重要的图动态加载,
2、保真压缩图片,
3、使用webp替代png体积更小,
4、使用lint删除无用资源minifyEnabled true, shrinkResources true

这是谷歌给出的建议,简单来说就是:VectorDrawable->WebP->Png->JPG
如果是纯色的icon,那么用svg
如果是两种以上颜色的icon,用webp
如果webp无法达到效果,选择png
如果图片没有alpha通道,可以考虑jpg

阿里系、腾讯系的产品都采用了一套图走天下的路子。这样的做法还是有利有弊的,权衡之下我给出如下建议:
聊天表情就出一套图,放在hdpi中
纯色小icon用svg做
背景等大图,出一套放在xhdpi中
大多数图片放在xxhdpi中


assets:一些通过AssetManager能够检索到的资源。如MP3、字体、webp等资源文件。
优化:
1、删除无用字体,
2、动态下载,MP3、字体、webp等使用时下载,
3、MP3、字体、webp等放在本地先压缩使用时解压

resources.arse:包括了所有可以被编译的位于res/values/目录下的编译后的二进制资源文件
优化:删除无用的语言,只保留中文

class.dex:包含了所有的Java文件编译后的class文件,单个的 classes.dex 文件可以容纳大约 64K 方法。如果你达到了这个限制,你必须要在你的工程中启用   multiDexEnabled true,通过设置 minifyEnabled 为 true,混淆将会移除所有未使用的方法、指令以减小 classes.dex 文件。
每个模块启用了混淆之后我们的 classes.dex 大小减小了几乎 50%。同时你可以看到方法数从 29897 降到15168(几乎 50%)。


AndroidManifest.xml:AndroidMainifest文件的权限、声明等配置

 

通常我减小 apk 体积的方式都是:先用 studio 自带的代码扫描分析工具 lint 删除无用资源;开启混淆,设置   shrinkResources true和 minifyEnabled true;当然你也可以借助第三方工具如 :乐固加固,360压缩啥的;还有注意不要重复使用库;插件化,比如功能模块放在服务器上,按需下载,可以减少安装包大小等都是常见的减少 apk 体积的方式。                                                                                 

其他

这都是本人的一些建议:

  • 1、序列化采用推荐的 Parcelable 代替 Serializable

  • 2、集合如果是插入和删除用的多,建议使用 LinkList。如果修改用的多,建议 ArrayList。

  • 3、写程序要思考,避免创建不必要的对象。

  • 4、对常量使用 static final,适用于基本类型和 String 常量。

  • 5、使用增强的 for 循环语法(foreach)。

  • 6、避免使用浮点数,浮点数比 Android 设备上的整数慢约2倍。

  • 7、尽可能少用 wrap_content,wrap_content 会增加布局 measure 时计算成本。

  • 8、合理使用动画,某些情况下可以用硬件加速方式来提供流畅度,或者采用自定义view代替动画,最后记得在Activity的ondestory()方法中调用Animation.cancle()进行动画停止。

  • 9、注意 webview 和 handler,一般在首次加载后 webview 就会存在于内存中,容易内存泄漏。

  • 10、数据量比较大或者内存比较宽裕考虑 HashMap,其他建议使用 SpareArray

最后,我们一定要学会使用 Android Studio 自带的各种工具如:

  • Lint:提示未使用到资源,不规范的代码,优化建议等。

    使用:选择 Analyze > Inspect Code 具体百度

  • 使用 Android Profiler 查看内存,已经各个操作内存和网络的变化。

  • 借助第三方工具,这个就多了去了,比如 LeakCanary,MemoryAnalyzer 等

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值