极简抖音中的优化点|青训营笔记
这是我参与「第四届青训营 」笔记创作活动的的第15天
前言
本文接上文 编写极简抖音的难点|青训营笔记 - 掘金 (juejin.cn),继续阐述我们在编写极简抖音期间进行优化的点。
对一个项目进行优化的时候,首先我们需要去检测项目中有什么需要优化的点。这时我们便需要使用到性能分析工具来对APP的各项性能进行检测,在本文中使用到的性能分析工具是 Dokit 。
下面我将罗列出几项我在项目中发现的可优化点,以及其优化方案。
初次启动白屏
由于APP启动的时候需要先启动 Application 进行初始化,在这个期间,会带来一个短暂的白屏展示,而这个白屏会给用户带来一个卡顿的感觉。
我们需要把白屏变成图案,这样子用户启动的时候就不会是看到一片空白。我使用到的方案是新建一个 SplashActivity,让其来充当一个启动页。
我们要解决以下几点问题:
- 如何让未展示到
SplashActivity的界面的时候,其背景就已经是我们需要设置的图案了 SplashActivity如何有效降低内存占用
首先,第一个问题,我们需要设置对应的主题,因为 Activity 在未加载绑定的布局资源的时候,会先加载主题文件,所以我们修改主题文件就可以把对应的白屏变为我们想要的启动页了
下面展示我的相关代码
<!--res/drawable/splash_bg.xml-->
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/white" />
</shape>
</item>
<item
android:gravity="center"
android:drawable="@drawable/potato"
/>
</layer-list>
<!--AndroidManifest.xml-->
<activity
android:name=".module.mine.activity.SplashActivity"
android:exported="true"
android:theme="@style/SplashTheme" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
//SplashActivity.java
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
ActivityUtil.startActivity(HomeActivity.class, true);
}
}
像上方这样子设置后,我们在启动的时候,就可以直接看到我们的启动页了

ViewPage2预加载带来的多次请求
当我们使用到 ViewPage2 去做一些 fragment 的加载的时候,我们有需求让滑动到对应的位置的时候才进行页面的加载,并且滑动回去的时候不再进行加载。
但是 ViewPage 本身就预设定了会进行预加载,我们无法对其预加载做一个禁止。所以要完成我们的业务,我们可以对 Fragment 做一个懒加载,就是在加载的时候,预先加载布局,但是不加载数据。
// 关闭预加载
getBinding().viewPager2.setOffscreenPageLimit(ViewPager2.OFFSCREEN_PAGE_LIMIT_DEFAULT); // 可以不设置 因为默认是 -1 默认不进行预加载
// 这个必须设置 不然仍然会启用预加载
((RecyclerView)getBinding().viewPager2.getChildAt(0)).getLayoutManager().setItemPrefetchEnabled(false);
// 设置缓存数量,对应 RecyclerView 中的 mCachedViews,即屏幕外的视图数量,此处设置为2
((RecyclerView)getBinding().viewPager2.getChildAt(0)).setItemViewCacheSize(2);
使用上述的代码,就可以让 ViewPage 页面进行懒加载,并且会对之前加载的页面进行一定数量的缓存
布局层过多,导致页面绘制压力大

