在这篇文章中,我将为你们展示开发者开启App时,因为加载一些库和数据遇到的初始化非常慢的这种情况。在这种情况下,开发者通常不会在主线程中初始化,因为这样做会使整个App卡住。相反,开发者希望通过后台初始化数据和库,然后在主线程处理初始化结果。

闪屏页 SplashActivity

首先,如果你已经有了一些需要在自定义Application类中初始化的东西,你可能想着要做一个恰当的闪屏页。这意味着你点击App图标的同时,闪屏页已经完整的显示出来了。通过设置闪屏页Theme的背景图,我们可以轻易实现这个需求。

res/values/styles.xml

1
2
3
< style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@drawable/background_splash</item>
</style>

AndroidManifest.xml

1
2
3
4
5
6
7
8
<activity
android:name=".splash.SplashActivity"
android:theme="@style/SplashTheme">

<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

通常情况下,闪屏页一般是放个logo,所以 @drawable/background_splash 可以写成一个 layer-list

例如:

1
2
3
4
5
6
7
8
9
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/holo_blue_dark"/>

<item>
<bitmap
android:gravity="center"
android:src="@drawable/ic_hockey_stick"/>

</item>
</layer-list>

顺便说一下,如果你用了矢量的 <vector> 作为 <src> 赋予 <bitmap> ,那么请你注意这个bug
坑爹的是,这个bug现在没办法解决,所以在API小于23的时候你只能用PNG来代替矢量图。

初始化数据和库

现在我们已经可以瞬间打开App了,那么接下来该怎么做?我们应该想办法如何初始化这种加载缓慢的库。Dagger2RxJava 或许对我们有帮助。

如果只是在闪屏页需要这种‘长初始化’库,来加载必要的资源,那么我们可以定义一个 SplashModule ,那么我们就能把所有库的引用都写到这里。可以,这很解耦。

1
2
3
4
5
6
7
@Module
public class SplashModule {
@Provides @NonNull @SplashScope
public SplashLibrary splashLibrary() {
return new SplashLibrary(); // Takes >5 seconds.
}
}

现在,我们还不能注入 @Inject ,因为这样做会阻塞我们的主线程。我们需要通过RxJava来创建一个观察者 Observable ,用来接收 SplashLibrary 实例,由于我们用了懒加载 Lazy<> ,我们的库仍未初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Module
public class SplashModule {
// ...

@Provides @NonNull @SplashScope
public Observable<SplashLibrary> observable(final Lazy<SplashLibrary> library) {
return Observable.defer(new Func0<Observable<SplashLibrary>>() {
@Override public Observable<SplashLibrary> call() {
return Observable.just(library.get());
}
});
}
}

注入这个库

最后,我们要把库 Observable<SplashLibrary> 注入到我们的闪屏页 SplashActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/** Observable which will emit an item when fully initialized. */
@Inject Observable<SplashLibrary> splashLibraryObservable;

/** Subscription to unsubscribe in onStop(). */
private Subscription subscription;

@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// ...

subscription = splashLibraryObservable
// Init library on another thread.
.subscribeOn(Schedulers.computation())
// Observe result on the main thread.
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<SplashLibrary>() {
@Override public void call(SplashLibrary splashLibrary) {

// Use the initialized library.

Intent intent = new Intent(activity, MainActivity.class);
startActivity(intent);
}
});
}
}

这儿还有一些个小问题等着你:

  1. 库加载的过程中,可能会抛出异常,我们需要在 onError 方法中去处理它们。
  2. 库加载过程中,用户可能会离开此页或旋转屏幕。由于我们在回调函数中引用了 Activity ,所以有可能导致内存泄露。

处理加载过程中的异常

为了处理这个问题,我们可以传一个 Observer 实例给 subscribe() 方法。

很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.subscribe(new Observer<SplashLibrary>() {
final String TAG = "Observer<SplashLibrary>";

@Override public void onCompleted() { }

@Override public void onError(Throwable e) {
Log.d(TAG, "Library init error!", e);
// Possible UI interaction.
// ...
finish();
}

@Override public void onNext(SplashLibrary splashLibrary) {
// ...
// Use the initialized library.

Intent intent = new Intent(activity, MainActivity.class);
startActivity(intent);
finish();
}
});

处理内存溢出问题

在这个例子中,我们不能从 Subscription 中取消订阅,因为对象一旦加载开始,Subscription 就不能释放资源了。这也就是为什么在内存中还存在着已经销毁的Activity对象,它导致了内存泄露。如果我们在Application中加入了严苛模式 StrictMode.enableDefaults(); ,我们可以很容易的在 LogCat 中看到看到Log。当我们旋转屏幕,严苛模式显示了Acitivty的实例信息。

1
2
3
E/StrictMode: class .SplashActivity; instances=2; limit=1
android.os.StrictMode$InstanceCountViolation: class .SplashActivity; instances=2; limit=1
at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)

这就是为什么我们需要在创建的 Observer 中释放Activity的引用了。我们可以创建一个静态类去实现 Observer<SplashActivity> ,在给他传入一个Activity的引用,然后在 onDestroy 中清除引用。这样,我们可以确保没有内存泄漏异常了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static final class OnInitObserver implements Observer<SplashLibrary> {
@Nullable private SplashActivity splashActivity;

OnInitObserver(@NonNull SplashActivity splashActivity) {
this.splashActivity = splashActivity;
}

@Override public void onCompleted() { /* ... */ }
@Override public void onError(Throwable e) { /* ... */ }
@Override public void onNext(SplashLibrary splashLibrary) { /* ... */ }

public void releaseListener() {
splashActivity = null;
}
}
1
2
3
4
5
@Override protected void onDestroy() {
super.onDestroy();

onInitObserver.releaseListener();
}

记住这几点,我们就能轻松地在闪屏页中加载库、数据,执行网络请求或者做一些其他的繁重的任务。

感谢阅读,获取源码请点这里


原文地址:How to Load Heavy Libraries on Splash Screen [the proper way]

作者:SAVELII ZAGURSKII