iOS 图片使用探究(1)-- 图片基础知识+图片格式

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 图片使用探究(1)-- 图片基础知识+图片格式相关的知识,希望对你有一定的参考价值。

参考技术A

图像 是人对视觉感知的物质再现。图像可以由光学设备获取,如 照相机 、 镜子 、 望远镜 及 显微镜 等;也可以人为创作,如手工绘画。图像可以记录、保存在纸质介质、胶片等等对光信号敏感的介质上。随着数字采集技术和信号处理理论的发展,越来越多的图像以数字形式存储。因而,有些情况下“图像”一词实际上是指 数字图像 。

与图像相关的话题包括图像采集、图像制作、 图像分析 和 图像处理 等。

图像分为静态影像,如 图片 、 照片 等,和动态影像,如 影片 等两种。

图像是一种视觉符号。透过专业设计的图像,可以发展成人与人沟通的视觉语言,也可以是了解族群文化与历史源流的史料。世界美术史中大量的平面绘画、立体雕塑与建筑,也可视为人类由古自今文明发展的图像文化资产。

计算机研究中一般是指 数字图像

数字图像 ,是二维 图像 用有限 数字 数值 像素 的表示。

通常,像素在计算机中保存为二维整数数组的 光栅图像 ,这些值经常用 压缩 格式进行传输和储存。

数字图像种类:二值图像、灰度图像、彩色图像、假彩色图像、多光谱图像、thematic、立体图像、三维图像

数字图像显示:光栅图像格式:BMP、GIF、JPEG、PNG等。矢量图像格式:WMF、SVG等

DPI(dots per inch)分辨率:每英寸点数

像素 = 尺寸 * 分辨率

颜色空间:对相同颜色数值的解释方式。比如说一个像素的数据时(FF0000FF),在RGBA 颜色空间中,会被解释为红色,而在BGRA 颜色空间中,则会被解释为蓝色。解码之前一般要提取出颜色空间参数,保证解码前后图片颜色空间保持一致。

CoreGraphic支持的颜色空间类型:HSB、RGB、CMYK、BGR

图片的位深度: 用多少位二进制来 记录图片中像素的色值(位深度决定了像素点的 颜色种类)

常见的位深度有:

在24位图片中, 红、绿、蓝 ( RGB ) 三基色各以2的8次幂,也就是256种颜色而存在的,这也是为什么 图片中三基色的色值都在0~255之间 的原因了。另外,有一些图片格式的位深度是固定的,比如GIF只有256种颜色。

图片格式选择

Apple 官方文档

sRGB 目前比较通用的全色彩图像色域,每个像素占4个字节。所以在iOS的实际使用中 图片实际占用的内存是 像素 * 4。一张分辨率很高的图片,展示时所消耗的内存会比图片实际的文件大小要大很多。( WWDC2018 图像最佳实践 )

Objccn 图片格式

还有占内存更小的格式:

选择正确的格式可以减少了内存的使用。简单总结一下:

那下一个话题来了,如何选择正确的格式呢?

简单的回答是:不需要你来选择格式,而是应该让格式选择你。是不是觉得一下子松了一口气?哈哈😆

使用 UIGraphicsBeginImageContextWithOptions 生成的图片,每个像素需要 4 个字节表示。建议使用 UIGraphicsImageRenderer ,这个方法是从 iOS 10 引入,在 iOS 12 上会自动选择最佳的图像格式,可以减少很多内存。

另外,如果想修改颜色,可以直接修改 tintColor,不会有额外的内存开销。(图片测试下 通过tintColor 和 iconfont占用的内存比较)

当你缩小一幅图像的时候,会按照取平均值的办法把多个像素点变成一个像素点,这个过程称为 Downsampling 。

UIImage 在设置和调整大小的时候,需要将原始图像加压到内存中,然后对内部坐标空间做一系列转换,整个过程会消耗很多资源。我们可以使用 ImageIO,它可以直接读取图像大小和元数据信息,不会带来额外的内存开销。

页面上实际展示的ImageView 所占用的内存大小,图片本身的尺寸决定的,所以通过Downsampling 生成缩略图 来降低内存 。

通过 Downsampling ,我们成功地减低了内存的使用,但是解码同样会耗费大量的 CPU 资源。如果用户快速滑动界面,很有可能因为解码而造成卡顿。

解决办法: Prefetching + Background decoding

Prefetch 是 iOS10 之后加入到 TableView 和 CollectionView 的新技术。我们可以通过 tableView(_:prefetchRowsAt:) 这样的接口提前准备好数据。有兴趣的小伙伴可以搜一下相关知识。

至于 Background decoding 其实就是在子线程处理好解码的操作。(最好在单个同步队列中处理,否则线程切换的性能也比较差)

WWDC 图像最佳实践

iOS 深入分析大图显示问题

由View的onAttachedToWindow引发的图片轮播问题探究

由View的onAttachedToWindow引发的图片轮播问题探究

文章目录

前言

