Launcher内存优化总结
1. 使用Android Monitor观察内存占用情况
在优化内存占用之前,我们可以使用工具来分析内存占用情况,然后有针对性的进行优化,这里推荐Android Studio中的Android Monitor工具。要使用该工具来分析应用的内存,必须将应用编译成debugable版本或直接使用debug版本软件。然后打开Android Studio并连接手机。
点击Android Monitor控制台下面的Monitors标签,就可以看到以上界面,Android Monitor可以同时监控Memory、CPU、Network和GPU。
这里我们只需关注Memory即可,在Memory上面我们可以看到有5个按键,我们关注第2个到第4个按键,分别表示手动GC(Intiate GC),打印Java堆栈(Dump Java Heap)和内存申请跟踪(Allocation Tracking)。下面有一个坐标,表示内存占用和进程启动时间的关系。
A)分析内存泄漏
当我们需要分析应用是否存在内存泄漏的情况,我们可以通过观察应用某个操作后坐标显示的内存使用情况,如果重复该操作,内存一直在增加并且点击GC按键也不会释放,则可以确定有内存泄漏。此时可以通过跟踪内存申请,来跟踪这一段时间内存申请的情况,就可以找到是哪里申请了内存但没有释放。
上图是Launcher启动时内存申请的情况,其中Method表示调用的方法,Count表示表演次数,Size表示申请的内存大小,单位为字节。通过不断向下查找就可以找到内存泄漏的具体位置。
B)打印内存堆栈
如果不存在内存泄漏,但内存占用过大,需要优化内存占用。可以在内存占用稳定后,打印内存堆栈,找到内存中占用较大的对象,对其进行优化。
上图是打印内存堆栈生成的快照,左边显示的是一个类名列表,列出了所有实例的类名以及该类的实例的数量(Total Count),每个实例占用内存的大小(Sizeof),所有实例占用的内存空间(Shallow Size),所有实例和其引用所占用的内存空间(Retained Size);选中某个类,右边就会列出该类的所有实例,点开该实例,可以看到该实例的所有成员变量,以及成员变量所占内存空间。 选中该实例在下面就会列出该实例的所有引用。
2. 功能精简
Launcher在相应内存优化过程中对于某些功能进行了精简,使其在不影响用户使用的情况下,尽量减少内存占用。精简功能必须要根据上面打印内存堆栈的方法找到内存占用较大的功能,然后再想办法进行精简。如果没有专门适配低Ram版本,对功能的精简应使用宏控制,Android SDK提供了专门的方法以便于区分低Ram的设备,可以使用如下方法:
A)关闭模糊背景功能
文件夹背景和AllApp的背景采用的是模糊背景,为了性能需要,模糊背景将整张壁纸进行模糊后缓存,所以会占用较大内存空间。考虑到关闭模糊壁纸功能并不会影响用户使用,而且可以减少很大的内存占用,所以Launcher直接将模糊背景功能关闭。以下是打开和关闭模糊背景的实际效果:
实际内存占用量:
打开模糊背景的内存占用: |
关闭模糊背景的内存占用: |
关闭模糊背景功能后,实际内存占用减少了15M左右。
B)采用双层桌面模式
诚壹Launcher支持两种桌面模式,即单层桌面模式和双层桌面模式。在单层桌面模式,所有的应用图标都会一次加载到桌面上。而双层桌面模式,就是如原生桌面一样带有抽屉,桌面只加载布局文件配置的应用图标。采用双层桌面模式,桌面上加载的图标会比较少,从而达到减少内存占用的目的。
单层桌面模式和双层桌面模式的实际效果如下:
单层桌面模式: |
双层桌面模式: |
该项优化跟实际的应用的数量相关,由于后续删减了很多应用,所以实际减少的内存占用只有2.2M左右。
3. 使用ViewStub延迟加载视图
Launcher是一个比较特殊的应用,整个应用除了桌面设置页面之外,只有一个Activity,所有可见的视图都显示在这个Activity中,视图之间的切换都是采用动画的方式进行切换的,这样可以提高页面切换的流畅感。但是,这样的设计方式也会导致Launcher的视图布局非常复杂,视图所占用的内存也会非常大。
<?xmlversion="1.0"encoding="utf-8"?> <com.cydroid.launcher3.LauncherRootView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:launcher="http://schemas.android.com/apk/res-auto" android:id="@+id/launcher" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true"> <TextView android:id="@+id/overview_panel_title" android:layout_width="match_parent" android:layout_height="@dimen/overview_panel_title_height" android:gravity="center" android:singleLine="true" android:textColor="?android:attr/textColorPrimaryInverse" android:textSize="@dimen/overview_panel_title_text_size" android:text="@string/overview_panel_title_normal" android:visibility="gone"/> <com.cydroid.launcher3.dragndrop.DragLayer android:id="@+id/drag_layer" android:clipChildren="false" android:importantForAccessibility="no" android:clipToPadding="false" android:background="@drawable/workspace_bg" android:layout_width="match_parent" android:layout_height="match_parent"> <com.cydroid.launcher3.Workspace android:id="@+id/workspace" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" launcher:pageIndicator="@+id/page_indicator"> </com.cydroid.launcher3.Workspace> <includelayout="@layout/hotseat" android:id="@+id/hotseat" android:layout_width="match_parent" android:layout_height="match_parent" launcher:layout_ignoreInsets="true"/> <includelayout="@layout/page_indicator" android:id="@+id/caret_page_indicator" android:visibility="gone"/> <includelayout="@layout/dots_page_indicator" android:id="@+id/dots_page_indicator" android:visibility="gone"/> <includelayout="@layout/overview_panel" android:id="@+id/overview_panel" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="bottom" launcher:layout_ignoreInsets="true" android:visibility="gone"/> <include android:id="@+id/drop_target_bar" layout="@layout/drop_target_bar_horz"/> <include layout="@layout/qsb_container" android:id="@+id/qsb_container"/> <includelayout="@layout/widgets_view" android:id="@+id/widgets_view" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="invisible"/> <includelayout="@layout/all_apps" android:id="@+id/apps_view" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="invisible"/> </com.cydroid.launcher3.dragndrop.DragLayer> <FrameLayout android:id="@+id/cling" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/half_transparent" android:clipChildren="false" android:fitsSystemWindows="false" launcher:layout_ignoreInsets="true" android:visibility="gone"> <com.github.ybq.android.spinkit.SpinKitView xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/progress" style="@style/SpinKitView.Large.FoldingCube" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" app:SpinKit_Color="?android:attr/colorAccent"/> <includelayout="@layout/guide_pages" android:id="@+id/guide_pages" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone"/> </FrameLayout> </com.cydroid.launcher3.LauncherRootView> |
在Launcher的视图布局里面可以看到有很多include子布局,每一个include的子布局都是一个非常复杂的自定义视图布局,所以会导致整个布局也非常复杂。
设置android:visibility=”gone”是否有用?
我们在设置不显示的视图的时候通常都会将它的visibility属性设置为gone或invisible,但实际上这些不显示的视图依然会初始化并加载到内存中,占用内存空间。例如一个显示一张图片的ImageView,默认不会显示,只有在特定条件下才会显示,我们通常的做法是将它的visibility属性设置为gone或invisible,但实际上那张图片依然会被加载到内存中。
有没有什么办法让视图延迟加载,可以在代码中需要的时候new一个视图实例,然后通过addView将视图加到布局中。但Android提供了一种更好的方法,那就是使用ViewStub。
通过分析我们发现,桌面向导页面占用了相当大的内存空间,因为桌面向导页面加载了数张比较大的图片,但是实际上桌面向导页面只在Launcher第一次启动的时候才会显示,然而每次Launcher启动加载视图的时候桌面向导页面都会被加载到内存中,从而占用了大量的内存空间。
<includelayout="@layout/guide_pages" android:id="@+id/guide_pages" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone"/> |
我们使用ViewStub进行优化后,布局文件代码如下:
<ViewStub android:layout="@layout/guide_pages" android:id="@+id/guide_pages_stub" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="gone"/> |
实际的改动就是将include改为ViewStub,然后将layout改为android:layout。只修改布局文件肯定不行,要想实现延迟加载,还必须修改相应的代码逻辑,让布局在显示的时候再加载。
publicvoid showGuidePages(){ setupGuidePages(); mGuidePages.setVisibility(View.VISIBLE); mGuidePages.setOnPageExitListener(new GuidePagesView.OnPageExitListener(){ @Override publicvoid onExit(){ hideGuidePages(); } }); } publicvoid setupGuidePages(){ if(mGuidePages ==null){ ViewStub guidePagesStub = (ViewStub) findViewById(R.id.guide_pages_stub); guidePagesStub.setVisibility(View.VISIBLE); mGuidePages =(GuidePagesView) findViewById(R.id.guide_pages); } } |
我们在showGuidePages之前setupGuidePages,需要注意的在通过findViewById获取GuidePagesView对象之前,必须通过ViewStub对象调用setVisibility(View.VISIBLE),或者调用inflate()来创建GuidePagesView,否则通过findViewById获取的对象为空。另外,还必须注意ViewStub只能调用一次setVisibility(View.VISIBLE)或inflate(),多次调用会抛出异常。这样我们就实现了视图的延迟加载。
通过使用ViewStub对桌面向导页面和AllApps页面以及编辑模式页面进行优化后,Launcher初始化完成后视图数量的对比如下:
优化前: |
优化后: |
我们可以看到,Launcher初始化完成后,视图数量从166减少到了95个。
内存占用对比如下:
优化前: |
优化后: |
从内存占用对比可以看到,内存占用减少了15M左右。