android性能优化实践与总结(包含启动,内存优化)

Posted 六道对穿肠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了android性能优化实践与总结(包含启动,内存优化)相关的知识,希望对你有一定的参考价值。

应用中性能优化实践与总结(精心总结)

任何优化都需要进行检测,以数据说话,优化前和优化后有了怎样的提升

[TOC]

启动优化

检测启动时间

检测工具任选其一

  1. hugo 插件 ,
  2. 自己定义时间开始和结束手动计算时间.
  3. AOP 工具 AspectJ

adb的am start命令启动Activity测量耗时

从点击应用的启动图标開始创建出一个新的进程直到我们看到了界面的第一帧, 这段时间就是应用的启动时间

adb shell am start -W 包名/.第一个activity

冷启动

Activi
TotalTime: 1074
WaitTime: 1075
Complete

热启动

TotalTime: 115
WaitTime: 117
Complete

优化后冷启动


TotalTime: 773
WaitTime: 774
Complete


热启动

TotalTime: 114
WaitTime: 115
Complete

注意有个点

1、ThisTime:一般和 TotalTime 时间一样。除非在应用启动时开了一个透明的
Activity 预先处理一些事再显示出主 Activity,这样将比 TotalTime 小。

2、TotalTime:应用的启动时间。包含创建进程+Application 初始化+Activity 初
始化到界面显示。

3、WaitTime:一般比 TotalTime 大点,包含系统影响的耗时。

android studio 中利用Displayed 过滤关键字检测

查看优化后数据

Displayed com.sinochem.www.car.owner/.activity.WelcomeActivity: +1s104ms

hugo依赖库测量耗时

使用方法

  1. 项目根目录build.gradle添加hugo插件依赖
classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1'
  1. 主工程或者library的录build.gradle中声明hugo插件。
apply plugin: 'com.jakewharton.hugo'

注意在每个lib里面都要加上 你想在那个lib里面打印就在那个lib里面加上这句
3. 在类或方法上声明@DebugLog注解。


    @Override @DebugLog
    public void onCreate() 
        super.onCreate();

项目中使用

在MyApplication 的onCreate 上面加上注解

效果

2021-01-31 15:03:01.871 26943-26943/com.sinochem.www.car.owner V/MyApplication: ⇢ onCreate()
2021-01-31 15:03:02.161 26943-26943/com.sinochem.www.car.owner V/MyApplication: ⇠ onCreate [289ms]

过滤关键字就是在哪个方法上面加上的DebugLog.比如在onCreate方法上加的, 过滤关键字就是onCreate.

在打印的列表搜索方法名称,就可以知道该方法的运行耗时。

TODO注解 打点检测时间

自己写一个时间检测的工具类- 使用自定义注解实现 AOP模式



启动优化方案

1.异步线程优化方案:

在MyApplication 初始化一些第三方库的时候使用线程池异步加载

举例:

 ThreadManager.getDownloadPool().execute(new Runnable() 
            @Override
            public void run() 
                //屏幕适配
                configUnits();
//                mCounDownLatch.countDown();
            
        );

原来的时间:

2021-01-31 15:03:01.871 26943-26943/com.sinochem.www.car.owner V/MyApplication: ⇢ onCreate()
2021-01-31 15:03:02.161 26943-26943/com.sinochem.www.car.owner V/MyApplication: ⇠ onCreate [289ms]

效果:

2021-01-31 16:46:45.652 26568-26568/com.sinochem.www.car.owner V/MyApplication: ⇢ onCreate()
2021-01-31 16:46:45.882 26568-26568/com.sinochem.www.car.owner V/MyApplication: ⇠ onCreate [229ms]

优化了60多 毫秒.

主线程需要异步线程的结果

可以采用方案 CountDownLauch 同步计数器,同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信

不仅可以用在这里. 也可以用在其他地方.

比如在第一个页面就需要异步的结果,就可以使用 CountDownLauch

countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。

是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。

使用方法:

  1. 创建
 private CountDownLatch mCounDownLatch = new CountDownLatch(1);

这里的1 是指有几个地方用到了,也就是说异步任务有几个需要主线程确定执行完毕的.

  1. 使用
   ThreadManager.getDownloadPool().execute(new Runnable() 
            @Override
            public void run() 
                //屏幕适配
                configUnits();
                mCounDownLatch.countDown();
            
        );

//在代码的最后 也就是需要确定的地方加上这个
   try 
            mCounDownLatch.await();
         catch (InterruptedException e) 
            e.printStackTrace();
        