本篇文章是在View的postDelayed方法深度思考这篇文章的所有的基础理论上进行研究的,可以说是对于View的postDelayed方法深度思考这篇文章知识点的实践。

某天同事某进在做一个列表页添加轮播Banner的需求的时候,发下偶尔会出现轮播间隔时间错乱的问题。

我看了他的轮播的实现方案:利用Handle.postDelayed间隔轮播时长每次执行完轮播之后再次循环发送;

代码貌似没有太大问题,但通过现象看来应该是removeCallbacks失效了~!

Handle#removeCallbacks

stackoverflow上找了相关资料Why to use removeCallbacks() with postDelayed()?,之后尝试将postDelayed不靠谱那么改为post,发现貌似轮播间隔时间错乱的问题解决了~!

虽然不清楚什么原因导致问题不再出现,但后续因为其他工作打断未能继续排查下去。

若干天之后,再次发现轮播间隔时间错乱的问题有一次出现了。

这次我们使用自定Handler进行removeCallBackspostDelayed,完美的解决了问题。

下面记录一下整问题解决过程中的思考~!

待解决问题

  1. View.removeCallbacks 是否真的可靠;
  2. View.postView.postDelayed相比为什么bug复现频率更低;

View#dispatchAttachedToWindow

HandleremoveCallBacks移除方法是不可靠的么?如果当前的任务不是在执行中,那么该任务一定会被移除。
换句话说,Handle#removeCallBacks移除的就是在队列中等待被执行的Message

那么问题到底出在哪里,而且为什么postDelayed替换为post问题的复现概率降低了?

这次有些时间,跟了一下源码发现使用View#postDelayed发送的消息不一定会立即被放在消息队列。

回顾之前View的postDelayed方法深度思考这篇文章中关于View.postDelayed小结中的描述:

postDelayed方法调用的时候,如果当前的View没有依附在Window上的时候,先将Runnable缓存在RunQueue队列中。等到View.dispatchAttachedToWindow调用之后,再被ViewRootHandler进行一次postDelayed。这个过程中相同的Runnable只会被postDelay一次。

我们打印stopTimerstartTimer方法执行的时ViewPager#getHandlerHandler实例,发现在列表快速滑动时大部分为null

好吧,之前忽略了这个Banner在滑动过程中的被View#dispatchDetachedFromWindow。这个方法的调用会导致View内部的Handlenull

如果ViewHandlenull,那么Message的执行可能会收到影响。

View的postDelayed方法深度思考这篇文章中关于mAttachInfo对于View.postDelayed的影响,也都进行了分析。这里我们捡主要的源码阅读一下。

//View.java
void dispatchAttachedToWindow(AttachInfo info, int visibility) 
    mAttachInfo = info;
    /****部分代码省略*****/
    // Transfer all pending runnables.
    if (mRunQueue != null) 
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    
    performCollectViewAttributes(mAttachInfo, visibility);
    onAttachedToWindow();
    /****部分代码省略*****/

public boolean postDelayed(Runnable action, long delayMillis) 
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) 
        return attachInfo.mHandler.postDelayed(action, delayMillis);
    
    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().postDelayed(action, delayMillis);
    return true;

public boolean post(Runnable action) 
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) 
        return attachInfo.mHandler.post(action);
    
    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;

public boolean removeCallbacks(Runnable action) 
    if (action != null) 
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) 
            attachInfo.mHandler.removeCallbacks(action);
            attachInfo.mViewRootImpl.mChoreographer.removeCallbacks(
                  Choreographer.CALLBACK_ANIMATION, action, null);
        
        getRunQueue().removeCallbacks(action);
    
    return true;

postpostDelayedView的postDelayed方法深度思考这篇文章中进行过讲解,会在View执行dispatchAttachedToWindow方法的时候执行RunQueue中存放的Message

RunQueue.executeActions是在ViewRootImpl.performTraversal当中进行调用;

RunQueue.executeActions是在执行完host.dispatchAttachedToWindow(mAttachInfo, 0);之后调用;

RunQueue.executeActions是每次执行ViewRootImpl.performTraversal都会进行调用;

RunQueue.executeActions的参数是mAttachInfo中的Handler也就是ViewRootHandler;

从这里看也是没有任何问题的,我们使用View#post的消息都会在ViewAttached的时候进行执行;

一般程序在开发的过程中,如果涉及容器的使用那么必然需要考虑的生产和消费两个情况。
上面的源码我们是看了到了消息被执行的逻辑(最终所有的消息都会被放在MainLooper中被消费),如果涉及消息被移除呢?

public class HandlerActionQueue 
    public void removeCallbacks(Runnable action) 
        synchronized (this) 
            final int count = mCount;
            int j = 0;
            final HandlerAction[] actions = mActions;
            for (int i = 0; i < count; i++) 
                if (actions[i].matches(action)) 
                    // Remove this action by overwriting it within
                    // this loop or nulling it out later.
                    continue;
                
                if (j != i) 
                    // At least one previous entry was removed, so
                    // this one needs to move to the "new" list.
                    actions[j] = actions[i];
                
                j++;
            
            // The "new" list only has j entries.
            mCount = j;
            // Null out any remaining entries.
            for (; j < count; j++) 
                actions[j] = null;
            
        
    

