十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
本篇内容主要讲解“如何优化内置图网络”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“如何优化内置图网络”吧!
公司主营业务:成都网站设计、成都做网站、移动网站开发等业务。帮助企业客户真正实现互联网宣传,提高企业的竞争能力。创新互联是一支青春激扬、勤奋敬业、活力青春激扬、勤奋敬业、活力澎湃、和谐高效的团队。公司秉承以“开放、自由、严谨、自律”为核心的企业文化,感谢他们对我们的高要求,感谢他们从不同领域给我们带来的挑战,让我们激情的团队有机会用头脑与智慧不断的给客户带来惊喜。创新互联推出嫩江免费做网站回馈大家。
通过apk包结构可以发现,对于包大小优化的主要手段都是集中在资源优化方向
经过调研和总结可以分为以下四点,本文也主要是针对这四点展开。
拦截图片加载时机
图片如何显示
图片下载和缓存
内置图片删除
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文件容器格式传送门
到此,相信大家对“如何优化内置图网络”有了更深的了解,不妨来实际操作一番吧!这里是创新互联网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!