Android性能优化工具方法总结

性能优化的文章很多,这里单纯记录优化点和优化方法的总结。

UI优化

Android UI的基础理论知识可以看这里

UI性能分析

HierarchyViewer

什么是HierarchyViewer

能够可视化的直观获得UI布局设计结构和各种属性信息,帮助我们优化布局,是Android自带的工具。

如何开启支持使用HierarchyViewer

Android系统出于安全考虑,Hierarchy Viewer只能连接开发版手机或模拟器,我们普通的商业手机是无法连上的(老版本的Hierarchy Viewer可以)。HierarchyViewer原理分析开启HierarchyViewer使用方法:

  • 配置ANDROID_HVPPROTO环境变量

Mac

vim ~/.bash_profile
export ANDROID_HVPROTO=ddm

配置以后好像Google的手机才能生效,没有测试成功。

如何查看HierarchyViewer


这里写图片描述

点击对应的view,可以显示当前view详情信息。

View绘制分为measure layout 和draw三个过程 三个点分布对应以上三个过程 三个圆点分为绿 黄 红三个颜色 绿色代表该View在本view tree中速度是前50% 黄色表示后50% 而红色表示是过度渲染,花费时间最长。

Android开发者模式GPU过度绘制

打开手机的开发者模式的调试GPU过度绘制开关,我们可以看到手机界面会被几种颜色的遮盖物遮住。
- Q:什么时GPU过度绘制
- A:在屏幕的一个像素上绘制多次
遮罩颜色含义:


这里写图片描述

这个我感觉没有看到特多的作用,使用减少层次结构分析则可以相应的减少过度绘制的地方。

UI性能优化方法

UI代码优化

使用<merge/>标签

该标签在UI的结构优化中起着非常重要的作用,它可以删减多余的层级,优化UI。多用于替换FrameLayout或者当一个布局包含另一个时,消除视图层次结构中多余的视图组。例如你的主布局文件是垂直布局,引入了一个垂直布局的include,这时如果include布局使用的LinearLayout就没意义了,使用的话反而减慢你的UI表现。这时可以使用标签优化

使用<ViewStub>标签

最大的优点是当你需要时才会加载,使用他并不会影响UI初始化时的性能。各种不常用的布局想进度条、显示错误消息等可以使用该标签,以减少内存使用量,加快渲染速度。它是一个不可见的,大小为0的View。

使用Lint检测

点击AndroidStudio Analyze->Inspect Code执行完成以后会看到如下:


这里写图片描述

Performance选项里面有关于Layout性能优化的选项,当然这个工具还给我们提供了很多优化的选择项。

App卡顿优化

开发的应用卡我们很常见这个问题,有时候不知道如何条理的来进行分析。这里罗列出一些列方法,待以后可以更加有条理的来进行分析。应用卡有很多原因可能是我们上面分析了的UI卡,或者写的逻辑代码部分导致卡顿。所以我们首先要定位出卡顿的问题。

TraceView

简介

它是一个分析器,能够记录每个函数的执行时间。

如何使用TraceView

使用TraceView有两种方式

  • 打开Android Device Monitor


    这里写图片描述

    点击停止后生成如下图:

    这里写图片描述

其中下面的百分数的含义为:

属性含义
Name线程调用方法名
Incl CPU Time当前方法(包含内部调运的子方法)执行占用的CPU时间
Excl CPU Time当前方法(不包含内部调运的子方法)执行占用的CPU时间
Incl Real Time当前方法(包含内部调运的子方法)执行的真实时间,ms单位
Excl Real Time当前方法(不包含内部调运的子方法)执行的真实时间,ms单位
Call+Recur Calls/Total当前方法被调运的次数及递归调运占总调运次数百分比
CPU Time/Call当前方法调运CPU时间与调运次数比,即当前方法平均执行CPU耗时时间
Real Time/Call当前方法调运真实时间与调运次数比,即当前方法平均执行真实耗时时间(重点关注)

这种方法确立的范围就比较大了,但是我们可以有针对的对某个操作来进行分析。

  • 通过代码精确到某个固定的范围
    • 在要分析的代码部分添加android.os.Debug.startMethodTracing();和android.os.Debug.stopMethodTracing();其中第一个方法默认保存的文件名为dtrace.trace,也可以传入名称。执行完会在/sdcard/Android/data/packagename/files下面找到我们的这个文件。
    • 把手机的这个文件拷贝到电脑在AndroidStudio中通过DDMS打开即可