移除消息的时候如果当前ViewmAttahInfo为空,那么我们只会移除RunQuque中换缓存的消息。。。

哦哦
原来是这样啊~!
确实只能这样~!

总结一下,如果View#mAttachInfo不为空那么你好,我好,大家好。否则View#post的消息会在缓存队列中等待被添加,但移除的消息却只能移除RunQueue中缓存的消息。如果此时RunQueue中的消息已经被同步到MainLooper中那么,抱歉没有View#mAttachInfo臣妾移除不了呀。

按照之前的业务代码,如果当前ViewdispatchDetachedFromWindow之后执行消息的移除操作,那么已经在MainLooper队列中的消息是无法被移除且如果继续添加轮播消息,那么就会造成轮播代码块的频繁执行。

文字描述可能一时间不太容易理解,下面是一次超预期之外的轮播(为什么会有多个轮播消息)流程简单的分析图:

再说post和postDelayed

如果只看相关源码我感觉是发现不了问题了,因为post最后执行的也是postDelayed方法。所以两者相比只不过时间差而已,这个时间差能造成什么影响呢?
回头看了看自己之前写的文章又一年对Android消息机制(Handler&Looper)的思考,其中有一个名词叫做同步屏障

同步屏障:忽略所有的同步消息,返回异步消息。再换句话说,同步屏障为Handler消息机制增加了一种简单的优先级机制,异步消息的优先级要高于同步消息。

同步屏障用的最多的就是页面的刷新(ViewRootImpl#mTraversalRunnable)相关文章可以阅读Android系统的编舞者Choreographer,而ViewRootImpl的独白,我不是一个View(布局篇)这篇文章讲述了View#dispatchAttachedToWindow的方法就是由ViewRootImpl#performTraversals触发的。

为什么要说同步屏障呢?上面的超预期轮播的流程图中可以看出View#dispatchAttachedToWindow的方法调用对于整个流程非常重要。移除添加两个消息两个如果由于postDelayed导致中间有其他消息的插入,而同步屏障是最有可能被插入的消息且这条消息会使View#mAttachInfo产生变化。
这就使原来有些小问题的代码雪上加霜,bug更容易复现。

话说RecycleView

为什么要提到这个问题,因为好多时候我们使用View.post执行任务是没有问题(PS:我感觉这个观点也是这个问题产生的最初的源头)。

我们知道RecycleView的内部子View仅仅是比屏幕大小多出一条预加载View,超过这个范围或者进入这个范围都会导致View被添加和移除。

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 
    /***部分代码省略***/
    private void initChildrenHelper() 
        this.mChildHelper = new ChildHelper(new Callback() 
            public int getChildCount() 
                return RecyclerView.this.getChildCount();
            

            public void addView(View child, int index) 
                RecyclerView.this.addView(child, index);
                RecyclerView.this.dispatchChildAttached(child);
            

            public int indexOfChild(View view) 
                return RecyclerView.this.indexOfChild(view);
            

            public void removeViewAt(int index) 
                View child = RecyclerView.this.getChildAt(index);
                if (child != null) 
                    RecyclerView.this.dispatchChildDetached(child);
                    child.clearAnimation();
                

                RecyclerView.this.removeViewAt(index);
            
        
        /***部分代码省略***/
    
    /***部分代码省略***/

如果我们频繁来回滑动列表,那么这个Banner会不断的被执行dispatchAttachedToWindowdispatchDetachedToWindow
这样导致View#mAttachInfo大部分时间为null,从而影响到业务代码中往主线程中发送的Message的执行逻辑。

文章到这里就讲述的差不多了,解决这个问题给我带来的感受挺深刻的,之前学习Android系统的相关源码只不过是大家都在学、面试都在问。
能在应用到实际研发过程中涉及到的知识点还是比较少,好多情况下都是能解决问题就行,也就是知其然而不知其所以然。
这次解决的问题能让我深切感受到fuck the source code is beatifully

文章到这里就全部讲述完啦,若有其他需要交流的可以留言哦~!

2023年祝你在新一年心情日新月异,快乐如糖似蜜,朋友重情重义,爱人不离不弃,工作频传佳绩,万事称心如意!

以上是关于iOS 图片使用探究(1)-- 图片基础知识+图片格式的主要内容,如果未能解决你的问题,请参考以下文章

Flutter图片加载和缓存机制探究

由View的onAttachedToWindow引发的图片轮播问题探究

由View的onAttachedToWindow引发的图片轮播问题探究

由View的onAttachedToWindow引发的图片轮播问题探究

iOS开发基础-UIScrollView实现图片缩放

WPF的Image控件图片不能显示出来的问题探究