好搭项目卡顿问题优化专题

公司做了一波推广,卡顿问题比较明显的显示了出来。华为荣耀系列的卡顿问题尤其明显,很多用户反映在看上新的时候卡顿比较严重,还有用户说在使用时间较长之后,就比较卡顿。

刚好这周把需求做完并且全部提测通过了,因此有三天的时间来做轻度优化。不过这个专题需要常驻,因为app需要优化的地方实在是太多了。

商品详情

商品详情页面是所有spu最终展示的地方,这个地方是所有用户必然经过的地方,商品详情页不是所有页面的终点,不是唯一的,所以商品详情页面有很多可以优化的地方。

无限跳转优化

商品详情页面作为展示spu的地方,其中却又展示了很多不同的spu,另外相同spu之间也是支持跳转的。

我对比了淘宝和微博,淘宝只支持保留三个spu,但是相同spu是可以跳转的,而微博支持无限跳转,我试了超过10个都可以返回,但是微博不支持在一个人主页里面仍然跳进这个人主页,也就是不支持相同spu的跳转。

和产品讨论了一下,这个地方相同spu仍然支持跳转,但是最多保留5个。

我设计这地方的方案是在application里面持有activity的引用,在activity启动的时候,进行判断,如果是详情页面,就放入数组里面,否则就不管。放进数组里面之后会判断是否数组长度是否超过4个,超过的话就会进行移除的操作,手动执行finish。

另外在destroy的时候,也这样判断,决定是否直接移除。

操作的入口比较好找,直接套用application的registerActivityLifecycleCallbacks即可。

在这个地方有一个问题,关于对activity的持有是否需要使用weakreference。

registerActivityLifecycleCallbacks的源码在application里面,主要的操作通过collectActivityLifecycleCallbacks来反馈

1
2
3
4
5
6
7
8
9
private Object[] collectActivityLifecycleCallbacks() {
Object[] callbacks = null;
synchronized (mActivityLifecycleCallbacks) {
if (mActivityLifecycleCallbacks.size() > 0) {
callbacks = mActivityLifecycleCallbacks.toArray();
}
}
return callbacks;
}

这个方法主要是在各activity生命周期开始的时候进行执行

启动的时候如下

1
2
3
4
5
6
7
8
9
/* package */ void dispatchActivityCreated(Activity activity, Bundle savedInstanceState) {
Object[] callbacks = collectActivityLifecycleCallbacks();
if (callbacks != null) {
for (int i=0; i<callbacks.length; i++) {
((ActivityLifecycleCallbacks)callbacks[i]).onActivityCreated(activity,
savedInstanceState);
}
}
}

关闭的时候如下

1
2
3
4
5
6
7
8
/* package */ void dispatchActivityDestroyed(Activity activity) {
Object[] callbacks = collectActivityLifecycleCallbacks();
if (callbacks != null) {
for (int i=0; i<callbacks.length; i++) {
((ActivityLifecycleCallbacks)callbacks[i]).onActivityDestroyed(activity);
}
}
}

问题就在actvity是在走destroy之前执行该方法,还是走完之后执行的。

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
28
29
30
31
32
33
34
35
36
37
38
39
protected void onDestroy() {
if (DEBUG_LIFECYCLE) Slog.v(TAG, "onDestroy " + this);
mCalled = true;

// dismiss any dialogs we are managing.
if (mManagedDialogs != null) {
final int numDialogs = mManagedDialogs.size();
for (int i = 0; i < numDialogs; i++) {
final ManagedDialog md = mManagedDialogs.valueAt(i);
if (md.mDialog.isShowing()) {
md.mDialog.dismiss();
}
}
mManagedDialogs = null;
}

// close any cursors we are managing.
synchronized (mManagedCursors) {
int numCursors = mManagedCursors.size();
for (int i = 0; i < numCursors; i++) {
ManagedCursor c = mManagedCursors.get(i);
if (c != null) {
c.mCursor.close();
}
}
mManagedCursors.clear();
}

// Close any open search dialog
if (mSearchManager != null) {
mSearchManager.stopSearch();
}

if (mActionBar != null) {
mActionBar.onDestroy();
}

getApplication().dispatchActivityDestroyed(this);
}

