Android 页面秒开优化总结

8,944 阅读6分钟

性能优化是一个长期的过程,并非一劳永逸,需要我们去抠细节,找到可以提升的地方。

针对Android平台自身特性的一些优化(例如xml布局优化、方法耗时之类)在这里就不展开了,主要还是从逻辑和业务出发~

数据加载优化

网络请求前置

也许是因为时序的问题,通常情况下 Activity 启动之后有三个步骤:

  1. 加载布局及初始化View
  2. 再进行网络请求等待
  3. 请求结果json解析
  4. 最后再渲染到界面上。

而实际上 步骤1、2、3 这三步是可以并行去做的,假设说 加载布局及初始化View 需要 150ms,整个网络请求耗时 200ms,那么并行之后理想情况就可以节省 150ms 的启动时间。

这时候可能就有疑问了,假设网络请求时间比View初始化来得快,网络请求结束后要去更新UI,就很有可能引起空指针问题。所以针对这种情况,我们需要做一个等待View初始化完的操作。

其实因为 Android 基于消息机制,并且通常情况下View的更新都在主线程,实际上网络请求结束后,post到主线程后更新UI,onCreate 已经执行完了,所以不需要等待也可以。但如果是在子线程去调用非更新View的方法,比如获取一些状态之类的,那就需要做等待操作。

Activity

public abstract class BaseActivity extends AppCompatActivity {
    
    private ReentrantLock mReentrantLock = new ReentrantLock();

    protected void onCreate(Bundle savedInstanceState) {
        try {
            mReentrantLock.lock();
            onInitData(savedInstanceState);
        } catch (Exception ignored) {
        } finally {
            super.onCreate(savedInstanceState);
            onCreateView(savedInstanceState);
            mReentrantLock.unlock();
        }
    }

    protected void waitViewInitialized() {
        try {
            mReentrantLock.lock();
        } catch (Exception ignored) {
        } finally {
            mReentrantLock.unlock();
        }
    }
    
    protected abstract void onInitData(Bundle savedInstanceState);
    protected abstract void onInitView(Bundle savedInstanceState);
}

Fragment

public abstract class BaseFragment extends Fragment {
    
    private ReentrantLock mReentrantLock = new ReentrantLock();

    protected View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView;
        try {
            mReentrantLock.lock();
            onInitData(savedInstanceState);
        } catch (Exception ignored) {
        } finally {
            rootView = onInitView(inflater, container, savedInstanceState);
            mReentrantLock.unlock();
        }
        return rootView;
    }

    protected void waitViewInitialized() {
        try {
            mReentrantLock.lock();
        } catch (Exception ignored) {
        } finally {
            mReentrantLock.unlock();
        }
    }
    
    protected abstract void onInitData(Bundle savedInstanceState);
    protected abstract View onInitView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState);
}

json异步解析

在上述步骤1和2并行的情况下,json在子线程解析效率理论上来讲要优于在主线程。View的初始化在主线程,假设网络请求比view初始化来得快,那么view初始化完成还需要等待json解析,那速度可能要更慢一些。我们统计了Android线上搜索结果的fastjson解析时间的平均数据,需要40ms左右。

json子线程解析在加载更多的场景下对滑动帧率也是有帮助的。

缓存&预加载

数据后带

针对一些特殊场景,例如从 搜索结果列表页 跳 商品详情页,可以把商品主图、标题等信息带过去,提前展示,提升白屏体验。

数据预加载

1、空间换时间方案

通过端智能及数据分析(可能需要算法的配合),对高频用户点击或展示的数据,可以在空闲线程做适当的预加载处理。

2、H5等资源内置、离线包或预加载

数据缓存

结合业务场景,针对一些非实时更新但是复用性较高的接口,可以做一层网络数据缓存。

通过 LRUCache 做缓存限制 缓存失效时间策略,降低数据出错的可能性

附:LRUCache 简单实现,可以做一些定制扩展

public class LRUCache<K, V> {

    public static class Entry<K, V> {
        public K key;
        public V value;

        public Entry<K, V> pre;
        public Entry<K, V> next;

        public Entry(K key, V value) {
            this.key = key;
            this.value = value;
        }
    }

    private static final int DEFAULT_SIZE = 2;

    private int size = DEFAULT_SIZE;

    private Map<K, Entry<K, V>> values;

    private Entry<K, V> first;
    private Entry<K, V> last;

    public LRUCache(int size) {
        if (size > 0) {
            this.size = size;
        }
        // 设定初始容量*扩容因子 避免扩容
        values = new HashMap<>((int)Math.ceil(size * 0.75f));
    }

    public final void put(@NotNull K key, V value) {
        Entry<K, V> entry = values.get(key);
        if (entry == null) {
            if (values.size() >= size) {
                removeLastEntry();
            }
            entry = new Entry<>(key, value);
        } else {
            entry.value = value;
        }
        moveEntryToFirst(entry);
    }

    public final V get(@NotNull K key) {
        Entry<K, V> entry = values.get(key);
        if (entry == null) {
            return null;
        }
        moveEntryToFirst(entry);
        return entry.value;
    }

    private void moveEntryToFirst(@NotNull Entry<K, V> entry) {
        values.put(entry.key, entry);
        if (first == null || last == null) {
            first = last = entry;
            return;
        }

        if (entry == first) {
            return;
        }

        if (entry.pre != null) {
            entry.pre.next = entry.next;
        }
        if (entry.next != null) {
            entry.next.pre = entry.pre;
        }

        if (entry == last) {
            last = last.pre;
        }

        entry.next = first;
        first.pre = entry;
        first = entry;
        first.pre = null;
    }

    private void removeLastEntry() {
        if (last != null) {
            values.remove(last.key);
            last = last.pre;

            if (last == null) {
                first = null;
            } else {
                last.next = null;
            }
        }
    }
}

数据&View懒加载

首屏不使用到的数据或者view,尽量采用懒加载的方式。

例如针对搜索结果页侧边栏筛选,可以在点击展开之后再添加筛选项。并且针对一些使用频率不高的功能,懒加载也能节约一定的运行内存空间。

布局加载优化

提前异步Inflate

布局 Inflate 过程慢主要有两个原因:

1、xml文件读取io过程 2、反射创建View

AsyncLayoutInflater:support v4包下面提供的类,用于在 work thread 加载布局,最后回调到主线程。

通常在网络请求的过程中,页面会处于一个空闲的状态,假设场景是搜索结果列表页,那么我们可以在数据请求前置的同时,去异步 inflate 一些 recyclerview 的 itemview,那么在渲染阶段就可以节约 recyclerview 的 createViewHolder 的时间。

并发优化

客户端通常情况下需要并发处理的场景比较少,这里举个特殊场景。

搜索结果页采用 Mist 做动态化方案。需要再 view 渲染之前,异步去 build 每个数据对应的节点信息(主要是measure和layout过程),通过测试比较,针对某一款机型,单线程去build 30个数据节点需要300ms以上,多线程并发只需要100ms左右,并发线程数为 CPU核心数-1。

多线程并发对资源有抢占,但整体效果还是可以的。并且要做好任务分配,让并发的几个线程处理的任务数差不多,减少最后的等待时间。

日志治理

大量的打印日志也会影响页面启动性能,需要相应治理。

交互优化 增强体感

骨架图

假设说网络请求的时间要比view初始化慢得多,可以通过骨架图的形式,提前创建好一些itemview,来增强一些用户体感,同事也达到提前创建 view 的效果。

RPC 优化

  • 推动服务端进行rt优化
  • 数据冗余压缩策略,例如接口数据携带大量埋点信息,可以考虑做精简

博客原文链接