这两种方式第一种就比较简单,第二种就比较精确,从我们的分析场景来决定使用哪一种。

Allocation Tracking

通过上面的方法我们可以定位到哪个方法卡顿了,有时候在一个方法里面操作的东西很多,我们还要进一步确认到底是里面的哪个东西导致卡顿了。这时候Allocation Tracking就有用了

什么是Allocation Tracking

它可以分析内存占用情况,在Monitors的memory中可以看出各个成员在内存中所占的大小。追踪对象在内存中的创建过程。

如何使用Allocation Tracking


这里写图片描述

这个会有开始和结束两种状态完成以后我们会看到这个分析文件

这里写图片描述

它列出了每个线程的执行每个方法的执行,右边可以根据百分比来排序。可以追踪具体哪个地方耗时。

Systrace

这个工具可以分析出UI的每一帧的情况,我们然后跟踪发现问题,但是感觉理解还没有到位,分析不能精确到哪个地方有问题,但是好像Google力推使用这个工具,它必定有它的好处。它的分析文件以后有时间具体的弄明白。

如何使用Systrace

打开Android Device Monitor点击如下按钮


这里写图片描述

然后会出现时间和文件保存路径和检测内容的配置。生成完成以后使用Chrome浏览器打开即可。
推荐两篇分析生成的.trace文件的文章 点击查看 点击查看

ANR分析

ANR在我们的开发中应该很熟悉了(哈哈)有时候真的是无心就导致了这个问题。
我们常见的ANR大概有如下:

  • 5s内无法响应用户输入事件
  • BroadCast接收方法里面10s还么有结束
  • Service20s内无法结束执行一个操作
    查看ANR trace文件导出adb pull data/anr/trace.txt查看
    我让主线程睡眠10s钟
.....
"main" prio=5 tid=1 Sleeping
  | group="main" sCount=1 dsCount=0 obj=0x7539d000 self=0x7f9f696a00
  | sysTid=28990 nice=0 cgrp=default sched=0/0 handle=0x7fa34c1a98
  | state=S schedstat=( 303625712 88859114 473 ) utm=24 stm=6 core=2 HZ=100
  | stack=0x7fe2604000-0x7fe2606000 stackSize=8MB
  | held mutexes=
  at java.lang.Thread.sleep!(Native method)
  - sleeping on <0x04d8d182> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:371)
  - locked <0x04d8d182> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:313)
  at android.os.SystemClock.sleep(SystemClock.java:120)
  at com.lyman.opengldemo.MainActivity$1.onClick(MainActivity.java:34)
  at android.view.View.performClick(View.java:5611)
  at android.view.View$PerformClick.run(View.java:22276)
  at android.os.Handler.handleCallback(Handler.java:754)
  at android.os.Handler.dispatchMessage(Handler.java:95)
  at android.os.Looper.loop(Looper.java:160)
  at android.app.ActivityThread.main(ActivityThread.java:6200)
  at java.lang.reflect.Method.invoke!(Native method)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:874)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:764)
....

我们可以在main后面看到我们的ANR原因。

应用代码优化

有时候我们写的代码粗心没有注意到一些潜在的问题,好在Android里面有一些检测问题的工具我们可以使用

StrictMode

什么是StrictMode

StrictMode是Android2.3引入的一个工具类,它可以帮助开发者发现代码中的一些不规范问题。

如何使用StrictMode

在程序中代码启用

可以在Activity的onCreate方里面来写如下代码,但是最好是放在Application里面监测所有的。

if (BuildConfig.DEBUG) {
            //线程策略检查
            StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                    //.detectDiskReads()//磁盘读取
                    //.detectDiskWrites()//磁盘写入
                    //.detectNetwork()//网络
                    //.detectResourceMismatches()//资源不匹配
                    //.detectCustomSlowCalls()//检查执行缓慢代码
                    //.penaltyDeath()弹出违规提示框
                    .detectAll()
                    .penaltyLog()//在Logcat打印日志
                    .build());
            //虚拟机策略检查
            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                    //.detectLeakedSqlLiteObjects()//Sql泄漏
                    //.detectLeakedClosableObjects()//威关闭Closable对象泄漏
                    //.detectLeakedRegistrationObjects()//注册泄漏
                    //.detectActivityLeaks()//Activity泄漏
                    .detectAll()
                    .penaltyLog()
                    .build());
        }
