温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

如何优化内置图网络

发布时间:2021-10-15 09:46:59 来源:亿速云 阅读:146 作者:iii 栏目:编程语言

本篇内容主要讲解“如何优化内置图网络”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何优化内置图网络”吧!

App包主要优化手段

通过apk包结构可以发现,对于包大小优化的主要手段都是集中在资源优化方向 如何优化内置图网络

内置图网络化技术分析

经过调研和总结可以分为以下四点,本文也主要是针对这四点展开。

  1. 拦截图片加载时机

  2. 图片如何显示

  3. 图片下载和缓存

  4. 内置图片删除

如何优化内置图网络

如何拦截view设置图片的方法

图片加载两个拦截方法
  • getDrawable

  • loadDrawable

Android系统view显示图片最终都是通过Resources类获得图片的drawable对象显示。获得drawable对象有两个接口getDrawable、loadDrawable。getDrawable是一个公共接口,可以重载这个方法达到拦截,一般setBackground或者setImageDrawable会调用,loadDrawable方法系统View初始化获取Drawable调用。

//Resource#getDrawable,
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
        throws NotFoundException {
         if(图片是否需要网络化){
             return 网络加载
         } else {
             //返回正常流程
             return baseResources.getDrawable(id,  theme);
         }
}

getDrawable比较好处理,但是loadDrawable方法是一个受保护方法,无法拦截。查看源码loadDrawable之后流程也没有找到可以hook的机会。一度以为拦截drawable很容易就可以实现,最后没想到在这个问题上花费很多时间。查资料、看源码最终找到一种方法!

因为loadDrawable这个方法只有xml配置的系统基础view (如"ImageView、TextView、各种布局管理器等")的src和background属性,在初始化view过程获得drawable对象才会用到。所以影响的只是xml布局文件配置的view。那么通过实现LayoutInflater.Factory2,拦截xml View创建过程将xml 的view替换为我们自定义基础view。在自定义view内通过遍历当前Attr属性判断使用src或者background,然后调用相应的setImageDrawable或者setBackground达到触发Resournces#getDrawable接口完成hook。通过这种hook的方式可以达到我们对XML布局view设置drawable的拦截目的

class SkinTextView extend TextView {
public SkinTextView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        setSkin(this,attrs);
    }
private static final int[] ATTR_ARRAY = {
       // 这个属性是系统类View的属性,对于APP领域是不可见的。
       // 但是这个值是固定的,所以可以这样写,这里参考了RecycleView#NESTED_SCROLLING_ATTRS实现
        16842964/* android.R.attr.background */,
        android.R.attr.src
};

public static void setSkin(View view, AttributeSet attrs, DraweeHolderSupplier supplier){

    Context context = view.getContext();
    Resources resources = context.getResources();

    TypedArray ta = context.obtainStyledAttributes(attrs, ATTR_ARRAY);
    Drawable background ;

    int drawableId ;
    for (int i = 0; i < ATTR_ARRAY.length; i++) {
        int attr = ATTR_ARRAY[i];
        drawableId = ta.getResourceId(i,0);
        if (drawableId == 0){
            continue;
        }
        background = resources.getDrawable(drawableId,context.getTheme());
        switch (attr) {
            case 16842964:
                view.setBackground(background);
                break;
            case android.R.attr.src:
                if (view instanceof ImageView) {
                    ((ImageView)view).setImageDrawable(background);
                }
                break;
        }
    }
    ta.recycle();
}
}

但是以上方案只能解决XML中系统基础的View,如果XML中使用开发自定义View则不管用。为了解决自定义view的问题我想到了两种解决方案。

方案一

  • 通过字节码修改方式将所有自定义view继承的系统基础view改为继承我们自定义的基础view

我通过asm字节码修改将APP内所有自定义view继承的系统基础view改为自定义基础view,这个方案可行,但是缺点比较多需要全局修改所有库的字节码包括androidx库AppCompatView,修改范围太大,框架稳定性不太容易保证,由于自定义基础view有一些拦截代码所以对view初始化性能也有一定影响,且ASM代码编写出现bug不易排查。如果只修改我们业务线的字节码,可以正常运行。但修改第三方aar字节码后,遇到一个坑,应用一直ANR期间没找到具体原因。