这样就形成了 必须等屏幕适配结束 才继续.
可以看到运行时间289ms

2.延迟加载方案

2.1 常规实现

在 MainActivity#OnCreate 执行一个 postDelayed(Runnable r, long delayMillis)

Handler.postDelay()不能确定具体需要 delay 的时间.
如果时间设置过短,那么此时 UI 还没渲染完毕,这势必会阻塞到 UI 的渲染,如果过长,那么又导致延迟时间变长了。

2.2 闲时机制方案

IdleHandler(闲时机制) 处理延迟加载

IdleHandler是一个回调接口,可以通过MessageQueue的addIdleHandler添加实现类。当MessageQueue中的任务暂时处理完了(没有新任务或者下一个任务延时在之后),这个时候会回调这个接口,返回false,那么就会移除它,返回true就会在下次message处理完了的时候继续回调。

源码位置:MessageQueue.IdleHandler

自建一个队列保存任务

//LinkedList 实现了Deque 接口 Deque接口继承了Queue 所以可以这样创建对象
    private Queue<Runnable> delayTasks = new LinkedList<>();

使用

    private MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() 
        @Override
        public boolean queueIdle() 
            LogUtils.d("queueIdle调用");
            if (delayTasks.size() > 0) 
                LogUtils.d("从队列取出");
                Runnable runnable = delayTasks.poll();
                if (runnable != null) 
                    runnable.run();
                
            
            return !delayTasks.isEmpty();
            //delayTasks非空时返回ture表示下次继续执行
            //为空时返回false系统会移除该IdleHandler不再执行
        
    ;

2.3 使用启动器

TODO 写一个启动器处理启动任务

AppStartFaster:

AppStartFaster 项目 主要是通过 ClassName 找到相应的 Task,

阿里 alpha项目 :

是通过 taskName 找到相应的 Task,并且需要指定 ITaskCreator。两种方式各有优缺点,具体看使用场景。

3.优化应用启动时的体验白屏问题

对于应用的启动时间,仅仅能是尽量的避免一些耗时的、非必要的操作在主线程 中,这样相对能够缩减一部分启动的耗时,另外一方面在等待第一帧显示的时间
里,能够增加一些配置以增加体验,比方增加 Activity 的 background,这个背 景会在显示第一帧前提前显示在界面上。

单独做个主题 也可以做个透明的样式 都可以我这里做了图片

  1. 替换背景图
 <style name="AppTheme.welcome" parent="@style/Theme.AppCompat.Light.NoActionBar">
       <item name="android:windowBackground">@android:color/transparent</item>
   </style>
  1. 透明的样式
<style name="AppTheme.welcome" parent="@style/Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowBackground">@android:color/transparent</item>
    </style>

然后可以在下一个页面恢复样式

Override
protected void onCreate(Bundle savedInstanceState) 
setTheme(R.style.AppTheme); super.onCreate(savedInstanceState);

4.其他优化方式

  1. 在 Application 的构造器方法、attachBaseContext()、onCreate()方法中不要 进行耗时操作的初始化,一些数据预取放在异步线程中,能够採取 Callable 实现。

  2. 对于 sp 的初始化,由于 sp 的特性在初始化时候会对数据所有读出来存在内 存中,所以这个初始化放在主线程中不合适,反而会延迟应用的启动速度,对于 这个还是须要放在异步线程中处理。

  3. 对于 MainActivity,由于在获取到第一帧前。须要对 contentView 进行測量 布局绘制操作,尽量降低布局的层次。考虑 StubView 的延迟载入策略。当然在 onCreate、onStart、onResume 方法中避免做耗时操作。

  4. 不创建全局静态对象,而是转为单例模式,其中应用仅在第一次访问对象时初始化它们。此外,考虑使用依赖注入框架(如 Dagger),它们会在首次注入时创建对象和依赖项。

5. 其他注意点

  • Application重复启动

多进程任务导致Application重复启动

解决方案:

把下面代码放到MyApplication开头

 //多进程会重复调用
        String curProcessName = getCurrentProcessName(this);
        if (!curProcessName.equals(getPackageName())) 
            return;
        
/**
     * 获取当前的进程名
     *
     * @param context:上下文
     * @return :返回值
     */
    public String getCurrentProcessName(Context context) 
        int pid = android.os.Process.myPid();
        ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningAppProcessInfo> runningApps = am.getRunningAppProcesses();
        if (runningApps == null) 
            return null;
        
        for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) 
            if (procInfo.pid == pid) 
                return procInfo.processName;
            
        
        return null;
    

