• sc7731 Android 5.1 LCD驱动简明笔记之三


    此篇笔记基于sc7731 - android 5.1,对lcd的gralloc库做一个简明笔记。

    第一部分 调用gralloc.sc8830.so
    所谓的Gralloc模块,它就是一个模块,一个操作kernel层framebuffer驱动的动态库模块,它属于大名鼎鼎的HAL层。
    用的时候就加载到内存空间,不用的时候就从内存空间中卸载掉。下面看下系统如何将该模块加载到内存空间的。
    在Android系统中,所有访问HAL层模块的应用,都需要通过一个叫 hw_get_module() 的方法去获得需要的HAL 模块。

    一、hw_get_module() 声明说明

    定义在 7731_5.1/hardware/libhardware/hardware.c 文件中:

    1 /*
    2     id : 模块ID
    3     module : 对应ID的模块地址
    4 */
    5 int hw_get_module(const char *id, const struct hw_module_t **module);

    说明:
    每个模块都有自己的ID,比如gralloc 模块ID 就是 GRALLOC_HARDWARE_MODULE_ID,它是一个字符串的宏:

    #define GRALLOC_HARDWARE_MODULE_ID "gralloc"

    模块地址: 如果一切都正确,那么根据调用者给定的ID,就一定能找到一个对应的模块。hw_get_module() 就会把这个模块的地址返回给调用者。

    二、hw_get_module() 实现
    可以先猜想一下,加载一个动态库?
    第一步,需要先在指定的路径下(PATH)找到该库;
    第二步,加载该库到内存(解析库的内容)

    1. 找HAL 库

     1 int hw_get_module(const char *id, const struct hw_module_t **module)
     2 {
     3     return hw_get_module_by_class(id, NULL, module);
     4 }
     5 
     6 
     7 int hw_get_module_by_class(const char *class_id, const char *inst,
     8                            const struct hw_module_t **module)
     9 {
    10     //....
    11     
    12     //property_get();
    13     
    14     //去对应的路径找相应的库
    15     if (hw_module_exists(path, sizeof(path), name, prop) == 0){
    16          goto found;
    17     }
    18 
    19     //....
    20     
    21 found:
    22     /* load the module, if this fails, we're doomed, and we should not try
    23      * to load a different variant. */
    24      //如果找到,就开始执行加载库的动作
    25     return load(class_id, path, module);    
    26 }
    27 
    28 
    29 /*
    30  * Check if a HAL with given name and subname exists, if so return 0, otherwise
    31  * otherwise return negative.  On success path will contain the path to the HAL.
    32  */
    33 static int hw_module_exists(char *path, size_t path_len, const char *name,
    34                             const char *subname)
    35 {
    36     snprintf(path, path_len, "%s/%s.%s.so",
    37              HAL_LIBRARY_PATH2, name, subname);
    38     if (access(path, R_OK) == 0)
    39         return 0;
    40 
    41     snprintf(path, path_len, "%s/%s.%s.so",
    42              HAL_LIBRARY_PATH1, name, subname);
    43     if (access(path, R_OK) == 0)
    44         return 0;
    45 
    46     return -ENOENT;
    47 }

    hw_module_exists()就是找库的接口。
    库的路径:

    1 /** Base path of the hal modules */
    2 #if defined(__LP64__)
    3 #define HAL_LIBRARY_PATH1 "/system/lib64/hw"
    4 #define HAL_LIBRARY_PATH2 "/vendor/lib64/hw"
    5 #else
    6 #define HAL_LIBRARY_PATH1 "/system/lib/hw"
    7 #define HAL_LIBRARY_PATH2 "/vendor/lib/hw"
    8 #endif

    很显然,库的路径是可以修改的。

    access() 是测试该路径下的该库,是否可用,以及其具体权限如何(读写)。
    man access 的结果:

    1 NAME
    2        access - check real user's permissions for a file
    3 
    4 SYNOPSIS
    5        #include <unistd.h>
    6 
    7        int access(const char *pathname, int mode);
    8 ....

    说明:
    在找库的过程中,会使用 property_get()接口去获取相关的属性。对于该接口,涉及到了Android property系统,暂时对这块未做探究。

    2. 加载HAL 库( 解析HAL库)

    如果找到该库,而且该库也可以读,那就load 到内存空间吧。

     1 /**
     2  * Load the file defined by the variant and if successful
     3  * return the dlopen handle and the hmi.
     4  * @return 0 = success, !0 = failure.
     5  */
     6 static int load(const char *id,
     7         const char *path,
     8         const struct hw_module_t **pHmi)
     9 {
    10     int status;
    11     void *handle;
    12     struct hw_module_t *hmi;
    13 
    14     /*
    15      * load the symbols resolving undefined symbols before
    16      * dlopen returns. Since RTLD_GLOBAL is not or'd in with
    17      * RTLD_NOW the external symbols will not be global
    18      */
    19     handle = dlopen(path, RTLD_NOW);
    20 
    21     //...
    22 
    23     /* Get the address of the struct hal_module_info. */
    24     // #define HAL_MODULE_INFO_SYM_AS_STR  "HMI"
    25     const char *sym = HAL_MODULE_INFO_SYM_AS_STR;
    26     hmi = (struct hw_module_t *)dlsym(handle, sym);
    27 
    28     /* Check that the id matches */
    29     //比较模块的ID,是否符合要求
    30     if (strcmp(id, hmi->id) != 0) {
    31         //...
    32     }
    33 
    34     hmi->dso = handle;
    35 
    36     /* success */
    37     status = 0;
    38 
    39     done:
    40     if (status != 0) {
    41         hmi = NULL;
    42         if (handle != NULL) {
    43             dlclose(handle);
    44             handle = NULL;
    45         }
    46     } else {
    47         //....
    48     }
    49 
    50     //通过指针的方式,将对应的HAL module的地址返回给hw_get_module()函数调用者
    51     *pHmi = hmi;
    52 
    53     return status;
    54 }

    (1) Linux 下对应动态库的操作函数列表:

    1 #include <dlfcn.h>
    2 
    3 void *dlopen(const char *filename, int flag);
    4 
    5 char *dlerror(void);
    6 
    7 void *dlsym(void *handle, const char *symbol);
    8 
    9 int dlclose(void *handle);

    Link with -ldl.
    注意使用这些库函数的时候,需要 -l 的方式连接 dl 库

    (2) 模块的symbol(重要)
    在上述代码片段中,dlsym()函数的操作需要注意下:
    使用dlsym()函数 需要两个参数:一个是dlopen()函数打开的模块句柄;另外一个是模块的symbol。而关键的就是这个 symbol。
    从上面可以看见,HAL 层所有模块的symbol 都被宏定义成了"HMI" 这个字符串。换句话说,所有的HAL模块都需要导出一个 "HMI"的 symbol。而dlsym()函数会根据"HMI" 这个symbol找到模块真正的地址。
    在Linux下,所有的HAL层模块都是elf格式的二进制文件,使用readelf可以查看:

    readelf -s ./out/target/product/w830_0203/system/lib/hw/gralloc.sc8830.so

     1 Symbol table '.dynsym' contains 66 entries:
     2    Num:    Value  Size Type    Bind   Vis      Ndx Name
     3      0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     4      1: 00000000     0 FUNC    GLOBAL DEFAULT  UND __cxa_finalize
     5      2: 00000000     0 FUNC    GLOBAL DEFAULT  UND __cxa_atexit
     6      3: 00000000     0 FUNC    GLOBAL DEFAULT  UND strncmp
     7      4: 00001d3d   152 FUNC    GLOBAL DEFAULT    8 _Z17alloc_device_openPK11
     8      5: 000027c1   228 FUNC    GLOBAL DEFAULT    8 _Z23framebuffer_device_op
     9      6: 00000000     0 FUNC    GLOBAL DEFAULT  UND __aeabi_unwind_cpp_pr0
    10      7: 00000000     0 FUNC    GLOBAL DEFAULT  UND __android_log_print
    11      8: 00000000     0 FUNC    GLOBAL DEFAULT  UND ion_sync_fd
    12      9: 00000000     0 FUNC    GLOBAL DEFAULT  UND memset
    13     10: 00000000     0 FUNC    GLOBAL DEFAULT  UND ion_invalidate_fd
    14     11: 00000000     0 FUNC    GLOBAL DEFAULT  UND getpid
    15     12: 00000000     0 FUNC    GLOBAL DEFAULT  UND pthread_mutex_lock
    16     13: 00000000     0 FUNC    GLOBAL DEFAULT  UND munmap
    17     14: 00000000     0 FUNC    GLOBAL DEFAULT  UND __errno
    18     15: 00000000     0 FUNC    GLOBAL DEFAULT  UND strerror
    19     16: 00000000     0 FUNC    GLOBAL DEFAULT  UND pthread_mutex_unlock
    20     17: 00000000     0 FUNC    GLOBAL DEFAULT  UND ion_open
    21     18: 00000000     0 FUNC    GLOBAL DEFAULT  UND mmap
    22     19: 000015dd   224 FUNC    GLOBAL DEFAULT    8 _ZN16private_module_tC2Ev
    23     20: 00000000     0 FUNC    GLOBAL DEFAULT  UND pthread_mutex_init
    24     21: 000015dd   224 FUNC    GLOBAL DEFAULT    8 _ZN16private_module_tC1Ev
    25     22: 0000501c   444 OBJECT  GLOBAL DEFAULT   17 HMI
    26     23: 00000000     0 FUNC    GLOBAL DEFAULT  UND ion_close
    27     24: 00000000     0 FUNC    GLOBAL DEFAULT  UND close
    28     25: 00000000     0 FUNC    GLOBAL DEFAULT  UND _ZdlPv
    29     26: 00000000     0 FUNC    GLOBAL DEFAULT  UND ion_free
    30     27: 00000000     0 FUNC    GLOBAL DEFAULT  UND ion_alloc
    31     28: 00000000     0 FUNC    GLOBAL DEFAULT  UND ion_share
    32     29: 00000000     0 FUNC    GLOBAL DEFAULT  UND _Znwj
    33     30: 00002351  1132 FUNC    GLOBAL DEFAULT    8 _Z24init_frame_buffer_loc
    34     31: 00000000     0 FUNC    GLOBAL DEFAULT  UND dup
    35     32: 00001df1    10 FUNC    GLOBAL DEFAULT    8 _Z19compositionCompleteP2
    36     33: 00000000     0 FUNC    GLOBAL DEFAULT  UND glFinish
    37     34: 00000000     0 FUNC    GLOBAL DEFAULT  UND __aeabi_unwind_cpp_pr1
    38     35: 00000000     0 FUNC    GLOBAL DEFAULT  UND __sprintf_chk
    39     36: 00000000     0 FUNC    GLOBAL DEFAULT  UND fopen
    40     37: 00000000     0 FUNC    GLOBAL DEFAULT  UND fseek
    41     38: 00000000     0 FUNC    GLOBAL DEFAULT  UND __strlen_chk
    42     39: 00000000     0 FUNC    GLOBAL DEFAULT  UND fwrite
    43     40: 00000000     0 FUNC    GLOBAL DEFAULT  UND fclose
    44     41: 00000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail
    45     42: 00000000     0 OBJECT  GLOBAL DEFAULT  UND __stack_chk_guard
    46     43: 00000000     0 FUNC    GLOBAL DEFAULT  UND clock_gettime
    47     44: 00001f05   168 FUNC    GLOBAL DEFAULT    8 _Z17getApctFpsSupportv
    48     45: 00000000     0 FUNC    GLOBAL DEFAULT  UND fread
    49     46: 00000000     0 FUNC    GLOBAL DEFAULT  UND atol
    50     47: 000051d8     1 OBJECT  GLOBAL DEFAULT   17 gIsApctRead
    51     48: 00005218     1 OBJECT  GLOBAL DEFAULT   17 gIsApctFpsShow
    52     49: 00000000     0 FUNC    GLOBAL DEFAULT  UND __aeabi_l2f
    53     50: 00000000     0 FUNC    GLOBAL DEFAULT  UND __aeabi_l2d
    54     51: 00000000     0 FUNC    GLOBAL DEFAULT  UND setitimer
    55     52: 00000000     0 FUNC    GLOBAL DEFAULT  UND signal
    56     53: 00002a7d   316 FUNC    GLOBAL DEFAULT    8 _Z7dump_fbPvP17fb_var_scr
    57     54: 00000000     0 FUNC    GLOBAL DEFAULT  UND ioctl
    58     55: 00000000     0 FUNC    GLOBAL DEFAULT  UND memcpy
    59     56: 00000000     0 FUNC    GLOBAL DEFAULT  UND __aeabi_uldivmod
    60     57: 00000000     0 FUNC    GLOBAL DEFAULT  UND snprintf
    61     58: 00000000     0 FUNC    GLOBAL DEFAULT  UND open
    62     59: 00000000     0 FUNC    GLOBAL DEFAULT  UND property_get
    63     60: 00000000     0 FUNC    GLOBAL DEFAULT  UND atoi
    64     61: 000028a5   472 FUNC    GLOBAL DEFAULT    8 _Z8dump_bmpPKcPvP10buffer
    65     62: 00000000     0 FUNC    GLOBAL DEFAULT  UND fscanf
    66     63: 00005018     0 NOTYPE  GLOBAL DEFAULT  ABS _edata
    67     64: 00005018     0 NOTYPE  GLOBAL DEFAULT  ABS __bss_start
    68     65: 00005254     0 NOTYPE  GLOBAL DEFAULT  ABS _end

    看第 22行:
    22: 0000501c 444 OBJECT GLOBAL DEFAULT 17 HMI
    那么,dlsym()根据传入的"HMI" 就会匹配到该地址。至于HAL 模块怎么导出这个"HMI" 的symbol呢? 待后续分析。
    这里需要注意的是,dlopen() 和 dlsym() 的返回值是一个万能指针。
    特别是dlsym()返回值是一个动态库的地址。在该处调用中,它的返回值类型做了一个强制转换,转成了 struct hw_module_t 的指针形式---说明,HAL层模块需要提供相同的数据结构,起码前面结构体大小的字节要一一对应
    也即是,接下来要分析的Gralloc模块中,要在模块的前 sizeof(struct hw_module_t) 个字节内(这里的前多少个字节,要除开elf文件的一系列其他说明信息,具体elf文件格式,此处略),提供一样的数据结构才行。(ps: 指针指向的是某段内存的开始地址嘛)


    第二部分 Gralloc库的实现框架
    既然调用者已经通过hw_get_module()函数找到了我们grallo.so 的具体地址,也就加载到了内存中。Gralloc模块就该开始工作了。
    接下来简略分析下Gralloc实现。

    1. 文件结构
    展讯的Gralloc 库实现位于:
    7731_5.1/vendor/sprd/open-source/libs/gralloc/utgard/

    包含的文件如下:

    其中, gralloc_module.cpp 是核心文件,其他文件是接口的封装,同gralloc_module.cpp形成调用关系。

    2 代码分析
    (1) 入口对象
    在 gralloc_module.cpp 中,定义了一个对象,也即是展讯的Gralloc模块,就是一个HAL对象。

    1 /*
    2  * HAL_MODULE_INFO_SYM will be initialized using the default constructor
    3  * implemented above
    4  */ 
    5 struct private_module_t HAL_MODULE_INFO_SYM;

    HAL_MODULE_INFO_SYM 实质上是一个宏,定义在 7731_5.1/hardware/libhardware/include/hardware/hardware.h 文件中

    1 #define HAL_MODULE_INFO_SYM         HMI

    这个前面在分析 模块的symbol 的时候,已经分析到了 dlsym() 需要依赖模块的导出为 "HMI"的 symbol。在这里给出来了。
    至于网上有部分说法是,每个HAL 模块必须有一个 HAL_MODULE_INFO_SYM 这个东西。正确也不完全正确,我完全可以写成 struct private_module_t HMI 也没错。

    (2) struct private_module_t 类

    定义在了gralloc_priv.h文件中:

     1 struct private_module_t {
     2     gralloc_module_t base;                      /*很重要的一个成员,这里需要注册许多关键性的东西*///在该类的构造函数中分析
     3 
     4     private_handle_t* framebuffer;                 /* 指向图形缓冲区的句柄 */  
     5     uint32_t flags;                              /* 用来标志系统帧缓冲区是否支持双缓冲 */  
     6     uint32_t numBuffers;                        /* 表示系统帧缓冲的个数 */
     7     uint32_t bufferMask;                        /* 记录系统帧缓冲的使用情况 */  
     8     pthread_mutex_t lock;                        /* 保护结构体private_module_t的并行访问 */
     9     buffer_handle_t currentBuffer;              /* 描述当前正在被渲染的图形缓冲区 */ 
    10     int pmem_master;                            /* pmem设备节点的描述符 */
    11     void* pmem_master_base;                        /* pmem的起始虚拟地址 */
    12 
    13     struct fb_var_screeninfo info;                /* lcd的可变参数 */ 
    14     struct fb_fix_screeninfo finfo;                /* lcd的固定参数 */
    15     float xdpi;                                    /* x方向上每英寸的像素数量 */ 
    16     float ydpi;                                    /* y方向上每英寸的像素数量 */ 
    17     float fps;                                    /* lcd的刷新率 */
    18 };

    (注释参考 http://blog.csdn.net/g_salamander/article/details/8424334)

    base 成员非常重要,其一是需要注册许多关键性东西;其二,已经在前面说过,它内部的第一个变量需要是 struct hw_module_t 类型的---否则,dlsym()返回值的强制转换,将转成什么玩意儿呢?

     1 typedef struct gralloc_module_t {
     2     struct hw_module_t common; //这里必须是 struct hw_module_t 类型的变量(成员)
     3     
     4     /*注册一个图形缓冲区*/
     5     int (*registerBuffer)(struct gralloc_module_t const* module,
     6             buffer_handle_t handle);
     7 
     8     /*注销一个图形缓冲区*/
     9     int (*unregisterBuffer)(struct gralloc_module_t const* module,
    10             buffer_handle_t handle);
    11     
    12     /*在使用图形缓冲区前,先调用该函数加锁*/
    13     int (*lock)(struct gralloc_module_t const* module,
    14             buffer_handle_t handle, int usage,
    15             int l, int t, int w, int h,
    16             void** vaddr);
    17     
    18     /*图形缓冲器使用完毕,调用该函数解锁*/
    19     int (*unlock)(struct gralloc_module_t const* module,
    20             buffer_handle_t handle);
    21 
    22     /* reserved for future use */
    23     int (*perform)(struct gralloc_module_t const* module,
    24             int operation, ... );
    25 
    26     int (*lock_ycbcr)(struct gralloc_module_t const* module,
    27             buffer_handle_t handle, int usage,
    28             int l, int t, int w, int h,
    29             struct android_ycbcr *ycbcr);
    30 
    31     int (*lockAsync)(struct gralloc_module_t const* module,
    32             buffer_handle_t handle, int usage,
    33             int l, int t, int w, int h,
    34             void** vaddr, int fenceFd);
    35 
    36     int (*unlockAsync)(struct gralloc_module_t const* module,
    37             buffer_handle_t handle, int* fenceFd);
    38 
    39     int (*lockAsync_ycbcr)(struct gralloc_module_t const* module,
    40             buffer_handle_t handle, int usage,
    41             int l, int t, int w, int h,
    42             struct android_ycbcr *ycbcr, int fenceFd);
    43 
    44     /* reserved for future use */
    45     void* reserved_proc[3];
    46 } gralloc_module_t;

    (3) struct private_module_t 类的构造函数
    c++中,一个对象的建立,都会调用其构造函数。展讯在这里借助类的构造函数,搞定了gralloc模块的一切注册

     1 private_module_t::private_module_t()
     2 {
     3 #define INIT_ZERO(obj) (memset(&(obj),0,sizeof((obj))))
     4 
     5     base.common.tag = HARDWARE_MODULE_TAG;
     6     base.common.version_major = 1;
     7     base.common.version_minor = 0;
     8     base.common.id = GRALLOC_HARDWARE_MODULE_ID; //Gralloc 的模块ID
     9     base.common.name = "Graphics Memory Allocator Module";
    10     base.common.author = "ARM Ltd.";
    11     base.common.methods = &gralloc_module_methods;
    12     base.common.dso = NULL;
    13     INIT_ZERO(base.common.reserved);
    14 
    15     base.registerBuffer = gralloc_register_buffer;
    16     base.unregisterBuffer = gralloc_unregister_buffer;
    17     base.lock = gralloc_lock;
    18     base.lock_ycbcr = gralloc_lock_ycbcr;
    19     base.unlock = gralloc_unlock;
    20     base.perform = NULL;
    21     INIT_ZERO(base.reserved_proc);
    22 
    23     framebuffer = NULL;
    24     flags = 0;
    25     numBuffers = 0;
    26     bufferMask = 0;
    27     pthread_mutex_init(&(lock), NULL);
    28     currentBuffer = NULL;
    29     INIT_ZERO(info);
    30     INIT_ZERO(finfo);
    31     xdpi = 0.0f; 
    32     ydpi = 0.0f; 
    33     fps = 0.0f;
    34     swapInterval = 1;
    35 
    36     initialize_blk_conf();
    37 
    38 #undef INIT_ZERO
    39 };

    一个gralloc或者说一个HAL模块的模型,就上面那个构造函数里的样子。而需要我们自己实现的是:

    gralloc_module_methods (base.common.methods)
    gralloc_register_buffer(base.registerBuffer)
    gralloc_unregister_buffer(base.unregisterBuffer)
    gralloc_lock(base.lock)
    base.unlock(gralloc_unlock)

    (4) gralloc_module_methods

    1 static struct hw_module_methods_t gralloc_module_methods =
    2 {
    3     open: gralloc_device_open
    4 };

    (4.1)

     1 static int gralloc_device_open(const hw_module_t* module, const char* name, hw_device_t** device)
     2 {
     3     int status = -EINVAL;
     4 
     5     if (!strncmp(name, GRALLOC_HARDWARE_GPU0, MALI_GRALLOC_HARDWARE_MAX_STR_LEN))     //GPU0
     6     {
     7         status = alloc_device_open(module, name, device);
     8     }
     9     else if (!strncmp(name, GRALLOC_HARDWARE_FB0, MALI_GRALLOC_HARDWARE_MAX_STR_LEN)) //FB
    10     {
    11         status = framebuffer_device_open(module, name, device);
    12     }
    13 
    14     return status;
    15 }

    展讯这里经过一个循环调用,最终会进入的alloc_device_open()的调用。该函数处于 alloc_device.cpp 文件。

     1 int alloc_device_open(hw_module_t const *module, const char *name, hw_device_t **device)
     2 {
     3     //..
     4     alloc_device_t *dev;
     5 
     6     dev = new alloc_device_t;
     7     //...
     8     dev->alloc = alloc_device_alloc;
     9     dev->free = alloc_device_free;
    10     //...
    11 }

    alloc_device_alloc() 函数是与Framebuffer设备文件进行交互的。里面会调用gralloc_alloc_framebuffer() 根据LCD采用的颜色模式分配不同大小的显存空间。

    (4.2)

     1 int framebuffer_device_open(hw_module_t const *module, const char *name, hw_device_t **device)
     2 {
     3     //...
     4     status = gralloc_open(module, &gralloc_device);
     5     
     6     private_module_t *m = (private_module_t *)module;
     7     status = init_frame_buffer(m);
     8     
     9     //...
    10     framebuffer_device_t *dev = new framebuffer_device_t();
    11     
    12     //...
    13 }

    以上这些函数都是以注册的方式放在了Gralloc模块中,当一个应用把gralloc模块加载到了内存时,可以通过钩子函数调用的方式,在顶层使用它们。

    其他函数代码分析不再贴了,毕竟只是一种逻辑而已了。

    (over)
    2016-1-07

    总结:

         通过对展讯LCD 底层框架的简略性学习,做了三个简略性的笔记。第一篇是一个总的笔记,第二篇是针对framebuffer做了一个简略的分析,第三篇也就会这篇对HAL层gralloc做了一个简略性的总结。

    以前未学驱动之前,或许驱动是如此的神秘,kernel是如此的神秘。但是经过几个月kernel/driver代码分析下来后,这种神秘的面纱或许是可以揭开了。

    而这次从底层到上层的一个比较完整性的分析,更是拓宽了知识面,也改变了某些想法。也有可能,对职业发展方向有一定的影响。

       lcd驱动->framebuffer驱动->gralloc, 这里没有算完。gralloc 的上面是更加宽广的世界,比如mutilplay,opengl等等。精彩的世界,未知的世界,等待着去探索。

       于此,记之。

  • 相关阅读:
    VUE 多页面配置(二)
    VUE 多页面配置(一)
    VUE (vue-cli)脚手架项目说明
    CSS 三角形与圆形
    协同过滤算法之组合加权评分
    用户投票算法
    Android开发学习总结(一)——搭建最新版本的Android开发环境
    关于统计变换(CT/MCT/RMCT)算法的学习和实现
    SIFT 特征提取算法总结
    Android Listener侦听的N种写法
  • 原文地址:https://www.cnblogs.com/chineseboy/p/5110721.html
Copyright © 2020-2023  润新知