方案二

  • hook LayoutInflater解析XML自定义view过程

// LayoutInflater#createViewFromTag
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
    try {
        View view;
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }

        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }
       // 以下onCreateView方法可以重载,拿到view对象强制触发getDrawable即可,中间需要一些过滤。讲一下大致思路,细节就不加了。
        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }

        return view;
    }
}

这个方案可以将自定义view拦截,缺点就是依赖android系统版本,如果android系统这块逻辑发生变化那么需要适配。不过对于后续需要使用Fresco框架加载图片以及内存管理,这个方案无法做到融合Fresco,所以该方案最终也没有利用起来。

最终决定放弃对自定义view这种情况处理。通过遍历xml 将自定义attr和自定义view过滤。 字节码修改和自定义属性、view过滤方案可以参考下图。 如何优化内置图网络

/**
 * 自定义属性、view过滤
 * hook aapt打包过程,得到所有模块res资源路径,遍历所有res/layout下的xml
 */
Pattern pattern = Pattern.compile("(?<=(android:(background|src)=\"@drawable/))([a-z_0-9]*)")
void eachLayoutXml(File[] resDirs){
    resDirs.each {
        if (it.isDirectory() && it.name == "res") {
            eachLayoutXml(it.listFiles())

        } else if (it.isDirectory() && it.name == "layout") {
            it.listFiles().each { xml ->
                // 获得xml内容,通过正则表达式匹配字符串
            }
        }
    }
}

下载图片的方案以及图片如何显示

下载图片方案

当时考虑过两种下载图片方案

  • 图片插件apk,将所有需要的图片打包到apk,然后只下载一次插件,无需考虑图片内存问题

  • 网络直接下载图片,通过Fresco管理内存问题 如何优化内置图网络

对比这两种方案我选择了实现比较容易的第二种。

图片显示

这个问题比较好解决,view、drawable之间是通过Drawable.Callback进行传递,所以下载图片得到drawable对象后通过drawable callback#invalidateDrawable即可。当然这里返回的drawable应该是一个LayerDrawable,因为Drawable.Callback执行更新的Drawable必须是同一个Drawable对象,同时方便同步状态下返回默认图,异步网络图返回后刷新。

需要注意一点,这里不能直接使用Fresco RootDrawable对象返回,因为Fresco不支持view wrap_content属性

图片下载策略和缓存

因为需要用到Fresco,简单介绍下。Fresco结构分层可以分为三层,分别是图层、控制器、图片获取,每一层结构、功能如图。

  • RootDrawable是最终返回的图片Drawable对象

  • DataSources 返回图片信息的订阅源

  • Controler 图片获取和图层显示中间桥梁

  • 第三层是图片三级缓存,获取图片可以从缓存和网络获取

如何优化内置图网络

大致了解Fresco,下面描述内置图网络化框架融合Fresco,使用Fresco进行图片下载和缓存。

这个实现可以类比Fresco的DraweeView的实现,利用view的attach、detach、visible几个生命周期函数通过DraweeHolder触发drawble的加载和销毁,做到对Fresco的图片缓存和内存释放。具体过程不介绍了感兴趣可以阅读Fresco源码。

实现流程如下图。

如何优化内置图网络

内置图删除

经过实际调研,不能直接删除内置图,否则在打包过程进行图片链接的时候会抛出找不到资源错误,所以主要思路通过1像素图片替换要删除的图片。

删除内置图有以下几种方案 如何优化内置图网络

我选择方案四,具体有以下优点

  • 方便根据图片大小选择批量删除

  • 可以直接计算得到优化的包大小

  • 可以直接融合到APP编译过程,编译一步到位

实现方案如下

  • hook aapt资源打包过程moregeResources结束的时候

  • 遍历所有生成的图片flat二进制文件,将flat文件里png、webp、jpg二进制数据替换为一像素的默认图

这个方案实现比较麻烦的是对flat文件二进制流的读取过程 如何优化内置图网络

flat文件容器格式传送门

到此,相信大家对“如何优化内置图网络”有了更深的了解,不妨来实际操作一番吧!这里是亿速云网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI