Android性能优化[基于资料]

一个质量较高的性能分析专栏
我们先根据这个专栏出发,验证其各种方案的可行性。

性能优化大纲

大神博客

1.性能优化目的

image.png

性能优化的目的是:提高应用流畅性、稳定性、节省资源。
①流畅性:运行的更加流畅、不卡顿
②稳定性:稳定运行,使用过程中不出现引用崩溃和无响应(ANR)问题
③节省资源:节省耗费的资源,包括安装包大小、内存占用、耗电量、网络资源(手机流量)等

2.性能指标

流畅性、稳定性、资源节省性

具体操作

优化操作一:流畅性

主要针对三个方向优化:
启动速度、页面显示速度、响应速度
优化方案:
采用异步加载、分部加载、延期加载策略,减少主线程中的操作时间。

image.png

具体操作:

1.启动速度

1.减少onCreate的时间。
可以将onCreate的代码放到onResume中执行,不过为了防止重复初始化,需要加标志位。
2.优化布局文件
优化布局层次结构:使用include、merge、ViewStub、ConstraintLayout等。
merge使用主要是的目的是在优化布局,减少布局嵌套,一般与include结合使用
观察布局的工具:Hierarchy Viewer
优化布局的工具:Layoutopt

3.提高Adapter、ViewHolder的效率
4.减少主线程的耗时时间
将耗时过长的操作放入后台线程中执行,只在需要修改UI时通知主线程修改。

2.页面显示速度

优化方案:布局优化 、绘制优化
Android性能优化:手把手带你全面了解 绘制优化
Android性能优化:布局优化 详细解析
主要优化方向:
①降低View.onDraw的复杂度
②避免过度绘制(Overdraw)

具体操作就是:
第一点:不要在我们自定义的View中创建新的局部变量。
避免onDraw中执行大量或者耗时的操作。
第二点:移除默认的Window背景、移除控件中不必要的背景、减少布局文件的嵌套、自定义空间View优化(使用#clipRect、#quickReject)

可以在开发者选项中打开<调试GPU过度绘制>开关,比如蓝色是一层、绿色两层、红色四层等,尽量在两层以内,也就是绿色。(微信整个是红色的...)
第二点实际操作:
①移除默认的Window背景:
在应用程序默认继承的主题中,也就是清单文件中application项中的theme中:

image.png

添加一行:
<item name="android:windowBackground">@null</item>
也就是设置windowBackground为null。
②③可以使用AS自带的Layout Inspector来查看页面布局嵌套情况。(Hierarchy View已被废弃)
④自定义控件View优化:使用clipRect()/quickReject()
Canvas#clipRect:
给Canvas设置一个裁剪区域,只有该区域内才会被绘制,区域之外的都不绘制。
比如抽屉布局可以使用这个。
quickReject:
判断和某个矩形相交,若判断与矩形相交,则可跳过相交的区域,减少过渡绘制。

其他优化方案:
使用OpenGL绘图。(Android最高级的绘图机制,没用过..)
Profile GPU Rendering工具:开发者选项中可打开此开关,用于在屏幕上实时显示GPU渲染每一帧图像花费的时间。
参考资料

总结:


image.png
3.响应速度

优化原因
应用程序出现ANR情况,从而导致应用程序响应速度慢。

image.png

ANR原因
①应用在5s内未响应用户的输入事件
②广播接收器在10s内未完成相关的逻辑
③Service在20s内无法处理完成(不是15s哦)
【检测主线程中是否有耗时行为】
【开启StrictMode模式】
在Application的onCreate中开启:

  public void onCreate() {
        if (DEVELOPER_MODE) {
            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                    .detectDiskReads()
                    .detectDiskWrites()
                    .detectNetwork()   // or .detectAll() for all detectable problems
                    .penaltyLog()
                    .build());
            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                    .detectLeakedSqlLiteObjects()
                    .detectLeakedClosableObjects()
                    .penaltyLog()
                    .penaltyDeath()
                    .build());
        }
        super.onCreate();
  }
