面试记录01

最近开始面试,有些面试中问到的问题感觉自己答得不好,在这里记录下。

OpenGL相关

使用OpenGL渲染时,将视频数据写入纹理的耗时是多少

这个问题我之前确实没有研究过,因此回家试了下。

在iphone5s ios9.2.1的系统下,采集640*480的视频来渲染时,耗时如下:

2019-07-04 19:29:58.638 OpenGLDemo01[231:9211] render time 6.269287 2019-07-04 19:29:58.665 OpenGLDemo01[231:9211] glTexImage2D y 1.178467 2019-07-04 19:29:58.667 OpenGLDemo01[231:9211] glTexImage2D uv 0.995850 2019-07-04 19:29:58.670 OpenGLDemo01[231:9211] render time 6.311768 2019-07-04 19:29:58.699 OpenGLDemo01[231:9211] glTexImage2D y 1.174072 2019-07-04 19:29:58.701 OpenGLDemo01[231:9211] glTexImage2D uv 1.420166 2019-07-04 19:29:58.704 OpenGLDemo01[231:9211] render time 6.414795 2019-07-04 19:29:58.732 OpenGLDemo01[231:9211] glTexImage2D y 1.435059 2019-07-04 19:29:58.734 OpenGLDemo01[231:9211] glTexImage2D uv 1.227783

在1280*720的分辨率下:

2019-07-04 19:35:44.709 OpenGLDemo01[239:10627] render time 9.113770 2019-07-04 19:35:44.736 OpenGLDemo01[239:10627] glTexImage2D y 2.461914 2019-07-04 19:35:44.738 OpenGLDemo01[239:10627] glTexImage2D uv 1.538086 2019-07-04 19:35:44.740 OpenGLDemo01[239:10627] render time 6.697998 2019-07-04 19:35:44.769 OpenGLDemo01[239:10627] glTexImage2D y 2.527344 2019-07-04 19:35:44.771 OpenGLDemo01[239:10627] glTexImage2D uv 1.502930 2019-07-04 19:35:44.773 OpenGLDemo01[239:10627] render time 6.559082 2019-07-04 19:35:44.803 OpenGLDemo01[239:10627] glTexImage2D y 2.435547 2019-07-04 19:35:44.805 OpenGLDemo01[239:10627] glTexImage2D uv 1.551270

在1920*1080的分辨率下:

2019-07-04 19:38:24.197 OpenGLDemo01[245:11587] render time 9.066162 2019-07-04 19:38:24.228 OpenGLDemo01[245:11587] glTexImage2D y 4.268066 2019-07-04 19:38:24.231 OpenGLDemo01[245:11587] glTexImage2D uv 2.102051 2019-07-04 19:38:24.234 OpenGLDemo01[245:11587] render time 10.524902 2019-07-04 19:38:24.262 OpenGLDemo01[245:11587] glTexImage2D y 5.209961 2019-07-04 19:38:24.266 OpenGLDemo01[245:11587] glTexImage2D uv 2.260986 2019-07-04 19:38:24.268 OpenGLDemo01[245:11587] render time 10.947998 2019-07-04 19:38:24.293 OpenGLDemo01[245:11587] glTexImage2D y 4.169922 2019-07-04 19:38:24.296 OpenGLDemo01[245:11587] glTexImage2D uv 1.626953

可以看出,随着分辨率的提升,每次渲染的时间是增加的,同时每次渲染之中将视频数据写入纹理的耗时占比也随着分辨率的提升而提高,这应该是因为CPU的数据和GPU的数据不共享,需要有拷贝操作,而分辨率增大后数据也增大,耗时就会增多。 根据时间也可以看出,5s这样的设备可以支撑1080p视频60帧的渲染,而如果渲染多个视频流,可能就需要降低帧数或分辨率了。

使用OpenGL渲染时,对纹理的采样是怎么处理的

对于这个问题,我一开始只想到在片段着色器中定义sampler2D采样器,然后在写入纹理的时候将对应的采样器设置对应的纹理位置。面试官之后提示,比如说一个较小的纹理,要渲染到一个较大的view上时,多出来的像素是怎么确定颜色的,我的回答是取附近像素的颜色或者根据周围像素计算一个近似的颜色。

使用纹理坐标获取纹理颜色叫做采样(Sampling)。

一般传入纹理坐标时只是传入几个点,OpenGL会对纹理坐标进行插值,来计算出其他的像素的颜色,我们可以主动设置一些情况的插值方式,告诉OpenGL怎样对纹理采样。

一般会设置纹理的环绕方式和纹理过滤。 纹理环绕方式一般是用来设置纹理坐标超过(0,0)-(1,1)的范围后如何处理纹理的,默认是GL_REPEAT,重复纹理,还有一些其他的处理方式。如下图: texture_wrapping

纹理过滤就是处理上面面试官提到的情况,例如一个很大的物体但是纹理的分辨率很低的时候如何处理,一般分成线性过滤和邻近过滤,线性过滤就是根据其他像素的颜色算出一个近似值,邻近过滤就是使用最接近的位置的像素颜色,OpenGL默认处理是使用邻近过滤。