在手机设置开发者选项中开启严格模式

通过实践感觉第一种方式确实可以检测出程序的代码的一些问题。做优化的时候加上这些测试代码。

Lint

上面已经使用了Lint来优化我们的UI布局,其实这个工具很强大,可以检测出我们的代码或者资源的一些问题。

代码编写

这个是我们的代码在编写的时候留意的一些东西了。这里不详细列举。有空看一些这样的文章
http://mobile.51cto.com/abased-410791.htm

http://hukai.me/android-training-course-in-chinese/performance/performance-tips.html

内存优化

Android系统中每个应用都运行独立的进程和独立的虚拟机中,其中内存的大小有限制,如果内存使用不合理一个是使应用显得很卡严重的话直接就让应用进程崩溃了。关于理论内存的理论知识参考
http://hukai.me/android-performance-patterns/
https://mp.weixin.qq.com/s/2MsEAR9pQfMr1Sfs7cPdWQ
同样我们还是主要以发现内存问题然后来解决的思路来进行优化。

内存泄漏

内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。(Wiki介绍

Heap Viewer

Heap Viewer可以实时的查看App分配的内存大小和空闲内存的大小发现内存泄漏。这个工具在实际开发中不常用,我们了解它的使用方法https://www.kancloud.cn/digest/itfootballprefermanc/100907

Leakcanary

在实际开发中这个是我们检测内存泄漏的神奇。
使用也很简单附上链接https://github.com/square/leakcanary

内存溢出

每一个进程的Dalvik Heap Size 大小在应用中可以需要的时候增加,但是超过了系统设置的上限的时候就发生了内存溢出OOM。

  • 获取整个设备的堆内存信息
private void getMemoryInfo() {
        ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
        ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
        activityManager.getMemoryInfo(memoryInfo);
        Log.e(TAG, "getMemoryInfo: "+"totalMem=" + memoryInfo.totalMem/1024/1024 + ",availMem=" + memoryInfo.availMem);
    }
  • 获取当前应用可使用的最大堆内存,这是系统默认给一个应用的最大内存
ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
activityManager.getMemoryClass() 
  • 扩大应用内存
    在AndroidManifest.xml的application节点设置属性android:largeHeap="true"这个使用应用可以有最大分配的内存了,检测一下:
private void getMemoryInfo() {
        ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
        Log.e(TAG, "getMemoryInfo: "+activityManager.getMemoryClass() );
        Log.e(TAG, "getMemoryInfo: "+activityManager.getLargeMemoryClass());
        Log.e(TAG, "getMemoryInfo: isLargeHeap"+isLargeHeap(this) );
    }

    private  boolean isLargeHeap(Context context) {
        return (context.getApplicationInfo().flags & ApplicationInfo.FLAG_LARGE_HEAP) != 0;
    }

Allocation Tracker

在上面的分析应用的卡顿的时候我们使用了这个工具,它可以追踪内存对象的实际的数值的大小,可以帮助我们查看哪个东西内存占用内存最大以及分析。

关于OOM的建议和更多理论知识参考http://hukai.me/android-performance-oom/

内存优化是我们编写应用的重点内容提供几篇不错的分析文章
http://androidperformance.com/2015/07/20/Android-Performance-Memory-Google.html

应用启动优化

查看启动时间

Displayed

从Android 4.4开始,logical包含了一个输出名字包含Displayed的这个打印的值包含了从Launcher启动程序->实例化对象->创建初始化首个Activity->实例化布局->第一次绘制画面的总时间。有两种方式可以查看这个时间。

从AndroidStudio 日志输出查看,注意不要开启过滤


这里写图片描述

通过adb命令查看adb shell am start -S -W com.example.app/.MainActivity

reportFullyDrawn

上面的那个时间显示是系统打印的,如果我们现在要测量从启动到Activity里面一个异步任务加载完成的时间,那么就可以在异步任务调用完成执行activity.reportFullyDrawn方法。查找包含Fully drawn的日志则可以看到时间。

通过上面介绍的TraceView可以具体到某个地方的时间

使用Method Tracing


这里写图片描述

这个工具方便我们定位出启动一个Activity整个系统调用到我们的代码的每个函数的耗时。

应用启动白黑屏优化

产出的原因

因为我们的Activity要到执行完onCreate和onResume方法了才用户看的到,那么这段时间显示的百黑屏是Activity默认的主题的的那个背景的颜色。

解决方案

  • 自定义主题的背景颜色
<style name="WelcomeStyle" parent="android:Theme">        
    <item name="android:windowBackground">@drawable/welcome_bg</item>   
    <item name="android:windowNoTitle">true</item>    
</style>

然后在Activity中应用这个主题。

  • 把Activity的主题设为透明
<style 
      name="Appwelcome" 
parent="android:Theme.Translucent.NoTitleBar.Fullscreen">
</style>

这种方式在用户点击到显示的这段时间不会有变化,但是这个在用户感官上面可能会觉得是桌面卡了一下。这两种的话我还是倾向于第一种。

耗电量优化

查看电量待优化地方

我们要有上面一样的可以量化的工具来展示哪里有可以优化的地方可以使用Google开源的Battery Historian来进行分析,其安装和使用分析方法可以参考这篇博客http://www.10tiao.com/html/169/201705/2650823025/1.html

从代码上面优化

对电量相关代码的监听

电池管理对象BatteryManager会通过一个Intent广播所以的和电池相关的详情。这个广播是一个sticky intent,所以我们不需要注册广播接收器。下面的代码都是通过一个null 的接收器来接受电池相关信息,我觉得只有一种情况,我们要监听电池的使用量变化的时候那就注册一个广播持续监听,但是这个操作的性能影响是有一些的。

  • 判断当前的充电状态信息
IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus = context.registerReceiver(null, ifilter);
//你可以读到充电状态,如果在充电,可以读到是usb还是交流电

// 是否在充电
int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
                     status == BatteryManager.BATTERY_STATUS_FULL;

// 怎么充
int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
boolean usbCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_USB;
boolean acCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_AC;
boolean wirelessCharge = false;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
        wirelessCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_WIRELESS);
    }
  • 监听是否在充电
IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent intent = context.registerReceiver(null, ifilter);
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
        boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
                            status == BatteryManager.BATTERY_STATUS_FULL;

        int chargePlug = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
        boolean usbCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_USB;
        boolean acCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_AC;
  • 判断当前电量
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent intent = context.registerReceiver(null, filter);

filter.addAction(Intent.ACTION_BATTERY_CHANGED);
//当前剩余电量
int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
//电量最大值
int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
//电量百分比
float batteryPct = level / (float)scale;
  • 监听电量较低和离开较低电量状态
    我们如果没有特殊的需求不用去自己绘制电池变化,就监听电量的较低状态来进行我们的操作即可。
<receiver android:name=".BatteryLevelReceiver">
<intent-filter>
  <action android:name="android.intent.action.ACTION_BATTERY_LOW"/>
  <action android:name="android.intent.action.ACTION_BATTERY_OKAY"/>
  </intent-filter>
</receiver>

WakeLock

如果我们有一个这样的需求,我们的一项下载服务要在屏幕关闭的时候还能保证正常下载。(屏幕关闭默认情况CPU要进入休眠状态,那么我们的任务或许不能正常的执行。)关闭屏幕的时候我们的代码获取正常的后台运行没有问题,或许别的app把cup已经唤醒的一个状态。这个东西不常用,它的请求和释放操作要成对出现,否则会导致出错或浪费资源。

使用WakeLock
//first request permission in manifest
<uses-permission android:name="android.permission.WAKE_LOCK" />
//request cpu lock code
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
                PowerManager.WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"MyWakeLock");
                wakeLock.acquire();
//release cpu lock code
wakeLock.release();

JobService