然后过滤信息:
`adb logcat | grep StrictMode`

StrictMode详解

博客详解
StrictMode可以用于捕获应用主线程中发生的磁盘I/O、网络访问违规等问题。(并不会检测到耗时任务,比如Thread.sleep)
主要检测两大问题:线程策略(ThreadPolicy)和VM策略(VmPolicy)。

ThreadPolicy线程策略:
1.自定义的耗时调用:使用detectCustomSlowCalls()开启(检测不到耗时操作,需要自己定义)
2.磁盘读取、写入(I/O操作):detectDiskReads()、detectDiskWrites()
3.网络操作:detectNetwork()

VmPolicy虚拟机策略:
1.Activity泄露,使用detectActivityLeaks()开启(都不用LeakCanary)
2.未关闭的Closable对象泄露,使用detectLeadekClosedObjects()开启;
3.泄露的Sqlite对象,对象detectLeakedSqliteObjects
4.网络操作,使用detectNetwork
过滤日志:StrictMode

优化方案
使用多线程,实际开发中,当一个进程发生了ANR后,系统会在/data/anr目录下创建一个traces.txt文件,通过该文件可定位出ANR的原因。
总结

image.png

4.稳定性

大神博客
主要是:应用崩溃(Crash)、应用无响应(ANR)
ANR上面已经说了,主要看应用崩溃。
应用崩溃Crash很多情况是因为内存溢出,即OOM,故需要避免OOM现象。

image.png

因为内存优化很重要,所以我们来

内存优化

1.Android内存管理机制

image.png

从上图可以看到,Android内存分配主要就两个工作:
内存分配+内存回收(释放)
内存管理的对象:①进程②对象③变量
Android系统分为3个层次:
Application Framework、ART虚拟机、Linux内核
其中,负责进程内存的角色:Linux内核 、Application Framework
负责对象、变量内存的角色:ART虚拟机

针对进程的内存策略

ActivityManagerService集中管理所有进程的内存分配。
内存回收策略,分两步:
1.由Application Framework决定回收的进程类型
Android中的进程是托管的;有进程空间紧张时,会按进程优先级从低到高的顺序自动回收进程。
Android进程分为5个优先级:
前台进程、可见进程、服务进程、后台进程、空进程
当系统需回收进程时,最先回收空进程、最后回收前台进程
2.Linux内核真正回收具体进程
1.ActivityManagerService对所有进程进行评分(评分存放在变量adj中)
2.更新评分到Linux内核
3.由Linux内核完成真正的内存回收

内存分配策略

image.png

JMM(Java内存模型)中,将内存区域分为五个部分(图片中只有三个,并不太规范):
程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区
①程序计数器:线程私有,记录线程上下文(多线程是通过轮流切换和分配处理器时间片实现的),唯一没有OutOfMomery
②Java虚拟机栈(Stack):线程私有,与线程生命周期相同,线程创建的时候创建。主要存储方法的内容:局部变量、参数、返回值以及运算的中间结果等。
③本地方法栈:存储Native方法内容
Java堆:线程共享,运行时内存区域,占用内存最大的地方,几乎所有的对象实例都存在这里。分为三个部分:年轻代(Eden、Survivor)、老年代
⑤方法区:线程共享,主要存储已经被java虚拟机加载的类的信息,包括运行时常量池、字段、方法信息、静态变量等。在编译时就已经分配好,存在与程序整个运行期间,不需要回收。
垃圾回收算法:
1.标记-清除算法 2.复制算法 3.标记-整理算法 4.分代收集算法

常见的内存问题

1.内存泄漏
2.内存抖动
3.图片Bitmap相关
4.代码质量&数量
5.日常不正确使用

1.内存泄漏

发生内存泄漏的本质原因:
本该回收的对象因为某些原因而不能被回收、从而继续停留在堆内存中
常见内存泄漏原因:
①集合类
②Static变量修饰的成员变量
③非静态内部类/匿名类
④资源对象使用后未关闭