texture_filtering

还有多级渐远纹理,一般是处理高分辨率纹理在较远位置显示。

距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。

OpenGL纹理

描述一下OpenGL的渲染管线

其实这个问题用learnopengl-cn上的一个图就可以概括。

pipeline

渲染管线

描述下离屏渲染

我只答出了在设置一些圆角、阴影、遮罩时,会触发离屏渲染,导致一定的性能损耗。然后面试官追问为什么会消耗性能,我回答需要额外开辟一些缓冲,以及切换上下文,导致一些额外的开销。

实际上这样的回答是不够全面的,我之前对离屏渲染的理解也比较浅。

屏幕显示内容的原理

理解离屏渲染相关问题首先要知道屏幕显示内容的原理。

tile_render

iOS系统下界面最终的显示也是通过OpenGL的渲染显示的(最新的系统可能已经替换成Metal),整个渲染过程就是一个渲染管线的完整流程。摘抄一段液晶屏幕显示的原理:

通常来说,计算机系统中 CPU、GPU、显示器是以上面这种方式协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

在最简单的情况下,帧缓冲区只有一个,这时帧缓冲区的读取和刷新都都会有比较大的效率问题。为了解决效率问题,显示系统通常会引入两个缓冲区,即双缓冲机制。在这种情况下,GPU 会预先渲染好一帧放入一个缓冲区内,让视频控制器读取,当下一帧渲染好后,GPU 会直接把视频控制器的指针指向第二个缓冲器。如此一来效率会有很大的提升。

对于当前屏幕渲染,就是直接将绘制的内容放入屏幕显示的帧缓冲区,而离屏渲染,则是另外开辟帧缓冲区进行渲染。

离屏渲染出现的原因

离屏渲染出现的原因首先是GPU渲染的图元只能是一些基本的类型,对应到OpenGL中就是GL_POINTS、GL_LINES、GL_LINE_STRIP、GL_TRIANGLES等,如果只是显示简单的形状,可以通过一次渲染就直接显示,如果有一些圆角或遮罩的绘制,就需要多次绘制,就不能直接在屏幕内渲染。离屏渲染还有一个用处,在shouldRasterize属性值为YES时,会将一些复杂的图层合成为一个位图缓存,在后续显示时可以复用这个缓冲,用来节省绘制的时间。

离屏渲染带来的问题

离屏渲染带来的问题,首先是消耗。

一般情况下,你需要避免离屏渲染,因为这是很大的消耗。直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。

其次,离屏渲染的将一些复杂的图层合成为一个位图缓存的功能,也可能会因为位图可能再也不需要被复用,或者位图需要复用时GPU却已经将其卸载,而使这个功能无效,因此shouldRasterize属性需要根据需要设置。

OC问题

category为什么不能添加成员变量

这个我没有答出来,只知道可以使用关联对象来实现在分类里添加属性和成员变量。

通过查看category的源码,可以看出并不包含存储成员变量的地方。而成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods; // 对象方法
    struct method_list_t *classMethods; // 类方法
    struct protocol_list_t *protocols; // 协议
    struct property_list_t *instanceProperties; // 属性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

关于关联对象,是调用objc_setAssociatedObject和objc_getAssociatedObject函数来使用的。 看下runtime的源码

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
40
41
42
43
44
45
46
47
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}
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
class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

 class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
    };
	
	class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
    public:
        void *operator new(size_t n) { return ::malloc(n); }
        void operator delete(void *ptr) { ::free(ptr); }
    };

可以看到实际上就是调用_object_set_associative_reference函数。其中AssociationsManager是一个全局的对象用于管理关联对象,它有一个AssociationsHashMap,是封装的哈希表,它以对象为key,value为ObjectAssociationMap。ObjectAssociationMap是一个对map的封装,用来存放真正的关联对象的值。 总结一下,一个实例对象就对应一个ObjectAssociationMap,而ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation,为ObjcAssociation中存储着关联对象的value和policy策略。由此可以看出关联对象并不直接存储在对象的内存中,所以可以在运行时添加成员变量。

load和initialize方法

这个问题我答得是load是在程序运行的时候就调用的,initialize是在这个类被加载的时候才会运行。面试官又问如果在分类中这两个方法是怎么调用的,我就不是很确定了。

继续看runtime的源码,load方法的调用

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
void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

可以看到先是循环调用类中的load方法,然后再调用分类中的load方法。

1
2
3
4
5
void callInitialize(Class cls)
{
    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);
    asm("");
}

而initialize方法,是通过runtime的objc_msgSend函数发送消息调用的,因此会和普通方法一样,通过方法列表查找到方法再调用,因此会被分类中定义的方法覆盖。

总结

经过几次面试,可以感觉到,我对OpenGL的基础掌握的还是不够扎实,只是有一些使用的经验,虽然有研究过如何用OpenGL去渲染视频,但是对于一些概念性的东西还是不够了解,还需要多看些OpenGL的基础。然后对于OC的一些特性,了解的也不够全,其实很多时候去仔细阅读runtime的代码可以了解很多OC的底层实现,因此需要找时间把runtime的源码再好好读一读。