使用Job Scheduler,应用需要做的事情就是判断哪些任务是不紧急的,可以交给Job Scheduler来处理,Job Scheduler集中处理收到的任务,选择合适的时间,合适的网络,再一起进行执行。例如我们要抽取日志信息上传。现在的需求用这个东西不是很多,继承JobService处理代码和Service类似。参考官网使用:
https://developer.android.google.cn/reference/android/app/job/JobService.html

关于电量优化的更多可以参考这两篇文章
http://hukai.me/android-performance-battery/
http://www.10tiao.com/html/169/201705/2650823025/1.html

安装包大小优化

代码混淆

在app的build.gradle配置如下

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
}

常规的混淆配置文件

#############################################
#
# 对于一些基本指令的添加
#
#############################################
# 忽略警告
-ignorewarnings
# 代码混淆压缩比,在0~7之间,默认为5,一般不做修改
-optimizationpasses 5

# 混合时不使用大小写混合,混合后的类名为小写
-dontusemixedcaseclassnames

# 指定不去忽略非公共库的类
-dontskipnonpubliclibraryclasses

# 这句话能够使我们的项目混淆后产生映射文件
# 包含有类名->混淆后类名的映射关系
-verbose

# 指定不去忽略非公共库的类成员
-dontskipnonpubliclibraryclassmembers

# 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。
-dontpreverify

# 保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses

# 避免混淆泛型
-keepattributes Signature

# 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable

# 指定混淆是采用的算法,后面的参数是一个过滤器
# 这个过滤器是谷歌推荐的算法,一般不做更改
-optimizations !code/simplification/cast,!field/*,!class/merging/*


#############################################
#
# Android开发中一些需要保留的公共部分
#
#############################################

# 保留我们使用的四大组件,自定义的Application等等这些类不被混淆
# 因为这些子类都有可能被外部调用
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService


# 保留support下的所有类及其内部类
-keep class android.support.** {*;}

# 保留继承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**

# 保留R下面的资源
-keep class **.R$* {*;}

# 保留本地native方法不被混淆
-keepclasseswithmembernames class * {
    native <methods>;
}

# 保留在Activity中的方法参数是view的方法,
# 这样以来我们在layout中写的onClick就不会被影响
-keepclassmembers class * extends android.app.Activity{
    public void *(android.view.View);
}

-keepclassmembers class * extends android.support.v4.app.FragmentActivity{
    public void *(android.view.View);
}

# 保留枚举类不被混淆
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# 保留我们自定义控件(继承自View)不被混淆
-keep public class * extends android.view.View{
    *** get*();
    void set*(***);
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
}

# 保留Parcelable序列化类不被混淆
-keep class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator *;
}

# 保留Serializable序列化的类不被混淆
-keepnames class * implements java.io.Serializable
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    !static !transient <fields>;
    !private <fields>;
    !private <methods>;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

# 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆
-keepclassmembers class * {
    void *(**On*Event);
    void *(**On*Listener);
}

# webView处理,项目中没有使用到webView忽略即可
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
    public *;
}
-keepclassmembers class * extends android.webkit.webViewClient {
    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
    public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
    public void *(android.webkit.webView, jav.lang.String);
}

# 移除Log类打印各个等级日志的代码,打正式包的时候可以做为禁log使用,这里可以作为禁止log打印的功能使用
# 记得proguard-android.txt中一定不要加-dontoptimize才起作用
# 另外的一种实现方案是通过BuildConfig.DEBUG的变量来控制
#-assumenosideeffects class android.util.Log {
#    public static int v(...);
#    public static int i(...);
#    public static int w(...);
#    public static int d(...);
#    public static int e(...);
#}

#############################################
#
# 项目中特殊处理部分
#
#############################################

#-----------处理反射类---------------
#-----------处理js交互---------------
#-----------处理实体类---------------
# 在开发的时候我们可以将所有的实体类放在一个包内,这样我们写一次混淆就行了。
# 将下面替换成自己的实体类
#-----------处理第三方依赖库---------

开启资源压缩需要在开启混淆了添加shrinkResources true即可。

当然你也可以自定义要保留的资源文件的时候可以在res/raw/keep.xml中添加如下保存资源的代码,keep为要保留的,discard为舍弃的

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
    tools:discard="@layout/unused2" />

可读链接:
胡凯的优化系列
一篇汇总相关文章的集合

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值