StrictMode 工具

StrictMode主要检测两大问题:线程策略(TreadPolicy)和VM策略(VmPolicy)。

ThreadPolicy线程策略:

自定义的耗时调用,使用detectCustomSlowCalls()开启;
磁盘读取操作,使用detectDiskReads()开启;
磁盘写入操作,使用detectDiskWrites()开启;
网络操作,使用detectNetwork()开启。

VmPolicy虚拟机策略:
Activity泄漏,使用detectActivityLeaks()开启;
未关闭的Closable对象泄漏,使用detectLeakedClosableObjects()开启;
泄漏的Sqlite对象,使用detectLeakedSqlLiteObjects()开启;
检测实例数量,使用setClassInstanceLimit()开启。

注意:我们只需要在 app 的开发版本下使用 StrictMode,线上版本避免使

启动优化结果

经过以上优化结果 adb检测 从1074ms ,优化到773ms.Application的onCreate检测 从288ms 优化到220ms.

内存优化

表现形式:

  1. 内存抖动 : 锯齿状,GC导致卡顿
  2. 内存泄漏 : 可用内存减少,频繁GC
  3. 内存溢出 : OOM, 程序异常

检测工具:

memory profiler

android studio 自带的. 找不到profile 的话 顶部导航栏

  1. 点击下载样式的按钮可以找到当前的内存

这样可以看到当前内存是哪里消耗的最多,还有一些其他的内存信息, 还可以看一个bitmap的预览图

使用ActivityManager

ActivityManager有一个MemoryInfo对象,可以进行进程的内存获取;同时我们需要RunningAppProcessInfo获取到当前系统正在运行的进程的集合,然后去遍历这个集合,根据每个元素的processName(包名)字段去筛选我们需要获取内存信息的进程,然后通过ActivityManager的getProcessMemoryInfo方法去获取到这个进程的内存值,通过换算得到进程的内存占用信息;然后将这个进程的各项信息,比如包名,PID,内存占用等信息

LeakCanary

https://square.github.io/leakcanary/getting_started/

大致原理

监听Activity生命周期->onDestroy以后延迟5秒判断Activity有没有被回收->如果没有回收,调用GC,再此判断是否回收,如果还没回收,则内存泄露了,反之,没有泄露。没有回收的话会dump 内存 然后去分析

引入:

dependencies 
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6'

arthook方式,epic 库

对bitmap检测不合理

支持ART上的Java方法HOOK. 使用 epic 库

hok 住 imageview 的setImageBitmap 即可

https://github.com/tiann/epic

  1. 引入
implementation 'me.weishu:epic:0.11.0'
  1. hook之后的操作 找到imageview判断和bitmap是否合理设置
public class ImageHook extends XC_MethodHook 

    /**
     * hook方法之后的一个回调
     *
     * @param param
     * @throws Throwable
     */
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable 
        super.afterHookedMethod(param);
        //实现逻辑
        checkBitmap(param);
        
    

    private void checkBitmap(MethodHookParam param) 
//        DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new XC_MethodHook() 
//            @Override
//            protected void afterHookedMethod(MethodHookParam param) throws Throwable 
//                super.afterHookedMethod(param);
        if (param.thisObject instanceof ImageView) 
            final ImageView imageView = (ImageView) param.thisObject;
            if (imageView.getDrawable() instanceof BitmapDrawable) 
                BitmapDrawable drawable = (BitmapDrawable) imageView.getDrawable();
                final Bitmap bitmap = drawable.getBitmap();
                if (bitmap != null) 
                    //bitmap 宽高
                    int bitmapWidth = bitmap.getWidth();
                    int bitmapHeight = bitmap.getHeight();
                    //视图宽高
                    int viewWidth = imageView.getWidth();
                    int viewHeight = imageView.getHeight();
                    if (viewHeight > 0 && viewWidth > 0) //view 有宽高
                        //当图片宽高都大于视图宽高的2倍时就报出警告
                        if (bitmapWidth >= viewWidth << 2 && bitmapHeight >= viewHeight << 2) 
                            warn(bitmapWidth, bitmapHeight, viewWidth, viewHeight);
                        
                     else 
                        imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() 
                            @Override
                            public boolean onPreDraw() 
                                int bitmapWidth = bitmap.getWidth();
                                int bitmapHeight = bitmap.getHeight();
                                int viewWidth = imageView.getWidth();
                                int viewHeight = imageView.getHeight();
                                if (bitmapWidth >= viewWidth << 2 && bitmapHeight >= viewHeight << 2) 
                                    warn(bitmapWidth, bitmapHeight, viewWidth, viewHeight);
                                
                                imageView.getViewTreeObserver().removeOnPreDrawListener(this);
                                return true;
                            
                        );
                    
                
            
        
//            
//        );

    

    private void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight) 
        StringBuffer msg = new StringBuffer();
        msg.append("图片大小不合理:")
                .append("bitmapWidth=").append(bitmapWidth)
                .append(",bitmapHeight=").append(bitmapHeight)
                .append(",viewWidth=").append(viewWidth)
                .append(",viewHeight=").append(viewHeight);
        //不合理
        LogUtil.e((new Throwable(msg.toString())));
    


  1. 开启hook 注入
 //方式1 hook方式 注册到setImageBitmap 里面去
            DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());

用的时候minSdkVersion 21

用的时候无法注入.

检测图片的不合理-通过遍历ViewTree的的办法 (我采用的这种方案)

简单原理:
实时检查View中的BitmapDrawable,判断bitmap大小和view图片的大小是否匹配

  1. 注册 Application 的 registerActivityLifecycleCallbacks 方法,传入自己的lifecycleCallbacks
  2. onActivityCreated 方法内 对 ViewTreeObserver 进行addOnGlobalLayoutListener 并实现OnGlobalLayoutListener 可获得宽度或者高度(OnGlobalLayoutListener当在一个视图树中全局布局发生改变或者视图树中的某个视图的可视状态发生改变时,所要调用的回调函数的接口类)
  3. 进行递归遍历直到遍历到ImageView,同时获取Background的bitmap.
  4. 对比bitmap 和ImageView 计算出比例
float scale = Math.max(bitmap.getHeight()*1.0f/view.getHeight(),bitmap.getWidth()*1.0f/view.getWidth());
  1. 如果比例大于1.5倍则给出警告.

优化方案

针对内存泄漏

  1. 集合类
  2. Static关键字修饰的成员变量
  3. 非静态内部类 / 匿名类
  4. 资源对象使用后未关闭

集合类

  • 内存泄露原因 集合类 添加元素后,仍引用着 集合元素对象,导致该集合元素对象不可被回收,从而 导致内存泄漏

解决办法: 由于1个集合中有许多元素,故最简单的方法 = 清空集合对象 & 设置为null

Static关键字修饰的成员变量

  • 储备知识
    被 Static 关键字修饰的成员变量的生命周期 = 应用程序的生命周期
  • 泄露原因
    若使被 Static 关键字修饰的成员变量 引用耗费资源过多的实例(如Context),则容易出现该成员变量的生命周期 > 引用实例生命周期的情况,当引用实例需结束生命周期销毁时,会因静态变量的持有而无法被回收,从而出现内存泄露

解决办法:

  1. 尽量避免 Static 成员变量引用资源耗费过多的实例(如 Context,若需引用 Context,则尽量使用Applicaiton的Context
  2. 使用 弱引用(WeakReference) 代替 强引用 持有实例)

非静态内部类 / 匿名类

  • 静态成员变量有个非常典型的例子 = 单例模式
  • 储备知识 单例模式 由于其静态特性,其生命周期的长度 = 应用程序的生命周期(泄露原因 若1个对象已不需再使用 而单例对象还持有该对象的引用,那么该对象将不能被正常回收 从而 导致内存泄漏)

解决方案 单例模式引用的对象的生命周期 = 应用的生命周期

  • 储备知识 非静态内部类 / 匿名类 默认持有 外部类的引用;而静态内部类则不会
    常见情况 3种,分别是:非静态内部类的实例 = 静态、多线程、消息传递机制(Handler)

解决方案:

将非静态内部类设置为:静态内部类(静态内部类默认不持有外部类的引用)

该内部类抽取出来封装成一个单例

尽量 避免 非静态内部类所创建的实例 = 静态

Handler 内存泄露

使用leakcannry检测到内存泄漏后分析.一般造成内存泄漏的原因有以下几种

储备知识

  • 主线程的Looper对象的生命周期 = 该应用程序的生命周期
  • 在Java中,非静态内部类 & 匿名内部类都默认持有 外部类的引用

举例:

 //1. 实例化自定义的Handler类对象-
                //注:此处并无指定Looper,故自动绑定当前线程(主线程)的Looper、MessageQueue
             private Handler handler; = new Handler();