具体了解

1.集合类导致内存泄漏:

集合类添加元素后,仍引用着集合元素对象,导致该集合中的元素对象无法被回收,从而导致内存泄漏。
Android内存泄漏之集合类

// 通过 循环申请Object 对象 & 将申请的对象逐个放入到集合List
List<Object> objectList = new ArrayList<>();        
       for (int i = 0; i < 10; i++) {
            Object o = new Object();
            objectList.add(o);
            o = null;
        }
// 虽释放了集合元素引用的本身:o=null)
// 但集合List 仍然引用该对象,故垃圾回收器GC 依然不可回收该对象

解决方案:集合类添加集合元素对象后,在使用后必须从集合中删除(Android Studio检测内存泄漏是检测不到的!!!!!,所以我们必须手动释放)
TODO 验证!!

2.static关键字修饰的成员变量

这我想无需多言,被static关键字修饰的成员变量的生命周期 = 应用生命周期
常见的就是单例、静态context

3.非静态内部类的实例为静态的情况下

如果非静态内部类创建了静态的实例(其生命周期 = 应用生命周期),会因费静态内部类持有外部类的引用而导致外部类无法释放,最终造成内存泄漏。也就是
外部类中持有非静态内部类的静态对象
demo:

// 背景:
   a. 在启动频繁的Activity中,为了避免重复创建相同的数据资源,会在Activity内部创建一个非静态内部类的单例
   b. 每次启动Activity时都会使用该单例的数据

public class TestActivity extends AppCompatActivity {  
    
    // 非静态内部类的实例的引用
    // 注:设置为静态  
    public static InnerClass innerClass = null; 
   
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {        
        super.onCreate(savedInstanceState);   

        // 保证非静态内部类的实例只有1个
        if (innerClass == null)
            innerClass = new InnerClass();
    }

    // 非静态内部类的定义    
    private class InnerClass {        
        //...
    }
}

// 造成内存泄露的原因:
    // a. 当TestActivity销毁时,因非静态内部类单例的引用(innerClass)的生命周期 = 应用App的生命周期、持有外部类TestActivity的引用
    // b. 故 TestActivity无法被GC回收,从而导致内存泄漏

解决方案:
将非静态内部类设置为:静态内部类(静态内部类默认不持有外部类的引用)
该内部类抽取出来封装成一个单例
尽量 避免 非静态内部类所创建的实例 = 静态

4.多线程导致的内存泄漏

多线程泄漏条件:当多线程使用为非静态内部类或者匿名类的时候可能导致内存泄漏。
这里的多线程包括:AsyncTask/实现Runnable接口/继承Thread类
泄漏原因:
当工作线程正在处理任务且外部类需销毁时,由于工作线程实例持有外部类引用,将使得外部类无法被垃圾回收器GC回收,从而造成内存泄漏。
demo:

   /** 
     * 方式1:新建Thread子类(内部类)
     */  
        public class MainActivity extends AppCompatActivity {

        public static final String TAG = "carson:";
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);

            // 通过创建的内部类 实现多线程
            new MyThread().start();

        }
        // 自定义的Thread子类
        private class MyThread extends Thread{
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    Log.d(TAG, "执行了多线程");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

   /** 
     * 方式2:匿名Thread内部类
     */ 
     public class MainActivity extends AppCompatActivity {

    public static final String TAG = "carson:";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 通过匿名内部类 实现多线程
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    Log.d(TAG, "执行了多线程");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }.start();
    }
}


/** 
  * 分析:内存泄露原因
  */ 
  // 工作线程Thread类属于非静态内部类 / 匿名内部类,运行时默认持有外部类的引用
  // 当工作线程运行时,若外部类MainActivity需销毁
  // 由于此时工作线程类实例持有外部类的引用,将使得外部类无法被垃圾回收器(GC)回收,从而造成 内存泄露

从上面可以看出,造成内存泄漏的原因有2个关键条件:
1.存在 工作线程持有外部类的引用关系
2.工作线程实例的生命周期>外部类的生命周期,即工作线程还在运行,而外部类已经销毁了。

