1、优化前问题
我的测试手机是小米1,对于目前的App来说,运行时都会有卡顿。刚接手新的项目,在我的手机上跑跑,发现App启动后时候,在进入Splash页面前有3~4秒的白屏,这个不能接受,趁着空闲,着手优化一下。
2、优化方案
我记忆中的优化方案是给Application设置一个theme,给这个theme添加一个<item name="android:windowBackground">@drawable/ic_splash</item>,保证splash_img和Splash页面一样,这样就能正常加载显示了。Run一下,发现无效果,而给SplashActivity添加,冷启动却能正常显示图片的效果,但问题是图片有拉伸。这个肯定不行吧,找了一下网上的策略,发现都是这么直接给theme添加图片的,准备自己查找一下解决方案。
因为直接放图片,在不同手机上有严重的拉伸,如果创建多张不同尺寸的splash图片放在不同的drawable/mipmap中,那又会增大包大小。(没有去验证这种方法是否可行)那就只能是用xml文件来布局显示了。
我尝试将@layout/activity_splash直接作为windowBackground的值,发现App启动即异常退出,于是想到可以使用layer-list这种布局。
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:top="114dp">
<bitmap android:src="@drawable/ic_splash" android:width="21dp" android:height="21dp"/>
</item>
</layer-list>
运行后,图片还是有拉伸,google一下layer-list,发现文档中写的有防止图片缩放的方法。
默认情况下,所有可绘制项都会缩放以适应包含视图的大小。因此,将图像放在图层列表中的不同位置可能会增大视图的大小,并且有些图像会相应地缩放。为避免缩放列表中的项目,请在 <item>
元素内使用 <bitmap>
元素指定可绘制对象,并且对某些不缩放的项目(例如 "center"
)定义重力。例如,以下 <item>
定义缩放以适应其容器视图的项目:
<item android:drawable="@drawable/image" />
为避免缩放,以下示例使用重力居中的 <bitmap>
元素:
<item>
<bitmap android:src="@drawable/image"
android:gravity="center" />
</item>
好了,再次修改修改布局代码:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white"/>
<item android:top="114dp">
<bitmap android:src="@drawable/ic_splash" android:width="21dp" android:height="21dp"
android:gravity="center|top"/>
</item>
</layer-list>
运行后,非常完美。
3、再次优化
虽然在保证windowBackground的背景和SplashActivity的contentView一致情况下,效果已经完全可以了,但是了解activity的窗口的都清楚,其实windowBackground和contentView都是在同一个窗口中的,所以有了前者就不需要再设置后者了。于是,删除SplashActivity中的setContentView(R.layout.activity_splash);后,程序依旧完美运行。
4、原理剖析
其实这就是activity的窗口的机制了。Activity的attach方法。
Activity.java
final void attach(){
。。。
mWindow = new PhoneWindow(this, window, activityConfigCallback);//创建PhoneWindow
。。。
}
PhoneWindow.java
PhoneWindow(){
mUseDecorContext = true; //后面用到
。。。
mDecor = (DecorView) preservedWindow.getDecorView(); //获取DecorView,基本就是创建了,然后设置theme。
。。。
}
public final void setBackgroundDrawable(Drawable drawable) {
if (mDecor != null) {
mDecor.setWindowBackground(drawable); //设置PhoneWindow的背景,其实就是设置DecorView背景。
}
}
那什么时候调用PhoneWindow的setBackgroundDrawable来设置背景呢?看Activity的基类AppCompatActivity
AppCompatActivity.java
protected void onCreate(@Nullable Bundle savedInstanceState) {
AppCompatDelegate delegate = this.getDelegate();
delegate.installViewFactory();
delegate.onCreate(savedInstanceState);
。。。
}
public AppCompatDelegate getDelegate() {
if (this.mDelegate == null) {
this.mDelegate = AppCompatDelegate.create(this, this);
}
return this.mDelegate;
}
AppCompatDelegate.java
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
}
AppCompatDelegateImpl.java
AppCompatDelegateImpl(Context context, Window window, AppCompatCallback callback) {
//sWindowBackgroundStyleable就是private static final int[] sWindowBackgroundStyleable = {android.R.attr.windowBackground};
TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, (AttributeSet)null, sWindowBackgroundStyleable);
Drawable winBg = a.getDrawableIfKnown(0);
if (winBg != null) {
this.mWindow.setBackgroundDrawable(winBg); //设置window背景
}
}
好了,基本上厘清了,Activity在attach时候new PhoneWindow-》new Decorview,在onCreate时候,设置PhoneWindow背景,及Decorview背景,所以窗口能正常显示我们设置的布局内容,因为系统已经替我们设置了默认的布局视图。
而setContentView的工作内容呢。
public void setContentView(int resId) {
this.ensureSubDecor();
ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290);
contentParent.removeAllViews();
LayoutInflater.from(this.mContext).inflate(resId, contentParent); //只是将自定义的布局添加到16908290容器内,就是FrameLayout内容。
this.mOriginalWindowCallback.onContentChanged();
}
所以,setContentView在这个时候是画蛇添足的作用。
5、重要点。
(1)为SplashActivity的theme添加windowBackground。
(2)编写layer-list.xml布局文件。
(3)注掉SplashActivity的setContentView。
编辑、加图不太方便,先这么着。