[译文] 如何正确在闪屏页加载耗时的库

在这篇文章中我想展示的是当开发人员有一个初始化很慢的库时可能不希望在主线程中来初始化这个库,因为它将阻塞UI线程使应用无响应。相反,开发人员希望在后台加载它,然后将结果通知到主线程中。

闪屏页

首先,如果你已经有一些初始化的东西在自己定义的application中,你可能需要一个适当的闪屏页。这意味着当点击应用程序图标的同时应该出现闪屏页。它可以很容易通过设置SplashActivity的主题背景实现。

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>

关于这个的实现就到这里。

顺便说一句,如果你使用作为你bitmap的资源,注意这个bug。不幸的是,没有相应的解决方案所以在API版本小于23的情况下闪屏页中的图片要用png格式。

初始化库

现在我们有了闪屏页,接下来干什么呢?现在我们应该考虑一下如何加载这些耗时的库了,Dagger 2和RxJava来救驾!

如果这个漫长的初始化库只是需要在闪屏页加载一些数据的话我们可以把它定义在SplashModule中,这样的话当我们不再使用的时候可以清除掉它的引用。

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

目前我们还不能在任何地方注入这个库,因为它会阻塞UI线程。我们将会创建一个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());
}
});
}
}

注入这个库

最后我们可以在SplashActivity中注入Observable

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();
}
});

处理当用户离开activity页面时引发的内存泄露问题

在这个例子中我们仅仅从Subscription取消订阅是不够的,因为当对象正在初始化的过程中Subscription不能释放资源从而是我们在内存中持有已经销毁的activity的资源导致内存泄露。如果在Application中开启了StrictMode.enableDefaults();我们很容易在LogCat中看到,当旋转activity页面日志如下:

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的静态类来实现,并且在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();
}

记住这些点,可以在闪屏页中很容易初始化一个库、发起一个网络请求或者做一些复杂的处理。

感谢阅读!源码在这里


FullStackEngineer的公众号,更多分享

Share Comments