我们只需要破坏其中一条条件即可解决内存泄漏。
解决方案也有两种:
1.将内部线程类声明为静态内部类。
2.在onDestroy里调用Thread.stop()方法。

// 共有2个解决方案:静态内部类 & 当外部类结束生命周期时,强制结束线程
// 具体描述如下

   /** 
     * 解决方式1:静态内部类
     * 原理:静态内部类 不默认持有外部类的引用,从而使得 “工作线程实例 持有 外部类引用” 的引用关系 不复存在
     * 具体实现:将Thread的子类设置成 静态内部类
     */  
        public class MainActivity extends AppCompatActivity {

        public static final String TAG = "carson:";
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);

            // 通过创建的内部类 实现多线程
            new MyThread().start();

        }
        // 分析1:自定义Thread子类
        // 设置为:静态内部类
        private static class MyThread extends Thread{
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    Log.d(TAG, "执行了多线程");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

   /** 
     * 解决方案2:当外部类结束生命周期时,强制结束线程
     * 原理:使得 工作线程实例的生命周期 与 外部类的生命周期 同步
     * 具体实现:当 外部类(此处以Activity为例) 结束生命周期时(此时系统会调用onDestroy()),强制结束线程(调用stop())
     */ 
     @Override
    protected void onDestroy() {
        super.onDestroy();
        Thread.stop();
        // 外部类Activity生命周期结束时,强制结束线程
    }
Handler内存泄漏原因

Handler内存泄漏
Handler造成内存泄漏有两个关键条件:
1.存在"未被处理/正在处理的消息,持有非静态内部类Handler的实例(Handler又会持有外部类)"
2.Handler的生命周期 > 外部类的生命周期

即Handler消息队列中还有未处理的消息/正在处理消息 而 外部类又需要销毁。
解决思路:
只需要上面任一条件不成立即可。
①Handler声明为静态内部类,使用WeakReference持有外部类
②当外部类结束生命周期时,清空Handler内消息队列:
在onDestroy里清除Handler消息队列里的所有消息(调用removeCallbacksAndMessages(null))

5.资源对象使用后未关闭

泄漏原因:
对于资源的使用,如:
①广播BroadcastReceiver
②文件流File
③数据库游标Cursor
④图片资源Bitmap等
解决方案:
在Activity销毁时及时关闭/注销资源
①对于 广播BraodcastReceiver:注销注册
unregisterReceiver()
② 对于 文件流File:关闭流
InputStream / OutputStream.close()
③ 对于数据库游标cursor:使用后关闭游标
cursor.close()
④对于 图片资源Bitmap:Android分配给图片的内存只有8M,若1个Bitmap对象占内存较多,当它不再被使用时,应调用recycle()回收此对象的像素所占用的内存;最后再赋为null
Bitmap.recycle();
Bitmap = null;
⑤对于动画(属性动画)
将动画设置成无限循环播放repeatCount = “infinite”后
在Activity退出时记得停止动画

6.其他泄漏原因

image.png

总结

image.png

辅助分析内存泄漏的工具

1.MAT(Memory Analysis Tools)
2.Heap Viewer
3.Allocation Tracker
4.Android Studio 的Profiler
5.LeakCanary
其实后两者是最常用的,Android Studio自带的Profiler一直在迭代更新,4.1之后还会显示内存泄露的点,当然可能有些不全,但相信不久的将来,在Google工程师的努力下,会适配绝大多数场景。
LeakCanary 会将所有的内存泄露以通知栏的方式告知,很方便。

MAT的话,之前用过,还需要手动dump下来hprof文件,转成MAT需要的格式,操作起来比较麻烦。
HeapViewer 没用过,但是看使用文档,其具有的功能Android Profiler都有
Heap Viewer使用

Allocation Tracker:Profiler里检测内存的就叫Allocation Tracker...

Bitmap优化

在点三中有做过总结,概不赘述。

【其他小技巧】

1.获取当前可使用的内存大小
调用ActivityManager.getMemoryClass()方法可获取当前应用可用的内存大小(单位=兆)
2.【获取当前的内存使用情况】
在应用生命周期的任何阶段,调用onTrimMemory()获取应用程序当前内存使用情况,可根据该方法返回的内存紧张级别参数来释放内存。

image.png

onTrimMemory
① OnTrimMemory的主要作用就是指导应用程序在不同的情况下进行自身的内存释放,以避免被系统直接杀掉,提高应用程序的用户体验.
②哪些组件可以实现onTrimMemory回调:
Application.onTrimMemory()
Activity.onTrimMemory()
Fragment.OnTrimMemory()
Service.onTrimMemory()
ContentProvider.OnTrimMemory()

③onTrimMemory和onLowMemory的关系
onLowMemory是api <14的机器使用的
④onTirmMemory会传递不同的等级,等级值有很多,最常用的是:
TRIM_MEMORY_UI_HIDDEN 表示应用程序的所有UI界面被隐藏了,即用户点击了Home键或者Back键导致应用的UI界面不可见.这时候应该释放一些资源
onTrimMemory的TRIM_MEMORY_UI_HIDDEN 等级是在onStop方法之前调用的
我们项目中是在Application的onTirmMemory中释放掉Fresco的图片缓存。同时调用了System.gc(),手动GC一下。

onTrimMemory典型的使用场景
1.退出的时候,动态生成的View、图片缓存、Fragment可以及时释放
2.后台Service,用户退出UI界面后,可以及时释放部分UI和Cache

包体积优化

安装包组成结构:
①assets文件夹。存放一些配置文件、资源文件,assets不会自动生成对应的 ID,而是通过 AssetManager 类的接口获取。
②res。res 是 resource 的缩写,这个目录存放资源文件,会自动生成对应的 ID 并映射到 .R 文件中,访问直接使用资源 ID。
③META-INF。保存应用的签名信息,签名信息可以验证 APK 文件的完整性。
AndroidManifest.xml。这个文件用来描述 Android 应用的配置信息,一些组件的注册信息、可使用权限等。
④classes.dex。Dalvik 字节码程序,让 Dalvik 虚拟机可执行,一般情况下,Android 应用在打包时通过 Android SDK 中的 dx 工具将 Java 字节码转换为 Dalvik 字节码。
⑤resources.arsc。记录着资源文件和资源 ID 之间的映射关系,用来根据资源 ID 寻找资源。

image.png

①使用Android Lint删除冗余资源
Android Analyze
具体使用就是运行 Analyze -> Run Inspection by name

image.png

直接输入 unused ...就可以过滤各种没有使用的资源,比如:
unused resources
image.png

②使用TinyPNG压缩图片,一般体积至少能压百分之五十
原理:把相似像素的24bit位用8bit位来表示,并且移除了不必要的元数据,是有损压缩,不过肉眼很难看出来。
③不要引入过大的库
④代码混淆工具,如proGuard,含:压缩、优化、混淆等

其他

LeakCanary原理

监测机制利用了Java的WeakReference和ReferenceQueue,通过将Activity包装到WeakReference中,被WeakReference包装过的Activity对象如果被回收,该WeakReference引用会被放到ReferenceQueue中,通过监测ReferenceQueue里面的内容就能检查到Activity是否能够被回收(在ReferenceQueue中说明可以被回收,不存在泄漏;否则,可能存在泄漏,LeakCanary是执行一遍GC,若还未在ReferenceQueue中,就会认定为泄漏)。

如果Activity被认定为泄露了,就抓取内存dump文件(Debug.dumpHprofData);之后通过HeapAnalyzerService.runAnalysis进行分析内存文件分析;接着通过HeapAnalyzer (checkForLeak—findLeakingReference---findLeakTrace)来进行内存泄漏分析。最后通过DisplayLeakService进行内存泄漏的展示。

冷启动优化

冷启优化

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容