在Handler消息队列 还有未处理的消息 / 正在处理消息时,消息队列中的Message持有Handler实例的引用


1、资源性对象未关闭
对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null。例如Bitmap
等资源未关闭会造成内存泄漏,此时我们应该在Activity销毁时及时关闭。

2、注册对象未注销
例如BraodcastReceiver、EventBus未注销造成的内存泄漏,我们应该在Activity销毁时及时注销。

3、类的静态变量持有大数据对象
尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储。

4、单例造成的内存泄漏
优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封
装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可。

5、非静态内部类的静态实例
该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源
不能正常回收。此时,我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如
果需要使用Context,尽量使用Application Context,如果需要使用Activity Context,就记得用完后置
空让GC可以回收,否则还是会内存泄漏。

6、Handler临时性内存泄漏
Message发出之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引
用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的,
则会导致Activity或者Service不会被回收。并且消息队列是在一个Looper线程中不断地轮询处理消息,
当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message
持有Handler实例的引用,Handler又持有Activity的引用,所以导致该Activity的内存资源无法及时回
收,引发内存泄漏。解决方案如下所示:

1、使用一个静态Handler内部类,然后对Handler持有的对象(一般是Activity)使用弱引用,这样在回收时,也可以回收Handler持有的对象。

2、在Activity的Destroy或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中有待处理的消息需要处理。需要注意的是,AsyncTask内部也是Handler机制,同样存在内存泄漏风险,但其一般是临时性的。对于类似AsyncTask或是线程造成的内存泄漏,我们也可以将AsyncTask和Runnable类独立出来或者使用静态内部类。

7、容器中的对象没清理造成的内存泄漏
在退出程序之前,将集合里的东西clear,然后置为null,再退出程序

8、WebView
WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。我们可以为
WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业
务的需要选择合适的时机进行销毁,达到正常释放内存的目的。

9、使用ListView时造成的内存泄漏
在构造Adapter时,使用缓存的convertView。

针对内存抖动

查看: 主要查看memoryProfiler里面的内存是否总是频繁回收

解决办法:
主要查看是否有循环创建清空对象的地方.

  • 尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
  • 注意自定义View的onDraw()方法会被频繁调用,所以在这里面不应该频繁的创建对象。
  • 当需要大量使用Bitmap的时候,试着把它们缓存在数组中实现复用。
  • 对于能够复用的对象,同理可以使用对象池将它们缓存起来。

针对bitmap过大或不合理

https://github.com/smallnew/BitmapCanary

使用bBitmapCanary检测后,对图片进行处理

  1. 使用低色彩的解析模式,如 RGB565,减少单个像素的字节大小
  2. 资源文件合理放置,高分辨率图片可以放到高分辨率目录下
  3. 图片缩小,减少尺寸

针对加载gif图

https://juejin.cn/post/6854573219425288199

OOM

onTrimMemory 与 onLowMemory Android 系统的每个进程都有一个最大内存限制,如果申请的内存资源超过这个 限制,系统就会抛出 OOM 错误。 onTrimMemory 所以在实际开发过程中我们要尽可能避免内存泄漏与内存抖动之外,还要格外注 意内存使用情况。根据《Manage Your App’s Memory》,
我们可以对内存的状 态进行监听,我们的 Application、Acivity、Service、ContentProvider 与 Fragment 都实现了 ComponentCallbacks2 接口。所以能够重写 onTrimMemory 与 onLowMemory 函数。

onLowMemory 这个函数看名字就是低内存。这个函数的回调意味着后台进程已经被干掉了。这 个回调可以作为 4.0 兼容 onTrimMemory 的 TRIM_MEMORY_COMPLETE 来使用 如果希望在其他组件中也能接收到这些回调可以使用上下文的 registerComponentCallbacks 注册接收, unRegisterComponentCallbacks 反注册

在Application的onTrimMemory(level)