在此,我们容易看到,APP 的UI层级普遍过高,这是由于使用线性布局等,会带来层级过多的问题,这会导致App显示的时候需要渲染的次数也多,这也直接导致了 APP 的性能问题。譬如在启动一个 Activity ,或者页面有动画的时候,会看到帧率明显降低,并且会导致 APP 出现明显的卡顿。
我的解决方案是,选用约束布局,当我们的页面比较复杂的时候,选用约束布局会使得我们的布局层数可以明显的减低。关于约束布局的学习,我们可以查看这篇文章 最全面的ConstraintLayout教程 (qq.com)
WebView的启动耗时以及内存占用
当我们使用 WebView 的时候,我们一般会给其单开一个进程,这样子 WebView 也就不会影响到我们的主进程。由于 WebView 容易发生内存泄露,所以它会有相对大的崩溃可能性,所以我们为其单开一个进程,提高 App 可使用的内存并且不影响主进程的使用。
但是这也产生一个问题,那就是新开一个进程的时候,会去重新执行一次 Application ,会导致第一次开启新的进程时候会有较久的白屏
解决方案:
- 优化
Applicaiton中对各种类型资源的加载,对相关的进程只做必要额加载 - 多进程的开启不放到需要跳转时候才开启,对进程进行一个预加载
Application 优化
下面附上我的 Application 的代码
public class App extends Application {
private static Context context;
private static String sCurProcessName = null;
private String processName;
private String packageName;
/**
* Set the base context for this ContextWrapper. All calls will then be
* delegated to the base context. Throws
* IllegalStateException if a base context has already been set.
*
* @param base The new base context for this wrapper.
*/
@Override protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
processName = getCurProcessName(base);
packageName = getPackageName();
}
private boolean isMainProcess() {
return !TextUtils.isEmpty(packageName) && TextUtils.equals(packageName, processName);
}
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
if (isMainProcess()){
MMKV.initialize(this);
MMKV.mmkvWithID("MyID", MMKV.SINGLE_PROCESS_MODE, GlobalConstant.MMKV_KEY);
//载入Dokit监测
new DoKit.Builder(this)
.productId(context.getString(R.string.value_dokit_pid))
.build();
//使用订阅索引,加快编译速度
EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();
// 抖音授权
String clientkey = context.getString(R.string.value_client_key);
DouYinOpenApiFactory.init(new DouYinOpenConfig(clientkey));
//初始化
MyUtil.initialize(this);
//设置UI工具
RxTool.init(this);
//网络缓存
RetrofitCache.getInstance().init(this);
}else {
QbSdk.initX5Environment(getContext(), new QbSdk.PreInitCallback() {
@Override
public void onCoreInitFinished() {
// 内核初始化完成,可能为系统内核,也可能为系统内核
}
/**
* 预初始化结束
* 由于X5内核体积较大,需要依赖网络动态下发,所以当内核不存在的时候,默认会回调false,此时将会使用系统内核代替
* @param isX5 是否使用X5内核
*/
@Override
public void onViewInitFinished(boolean isX5) {
LogUtil.i("是否使用腾讯内核:" + isX5);
}
});
}
//设置打印开关
LogUtil.setIsLog(true);
//注册Activity生命周期
registerActivityLifecycleCallbacks(ActivityUtil.getActivityLifecycleCallbacks());
}
public static Context getContext() {
return context;
}
private static String getCurProcessName(Context context) {
if (!TextUtils.isEmpty(sCurProcessName)) {
return sCurProcessName;
}
sCurProcessName = getProcessName(android.os.Process.myPid());
if (!TextUtils.isEmpty(sCurProcessName)) {
return sCurProcessName;
}
try {
int pid = android.os.Process.myPid();
sCurProcessName = getProcessName(pid);
if (!TextUtils.isEmpty(sCurProcessName)) {
return sCurProcessName;
}
//获取系统的ActivityManager服务
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) {
return sCurProcessName;
}
for (ActivityManager.RunningAppProcessInfo appProcess : am.getRunningAppProcesses()) {
if (appProcess.pid == pid) {
sCurProcessName = appProcess.processName;
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return sCurProcessName;
}
private static String getProcessName(int pid) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("/proc/" + pid + "/cmdline"));
String processName = reader.readLine();
if (!TextUtils.isEmpty(processName)) {
processName = processName.trim();
}
return processName;
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
在 Application 中,我对当前进程进行判断,在对应的进程中才加载对应的资源,避免资源的过度加载
进程的预加载
对应多进程中,我们进行一个预加载,可以让APP需要启用到这个进程的时候,检测到有对应的进程已经创建了,就不会再次重新启用这个进程了,这样子就和打开一个普通的 Activity 的界面速度是一样的了。
进程预加载,我们需要使用到不可的服务或者广播,只需要将其绑定在同一个进程中,且对应服务为空即可。
<!--AndroidManifest.xml-->
<activity
android:name=".module.mine.activity.WebViewActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="false"
android:process=":h5"
android:screenOrientation="portrait"
android:theme="@style/BlackTheme" />
<service
android:name=".module.mine.service.PreLoadService"
android:process=":h5"
android:enabled="true"
android:exported="false" >
</service>
public class PreLoadService extends Service {
public PreLoadService() {
}
@Override public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}
}
//在对应的生命周期启用即可
private void startHideService(){
Intent intent = new Intent(this, PreLoadService.class);
this.startService(intent);
}
private void stopHideService(){
Intent intent = new Intent(this, PreLoadService.class);
this.stopService(intent);
}
WebView启动白屏
关于启动页白屏问题,和 初次启动白屏 的解决方案一致,我们设置这个Activity的背景为黑色,与抖音页面的背景一个颜色即可。
同时,我们需要注意的是,WebView 的背景也请记得设置为黑色
参考
(Android 多进程导致 Application 重复创建问题_wings专栏的博客-CSDN博客_android application多次创建