因此就证明了,destroy是在分发之前执行的,此时使用弱引用其实并不碍事,即使destroy的时候发生了gc,也没有太大的问题,只要之后的过程中将被回收的值对应的key清除即可。

weakreference在这边如果使用的话,需要考虑到contain的写法,需要判断的比较多,经过尝试,其实效率和直接强引用差不多。

详情页面泄露问题

详情页面启动速度慢的问题

详情页面启动速度慢的问题其实并不在详情页。经过排查,发现在有很多medel页面正在加载的地方,会出现启动速度过慢的问题。尤其是主页瀑布流尤为严重。通过一步一步的debug,发现当很多medel的bitmap在绘制时,cpu会爆满。
因为一直在分配线程,导致cpu调度过慢,甚至有时候会会在点击详情页的时候出现ANR问题。

排查出这个问题就好了,解决方法是将绘制由需要多少线程给多少线程改为固定数量线程池即可。这样cpu不至于说调度不来的问题。

这个问题给我一个想法,如果线程池结合observable,做出一个栈型线程池,但是在这个场景中其实不适合。

因此解决了启动速度慢的问题

订单发起页面

订单发起页面和普通页面不同,主要差别在于订单发起页面在很多地方都可以发起,但是其作用是唯一的,就是发起这个订单,并且继续后续的流程。但是由于在很多地方都可以发起订单,且订单页面又可以发起去很多地方。所以订单发起页面事实上也是无限的页面。

但是订单发起页面不需要保留用户的返回路径,仅仅只需要一个活着的页面反复调用即可。

因此我的做法是,在订单发起页面的start函数中,添加flag为FLAG_ACTIVITY_REORDER_TO_FRONT

因为试了一下发现FLAG_ACTIVITY_BROUGHT_TO_FRONT并无效,所以只可以尝试FLAG_ACTIVITY_REORDER_TO_FRONT

详细代码为

1
2
3
4
5
public static void start(Activity activity){
Intent intent = new Intent(activity, OrderConfirmActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
activity.startActivity(intent);
}

splash页优化

splash页面是app的进入时必走的页面,从leaks上面看,也有泄露的问题,检查发现泄露在lambda表达式中,而所有的lambda都是rxjava写的回调表达式,因此所做的是将这个页面所有的observable与该splashactivity的lifecycle绑定,另外eventbus也加了一些强判断(其实没啥必要),为了保险。

引导页面

2.8.0版本增加了引导页面,引导用户去设置medel,最近出现oom的现象,排查发现整个设置流程都没有进入destroy。因此在登录成功之后,启动了新的task来load,这样可以强制删除这些无用的流程。

vipZoon页面

vipzone是会员专区,这个页面是滑动式,每个请求发现都没有捆绑住lifecycle,进行了捆绑处理

列表页cpu占用率过高问题

凡是展示medel的列表页,以前都是通过简单粗暴的方式,直接使用rxjava开一个线程来处理,这个在往期并没有发现什么问题,事实上在业务不复杂的时候,这样做是首选。

后来问题来了,在快速滑动的时候,cpu负荷占满100%,此时点击进入详情页面,会等待超过5S然后产生ANR,说实话这是我第一次接受由于CPU负荷太高而导致的ANR。

针对此问题的解决方法为,限制线程数量,当快速滑动的线程数量被限制,这样无论如何都不会使得CPU负荷占满,这样也就可以完全满足这个问题了。

但是固定线程池带来了一个问题,就是假如任务队列中积压了上百个任务,此时我限制的最大为4个的线程池,基本上会使得最后一个的加载时间指数倍上升。

而最后一个却是目前用户视线的焦点,这就很矛盾了,为了降低CPU的负载,而使得用户看到想看的过于慢了,这也绝对不是开发者想要看到的。

因此此处需要兼得,开始想的方式是,通过仿照NIO的样例,一个线程用于接收请求,四个线程处理请求,但是仔细思考之后发现这样也不是解决办法,而是之前方法的一个变体。

之后决定使用优先队列,这下子思路就清晰了,后来的先处理。通过使用priorityBlockingQueue处理。这样在有限的资源下,仍然可以通过优先加载可见区域的内容,来增加加载的速度。

Ps:rxjava对优先队列的支持并不好,在rx1中jkwarton做过commit,但是并没有被merge,貌似还是对这个有些意见的,因此最好只是自己实现