内存紧张时停止一些非必要功能,如关闭推送进程,关闭后台service等


    @Override
    public void onTrimMemory(int level) 
        super.onTrimMemory(level);
        switch (level) 
            case TRIM_MEMORY_COMPLETE:
                //内存不足,并且该进程在后台进程列表最后一个,马上就要被清理
                LogUtil.d("内存不足,并且该进程在后台进程列表最后一个,马上就要被清理");
                break;
            case TRIM_MEMORY_MODERATE:
                //内存不足,并且该进程在后台进程列表的中部
                LogUtil.d("内存不足,并且该进程在后台进程列表的中部");
                break;
            case TRIM_MEMORY_BACKGROUND:
                //内存不足,并且该进程是后台进程
                LogUtil.d("内存不足,并且该进程是后台进程");
                break;
            case TRIM_MEMORY_UI_HIDDEN:
                //内存不足,并且该进程的UI已经不可见了。
                LogUtil.d("内存不足,并且该进程的UI已经不可见了");
                break;
            case TRIM_MEMORY_RUNNING_CRITICAL:
                //内存不足(后台进程不足3个),并且该进程优先级比较高,需要清理内存
                LogUtil.d("内存不足(后台进程不足3个),并且该进程优先级比较高,需要清理内存");
                break;
            case TRIM_MEMORY_RUNNING_LOW:
                //内存不足(后台进程不足5个),并且该进程优先级比较高,需要清理内存
                LogUtil.d("内存不足(后台进程不足5个),并且该进程优先级比较高,需要清理内存");
                break;
            case TRIM_MEMORY_RUNNING_MODERATE:
                //内存不足(后台进程超过5个),并且该进程优先级比较高,需要清理内存.
                LogUtil.d("内存不足,(后台进程超过5个),并且该进程优先级比较高,需要清理内存");
                break;
        
    

    @Override
    public void onLowMemory() 
        super.onLowMemory();
        //被回调时,已经没有后台进程
        //通过一键清理后,OnLowMemory不会被触发,而OnTrimMemory会被触发一次。
        LogUtils.d("内存不够了");
    

上述系统提供的API,还可以自己实现ComponentCallbacks,通过API注册,这样也能得到OnLowMemory回调。例如:

public static class MyCallback implements ComponentCallbacks  
        @Override
        public void onConfigurationChanged(Configuration arg)  
        

        @Override
        public void onLowMemory() 
            //do release operation
        
    

然后,通过

Context.registerComponentCallbacks()

在合适的时候注册回调就可以了。通过这种自定义的方法,可以在很多地方注册回调,而不需要局限于系统提供的组件。

  • 如果内存紧急了,直接清空 UIL/Fresco/glide 的内存缓存救急

线上监控

后台监控中采用garylog ,客户端就直接用post请求发送一些异常日志.

  1. 目前有做的是Crash后记录异常信息保存本地后上传服务器.(实现Thread.UncaughtExceptionHandler 接口)
  2. 一些自己做的 try catch 的异常信息
    • 未来要做的内存泄漏,内存消耗过大监控

这里择机发送日志需要注意几个方面:

  1. 需要有兜底的策略,不能一次性全部发过去,最多如果发送失败三次.则下次启动再发送 可以采用队列方式
  2. 不能影响正常业务.需要把异常上传信息独立出来

其他优化方案

  1. webview 依赖的activity销毁后需要调用mWebview.destroy();才能释放资源
  2. webview 启用独立进程 - (治标不治本,后续处理事情多,需在有时间时完成)
  • web进程 -> app主进程 通过aidl实现
  • app主进程 -> 回调到web进程 也是通过aidl接口实现
  • 广播本身跨进程的
  • 为了让组件中供web调用的 JsInterface 与业务分离,增加了一个H5Bridge.getInstance().register(JsInterfaceImpl.class)
  1. 关闭activiy后遍历view,清空所有ImageView中的图片的内存

内存优化结果

Chrash现象: 之前出现的兼容性问题主要出现在内存消耗峰值过高,以及存在内存泄漏情况.

检测方案: 采用技术工具检测到内存消耗过大的原因有几个方面.

  1. 大图片加载 和实际控件宽高不匹配
  2. gif图加载
  3. 3.代码存在内存泄漏的写法.

解决办法: 针对上面问题解决办法如下

  1. 采用加载图片的款号和实际ImageView控件的宽高做比对压缩,
  2. 采用底层C++库加载gif图片 .
  3. 修复代码存在内存泄漏的写法.

优化结果: 从1.1.0版本 标准兼容性测试开始通过率72%左右. 到现在通过率95%以上. 内存峰值消耗由原来300M+ 到现在稳定220M左右.

以上是关于android性能优化实践与总结(包含启动,内存优化)的主要内容,如果未能解决你的问题,请参考以下文章

抖音 Android 性能优化系列:Java 内存优化篇

JVM性能优化对象内存分配之虚拟机参数调优分析

爱奇艺技术分享:爱奇艺Android客户端启动速度优化实践总结

面试必备:深入 Java 应用性能调优实践

Android内存优化之OOM

腾讯优测优分享 | Android应用性能优化个人总结–图形优化