老司机种菜


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 公益404

  • 搜索

常用命令备忘

发表于 2019-02-26 | 分类于 tips
git stash
  • git stash save "test-cmd-stash":给stash增加message
  • git stash pop:将缓存堆栈中的第一个stash删除,并将对应修改应用到当前的工作目录下。
  • git stash apply:将缓存堆栈中的stash多次应用到工作目录中,但并不删除stash拷贝
  • git stash list:查看现有stash
  • git stash drop stash@{0}:移除stash
  • git stash clear:删除所有缓存的stash
  • git stash show:查看指定stash的diff,后面可以跟着stash名字,还可以添加-p或–patch,查看特定stash的全部diff
  • git stash branch:创建一个新的分支,检出储藏工作时的所处的提交,重新应用工作
  • git stash -u/--include-untracked储藏untracked文件,git stash -a/--all:stash当前目录下所有修改
ffmpeg

Nginx安装配置

发表于 2019-02-15 | 分类于 env

CentOS安装Nginx

Nginx(“engine x”)是一款是由俄罗斯的程序设计师Igor Sysoev所开发高性能的 Web和 反向代理 服务器,也是一个 IMAP/POP3/SMTP 代理服务器。

在高连接并发的情况下,Nginx是Apache服务器不错的替代品。

系统平台: 阿里云CentOS7.6 64位

1.安装编译工具及库文件

1
yum -y install make zlib zlib-devel gcc-c++ libtool  openssl openssl-devel

2.首先要安装PCRE

PCRE 作用是让 Nginx 支持 Rewrite 功能

2.1 下载PCRE安装包:

1
2
cd /usr/local/src/
wget http://downloads.sourceforge.net/project/pcre/pcre/8.35/pcre-8.35.tar.gz

2.2 解压安装包

1
tar zxvf pcre-8.35.tar.gz

2.3 进入安装包目录

1
cd pcre-8.35

2.4 编译安装

1
2
./configure
make && make install
2.5 查看pcre版本
1
pcre-config --version

3.安装Nginx

3.1 下载 Nginx,下载地址:http://nginx.org/download/nginx-1.6.2.tar.gz

1
2
cd /usr/local/src/
wget http://nginx.org/download/nginx-1.6.2.tar.gz

3.2 解压安装包

1
tar zxvf nginx-1.6.2.tar.gz

3.3 进入安装包目录

1
cd nginx-1.6.2

3.4 编译安装

1
2
3
./configure --prefix=/usr/local/webserver/nginx --with-http_stub_status_module --with-http_ssl_module --with-pcre=/usr/local/src/pcre-8.35
make
make install

3.5 查看nginx版本

1
/usr/local/webserver/nginx/sbin/nginx -v

4. Nginx 配置

创建 Nginx 运行使用的用户 www:

1
2
/usr/sbin/groupadd www 
/usr/sbin/useradd -g www www

配置nginx.conf ,将/usr/local/webserver/nginx/conf/nginx.conf替换为以下内容

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
cat /usr/local/webserver/nginx/conf/nginx.conf

user www www;
worker_processes 2; #设置值和CPU核心数一致
error_log /usr/local/webserver/nginx/logs/nginx_error.log crit; #日志位置和日志级别
pid /usr/local/webserver/nginx/nginx.pid;
#Specifies the value for maximum file descriptors that can be opened by this process.
worker_rlimit_nofile 65535;
events
{
use epoll;
worker_connections 65535;
}
http
{
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" $http_x_forwarded_for';

#charset gb2312;

server_names_hash_bucket_size 128;
client_header_buffer_size 32k;
large_client_header_buffers 4 32k;
client_max_body_size 8m;

sendfile on;
tcp_nopush on;
keepalive_timeout 60;
tcp_nodelay on;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
fastcgi_buffer_size 64k;
fastcgi_buffers 4 64k;
fastcgi_busy_buffers_size 128k;
fastcgi_temp_file_write_size 128k;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_types text/plain application/x-javascript text/css application/xml;
gzip_vary on;

#limit_zone crawler $binary_remote_addr 10m;
#下面是server虚拟主机的配置
server
{
listen 80;#监听端口
server_name localhost;#域名
index index.html index.htm index.php;
root /usr/local/webserver/nginx/html;#站点目录
location ~ .*\.(php|php5)?$
{
#fastcgi_pass unix:/tmp/php-cgi.sock;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi.conf;
}
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|ico)$
{
expires 30d;
# access_log off;
}
location ~ .*\.(js|css)?$
{
expires 15d;
# access_log off;
}
access_log off;
}

}

检查配置文件nginx.conf的正确性命令:

1
/usr/local/webserver/nginx/sbin/nginx -t

4. Nginx命令

1
2
3
4
/usr/local/webserver/nginx/sbin/nginx                      # 启动
/usr/local/webserver/nginx/sbin/nginx -s reload # 重新载入配置文件
/usr/local/webserver/nginx/sbin/nginx -s reopen # 重启 Nginx
/usr/local/webserver/nginx/sbin/nginx -s stop # 停止 Nginx

问题解决

nginx: [error] invalid PID number "" in "/usr/local/nginx/logs/nginx.pid" 解决方案

1
2
/usr/local/webserver/nginx/sbin/nginx -c /usr/local/nigin/conf/nginx.conf
/usr/local/webserver/nginx/sbin/nginx -s reload

FFMPEG常用命令

发表于 2019-02-11 | 分类于 FFMPEG
将多个视频片段合成一个视频

新建文本文件,输入要合成的视频列表(如果文件名有奇怪的字符,需要转意)

1
2
3
4
5
file 'output_train_video_0.mp4'
file 'output_train_video_1.mp4'
file 'output_train_video_2.mp4'
file 'output_train_video_3.mp4'
file 'output_train_video_4.mp4'

执行

1
ffmpeg -f concat -i filelist.txt -c copy output_set.mp4

OpenGL之EGL

发表于 2019-01-30 | 分类于 OpenGL

OpenGL ES的javax.microedition.khronos.openges包定义了平台无关的GL绘制指令,EGL(javax.microedition.khronos.egl)则定义了控制dispays,contexts以及surfaces的统一的平台接口.

image EGL 是 OpenGL ES 渲染 API 和本地窗口系统(native platform window system)之间的一个中间接口层,它主要由系统制造商实现。EGL提供如下机制:

  • 与设备的原生窗口系统通信
  • 查询绘图表面的可用类型和配置
  • 创建绘图表面
  • 在OpenGL ES 和其他图形渲染API之间同步渲染
  • 管理纹理贴图等渲染资源

为了让OpenGL ES能够绘制在当前设备上,我们需要EGL作为OpenGL ES与设备的桥梁。

image

  • Display(EGLDisplay)是对实际显示设备的抽象
  • Surface(EGLSurface)是对用来存储图像的内存区域FrameBuffer的抽象,包括Color Buffer, Stencil Buffer, Depth Buffer.
  • Context(EGLContext)存储OpenGLES绘图的一些状态信息

使用EGL绘制的一般步骤:

  1. 获取EGLDisplay对象
  2. 初始化与EGLDisplay之间的连接
  3. 获取EGLConfig实例
  4. 创建EGLContext实例
  5. 创建EGLSurface实例
  6. 连接EGLContext和EGLSurface
  7. 使用GL指令绘制图形
  8. 断开并释放与EGLSurface关联的EGLContext对象
  9. 删除EGLSurface对象
  10. 删除EGLContext对象
  11. 终止与EGLDisplay之间的连接

源码: 本地代码:

  • frameworks/native/opengl/libs/EGL:Android EGL框架,负责加载OpenGL函数库和EGL本地实现。
  • frameworks/native/opengl/libagl:Android提供的OpenGL软件库

JNI代码:

  • frameworks/base/core/jni/com_google_android_gles_jni_EGLImpl.cpp:EGL本地代码的JNI调用接口
  • frameworks/base/core/jni/com_google_android_gles_jni_GLImpl.cpp
  • frameworks/base/core/jni/android_opengl_GLESXXX.cpp:OpenGL功能函数的JNI调用接口

JAVA代码:

  • frameworks/base/opengl/java/javax/microedition/khronos/egl
  • frameworks/base/opengl/java/javax/microedition/khronos/opengles
  • frameworks/base/opengl/java/com/google/android/gles_jni/
  • frameworks/base/opengl/java/android/opengl :EGL和OpenGL的Java层接口,提供给应用开发者,通过JNI方式调用底层函数。
OpenGL ES/EGL Wrapper 库

前面我们已经介绍过 OpenGL ES/EGL Wrapper 库是一个将 OpenGL ES API 和 OpenGL ES API 具体实现绑定在一起的库,它对应的源码路径是:/frameworks/native/opengl/libs/,其中:

libGLESv1_CM.so:OpenGL ES 1.x API 的 Wrapper 库 libGLESv2.so:OpenGL ES 2.0 的 Wrapper 库 libGLESv3.so:OpenGL ES 3.0 的 Wrapper 库 其中因为 OpenGL ES 3.0 API 是兼容 OpenGL ES 2.0 API 的,所以 libGLESv2.so 库本质上和 libGLESv3.so 库是一样的。

OpenGL ES/EGL 实现库

如果Android系统平台支持 OpenGL ES 硬件加速渲染,那么 OpenGL ES/EGL 实现库由系统厂商以.so的共享链接库的形式提供,例如,Nexus 9 平板中的厂商提供的 OpenGL ES/EGL 实现库为:

1
2
flounder:/vendor/lib64/egl # ls
libEGL_tegra.so libGLESv1_CM_tegra.so libGLESv2_tegra.so

如果Android系统平台不支持 OpenGL ES 硬件加速渲染,那么它就会默认启用软件模拟渲染,这时 OpenGL ES/EGL 实现库就是由 AOSP 提供,链接库的存在的路径为: /system/lib64/egl/libGLES_android.so。而 libGLES_android.so 库在 Android 7.1 系统对应的实现源码路径为:/frameworks/native/opengl/libagl/ 。

Android 7.1 中加载 OpenGL ES 库的过程

Android 中图形渲染所采用的方式(硬件 or 软件)是在系统启动之后动态确定的,而确定渲染方式的这个源码文件就是 /frameworks/native/opengl/libs/EGL/Loader.cpp 。

####### Android 7.1 OpenGL ES 库和 EGL 库加载说明 How Android finds OpenGL libraries, and the death of egl.cfg 这篇文章中提到了非常关键的一点,就是从 Android Kitkat 4.4 之后,Android 中加载 OpenGL ES/EGL 库的方法发生了变化了(但是整个加载过程都是由 /frameworks/native/opengl/libs/EGL/Loader.cpp 程序所决定的,也就是说 Loader.cpp 文件发生了变化)。

在 Android 4.4 之前,加载 OpenGL ES 库是由 /system/lib/egl/egl.cfg 文件所决定的,通过读取这个配置文件来确定是加载 OpenGL ES 软件模拟实现的库,还是OpenGL ES 硬件加速实现的库。

但是,在Android 4.4 之后,Android 不再通过读取 egl.cfg 配置文件的方式来加载 OpenGL ES 库,新的加载 OpenGL ES 库的规则,如下所示:

从 /system/lib/egl 或者 /vendor/lib/egl/ 目录下加载 libGLES.so 库文件或者 libEGL_vendor.so,libGLESv1_CM_vendor.so,libGLESv2_vendor.so 库文件。 为了向下兼容旧的库的命名方式,同样也会加载 /system/lib/egl 或者 /vendor/lib/egl/ 目录下的 libGLES_.so 或者 libEGL_.so,libGLESv1CM.so,libGLESv2_.so 库文件。 3.2 硬件加速渲染 or 软件模拟渲染? 前面我们提到 OpenGL ES 库的实现方式有两种,一种是硬件加速实现,一种是软件模拟实现,那么系统是怎么确定加载那一种 OpenGL ES 库的呢?

Android 7.1 源码中负责加载 OpenGL ES/EGL 库部分的代码位于:/frameworks/native/opengl/libs/EGL/Loader.cpp 文件中,这个文件中代码的主要入口函数是 Loader::open() 函数,而决定加载硬件加速渲染库还是软件模拟渲染库主要涉及到下面两个函数:

setEmulatorGlesValue() checkGlesEmulationStatus() 下面就来简要的分析一下 Android 系统是如何选择加载硬件加速渲染库还是软件模拟渲染库:

  1. 首先,Loader::open() 入口函数会调用 setEmulatorGlesValue() 从 property 属性系统中获取一些属性值来判断当前 Android 系统是否在 Emulator 环境中运行,并根据读取出来的信息来重新设置新的属性键值对,setEmulatorGlesValue() 函数的代码如下所示:

    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
    static void setEmulatorGlesValue(void) {
    char prop[PROPERTY_VALUE_MAX];
    property_get("ro.kernel.qemu", prop, "0"); //读取 ro.kernel.qemu 属性值,判断Android系统是否运行在 qemu 中
    if (atoi(prop) != 1) return;

    property_get("ro.kernel.qemu.gles", prop, "0"); //读取 ro.kernel.qemu.gles 属性值,判断 qemu 中 OpenGL ES 库的实现方式
    if (atoi(prop) == 1) {
    ALOGD("Emulator has host GPU support, qemu.gles is set to 1.");
    property_set("qemu.gles", "1");
    return;
    }

    // for now, checking the following
    // directory is good enough for emulator system images
    const char* vendor_lib_path =
    #if defined(__LP64__)
    "/vendor/lib64/egl";
    #else
    "/vendor/lib/egl";
    #endif

    const bool has_vendor_lib = (access(vendor_lib_path, R_OK) == 0);
    //如果存在 vendor_lib_path 这个路径,那么就说明厂商提供了 OpenGL ES库自己的软件模拟渲染库,而不是 Android 系统自己编译得到的软件模拟渲染库
    if (has_vendor_lib) {
    ALOGD("Emulator has vendor provided software renderer, qemu.gles is set to 2.");
    property_set("qemu.gles", "2");
    } else {
    ALOGD("Emulator without GPU support detected. "
    "Fallback to legacy software renderer, qemu.gles is set to 0.");
    property_set("qemu.gles", "0"); //最后,默认采取的是方案就是调用传统的Android系统自己编译得到软件模拟渲染库
    }
    }
  2. 在 load_system_driver() 函数中,内部类 MatchFile 类中会调用 checkGlesEmulationStatus() 函数来检查 Android 系统是否运行在模拟器中,以及在模拟器中是否启用了主机硬件加速的功能,然后根据 checkGlesEmulationStatus() 函数的返回状态值来确定要加载共享链接库的文件绝对路径。load_system_driver() 和 checkGlesEmulationStatus() 函数代码如下所示:

    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
    static void* load_system_driver(const char* kind) {
    ATRACE_CALL();
    class MatchFile {
    public:
    //这个函数作用是返回需要加载打开的 OpenGL ES 和 EGL API 实现库文件的绝对路径
    static String8 find(const char* kind) {
    String8 result;
    int emulationStatus = checkGlesEmulationStatus(); //检查 Android 系统是否运行在模拟器中,以及在模拟器中是否启用了主机硬件加速的功能
    switch (emulationStatus) {
    case 0: //Android 运行在模拟器中,使用系统软件模拟实现的 OpenGL ES API 库 libGLES_android.so
    #if defined(__LP64__)
    result.setTo("/system/lib64/egl/libGLES_android.so");
    #else
    result.setTo("/system/lib/egl/libGLES_android.so");
    #endif
    return result;
    case 1: // Android 运行在模拟器中,通过主机系统中实现 OpenGL ES 加速渲染,通过 libGLES_emulation.so 库将 OpenGL ES API 指令重定向到 host 中执行
    // Use host-side OpenGL through the "emulation" library
    #if defined(__LP64__)
    result.appendFormat("/system/lib64/egl/lib%s_emulation.so", kind);
    #else
    result.appendFormat("/system/lib/egl/lib%s_emulation.so", kind);
    #endif
    return result;
    default:
    // Not in emulator, or use other guest-side implementation
    break;
    }

    // 如果不是上面两种情况,就根据库的命名规则去找到厂商实现库文件的绝对路径
    String8 pattern;
    pattern.appendFormat("lib%s", kind);
    const char* const searchPaths[] = {
    #if defined(__LP64__)
    "/vendor/lib64/egl",
    "/system/lib64/egl"
    #else
    "/vendor/lib/egl",
    "/system/lib/egl"
    #endif
    };

    ......
    }

    }
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
static int
checkGlesEmulationStatus(void)
{
/* We're going to check for the following kernel parameters:
*
* qemu=1 -> tells us that we run inside the emulator
* android.qemu.gles=<number> -> tells us the GLES GPU emulation status
*
* Note that we will return <number> if we find it. This let us support
* more additionnal emulation modes in the future.
*/
char prop[PROPERTY_VALUE_MAX];
int result = -1;

/* Check if hardware acceleration disabled explicitly */
property_get("debug.egl.hw", prop, "1"); //读取 debu.egl.hw 属性值,判断3D硬件加速功能是否被关闭了
if (!atoi(prop)) {
ALOGD("3D hardware acceleration is disabled");
return 0;
}

/* First, check for qemu=1 */
property_get("ro.kernel.qemu", prop, "0"); //读取ro.kernel.qemu,判断是否运行在 qemu 中
if (atoi(prop) != 1)
return -1;

/* We are in the emulator, get GPU status value */
property_get("qemu.gles", prop, "0"); // 如果 Android 系统运行在 qemu 中,就返回 qemu.gles 的值,根据这个值就可以确定加载的是那种 OpenGL ES 库了
return atoi(prop);
}
  1. 总结一下上面代码的功能就是,首先判断 Android 是否在 qemu 虚拟机中运行,如果不是,那么就直接去加载厂商存放库的路径中去加载 OpenGL ES 实现库(不管是硬件加速实现的,还是软件模拟实现的);如果是在 qemu 中运行,那么就要根据返回的 emulationStatus 值 来确定是加软件模拟实现的 OpenGL ES API 库 libGLES_android.so,还是加载 libGLES_emulation.so库将 OpenGL ES 指令重定向到 Host 系统中去执行。
OpenGL ES/EGL 库加载和解析过程

正如Android 系统图形栈: OpenGL ES 和 EGL 介绍这篇文章中分析的那样,在进行 OpenGL 编程时,最先开始需要获取 Display,这将调用 eglgGetDisplay() 函数被调用。在 eglGetDisplay() 里则会调用 egl_init_drivers() 初始化驱动:装载各个库进行解析,将 OpenGL ES/EGL API 函数接口和具体的实现绑定在一起,并将结果保存在 egl_connection_t 类型的全局变量 gEGLImpl 的结构体的成员变量中。

下面以 SurfaceFlinger 进程为例进行分析,整个 OpenGL ES/EGL 库的加载和解析流程如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
st=>start: Start
op1=>operation: SurfaceFlinger::init()
sub=>subroutine: Your Subroutine
op2=>operation: eglGetDisplay(EGL_DEFAULT_DISPLAY)
op3=>operation: egl_init_drivers()
op4=>operation: egl_init_drivers_locked();
op5=>operation: loader.open()
op6=>operation: load_driver()
op7=>operation: load_system_driver()
op8=>operation: Loader::init_api()
e=>end

st->op1->op2->op3->op4->op5->op6->op7->op8->e

gEGLImpl 全局变量 struct egl_connection_t 类型的 gEGLImpl 全局变量是一个非常重要变量,它里面的成员指向了打开的 OpenGL ES/EGL Wrapper 库和 OpenGL ES/EGL 实现库: /frameworks/native/opengl/libs/EGL/egl.cpp

1
2
3
egl_connection_t gEGLImpl;
gl_hooks_t gHooks[2];
gl_hooks_t gHooksNoContext;

其中 egl_connection_t 的定义: /frameworks/native/opengl/libs/EGL/egldefs.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct egl_connection_t {
enum {
GLESv1_INDEX = 0,
GLESv2_INDEX = 1
};

inline egl_connection_t() : dso(0) { }
void * dso; //指向打开的共享链接库的句柄
gl_hooks_t * hooks[2]; //指向打开的 OpenGL ES API 对象
EGLint major; // 主版本好
EGLint minor;
egl_t egl; //dui x

void* libEgl;
void* libGles1;
void* libGles2;
};

下面就对其中的主要成员进行一个说明:

  • hooks:这是一个 gl_hook_t* 类型的指针数组,它最终将 OpenGL ES API 和实现库钩在一起。
  • egl:这是一个 egl_t 类型的成员变量,它最终将 EGL API 和 EGL 实现库了钩在一起。 那么 gl_hook_t 和 egl_t 是什么呢?

gl_hook_t 和 egl_t 的定义如下所示: /frameworks/native/opengl/libs/hooks.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define GL_ENTRY(_r, _api, ...) _r (*_api)(__VA_ARGS__);
#define EGL_ENTRY(_r, _api, ...) _r (*_api)(__VA_ARGS__);

struct egl_t {
#include "EGL/egl_entries.in"
};

struct gl_hooks_t {
struct gl_t {
#include "entries.in"
} gl;
struct gl_ext_t {
// __eglMustCastToProperFunctionPointerType 是一个 typedef 的函数指针类型,它的返回值是void,参数也是void
__eglMustCastToProperFunctionPointerType extensions[MAX_NUMBER_OF_GL_EXTENSIONS];
} ext;
};

从上面的定义中我们发现,egl_t 和 gl_hooks_t 这两个结构体中主要就是一个 include 语句,那么它们包含的是什么呢?

#include “EGL/egl_entries.in 包含的文件路径如下所示: /frameworks/native/opengl/libs/EGL/egl_entries.in

1
2
3
4
5
6
EGL_ENTRY(EGLDisplay, eglGetDisplay, NativeDisplayType)
EGL_ENTRY(EGLBoolean, eglInitialize, EGLDisplay, EGLint*, EGLint*)
EGL_ENTRY(EGLBoolean, eglTerminate, EGLDisplay)
EGL_ENTRY(EGLBoolean, eglGetConfigs, EGLDisplay, EGLConfig*, EGLint, EGLint*)
EGL_ENTRY(EGLBoolean, eglChooseConfig, EGLDisplay, const EGLint *, EGLConfig *, EGLint, EGLint *)
...

在这个文件中,我们可以看到所有的内容都是 EGL_ENTRY 宏定义的,根据 EGL_ENTRY 宏定义: /frameworks/native/opengl/libs/EGL/hooks.h

1
2
3
4
//下面的两个宏定义分别是 OpenGL 和 EGL API 函数的函数指针变量
//这些函数变量最后会和具体的 OpenGL 和 EGL API 的实现绑定在一起
#define GL_ENTRY(_r,_api,...) _r (*_api)(__VA_ARGS__);
#define EGL_ENTRY(_r,_api,...) _r(*_api)(__VA_ARGS__);

我们可以将下面的这个宏定义展开成如下的形式:

1
2
3
EGL_ENTRY(EGLDisplay, eglGetDisplay, NativeDisplayType)
//展开后的形式如下所示,它实际上就是 EGL API 函数声明
EGLDisplay eglGetDisplay(NativeDisplayType)

#include “entries.in” 包含的文件路径为: /frameworks/native/opengl/libs/entries.in

1
2
3
...
GL_ENTRY(void, glReadPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void * pixels)
...

和上面的 EGL_ENTRY 一样,GL_ENTRY 都是通过宏定义的形式来将 OpenGL ES 的 API 函数接口进行声明,例如,上面的宏定义声明可以展开成下面的形式:

1
2
3
GL_ENTRY(void, glReadPixels, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void * pixels)
// 展开后的形式如下所示,它实际上就是 OpenGL ES API 函数声明
void glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void * pixels)

最后,通过 entries.in 和 egl_entries.in 这两个文件,我们就可以得到 OpenGL ES 和 EGL 中的所有 API 函数接口的说明。

SurfaceFlinger 初始化成员变量 mEGLDisplay 在 SurfaceFlinger 类中有一个 EGLDisplay 类型的成员变量 mEGLDisplay,它是 EGL 中用来构建 OpenGL ES 渲染环境所需的参数。

SurfaceFlinger 中调用 eglGetDisplay() 初始化 mEGLDisplay 的代码如下: /frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

1
2
3
4
5
6
7
8
9
10
11
12
void SurfaceFlinger::init() {
ALOGI( "SurfaceFlinger's main thread ready to run. "
"Initializing graphics H/W...");

status_t err;
Mutex::Autolock _l(mStateLock);

// initialize EGL for the default display
// 调用 eglGetDisplay 函数获取默认的显示设备
mEGLDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(mEGLDisplay, NULL, NULL);
...

紧接着在 eglGetDisplay() 中调用 egl_init_drivers() /frameworks/native/opengl/libs/EGL/eglApi.cpp

1
2
3
4
5
6
7
8
9
10
11
EGLDisplay eglGetDisplay(EGLNativeDisplayType display)
{
...
// 调用egl_init_drivers() 加载 OpenGL ES 库和 EGL 库
if (egl_init_drivers() == EGL_FALSE) {
return setError(EGL_BAD_PARAMETER, EGL_NO_DISPLAY);
}

EGLDisplay dpy = egl_display_t::getFromNativeDisplay(display);
return dpy;
}

最后,egl_init_drivers() 函数中使用了 pthread_mutex_lock 上锁保护,继续调用 egl_init_drivers_locked() 函数 /frameworks/native/opengl/libs/EGL/egl.cpp

1
2
3
4
5
6
7
8
EGLBoolean egl_init_drivers() {
EGLBoolean res;
pthread_mutex_lock(&sInitDriverMutex);
// 使用了 pthread_mutex_lock 上锁保护,继续调用 egl_init_drivers_locked
res = egl_init_drivers_locked();
pthread_mutex_unlock(&sInitDriverMutex);
return res;
}

下面就是对 egl_init_drivers_locked() 函数进行一个分析:

egl_init_drivers_locked() 函数代码如下所示: /frameworks/native/opengl/libs/EGL/egl.cpp

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
//在该文件起始位置定义的全局变量
egl_connection_t gEGLImpl; // 描述EGL实现内容的结构体对象
gl_hooks_t gHooks[2]; // gl_hooks_t 是包含 OpenGL ES API 函数声明对应的函数指针结构体
gl_hooks_t gHooksNoContext;
pthread_key_t gGLWrapperKey = -1;

static EGLBoolean egl_init_drivers_locked() {
if (sEarlyInitState) {
// initialized by static ctor. should be set here.
return EGL_FALSE;
}

// 得到 Loader 对象单例
// get our driver loader
Loader& loader(Loader::getInstance());

// gEGLImple 是一个全局变量,数据类型为 egl_connection_t 结构体类型
// dynamically load our EGL implementation
egl_connection_t* cnx = &gEGLImpl;

// cnx->dso 本质上是一个 (void *)类型的指针,它指向的对象是 EGL 共享库打开之后的句柄
if (cnx->dso == 0) {
// >= 将cnx中的 hooks 数组中指向OpenGL ES API 函数指针结构体指的数组成员,用 gHooks 中的成员的地址去初始化
//也就是说 gEGLImpl 中 hook 数组指向 gHooks 数组,最终指向同一个 OpenGL ES API 函数指针的实现
cnx->hooks[egl_connection_t::GLESv1_INDEX] =
&gHooks[egl_connection_t::GLESv1_INDEX];
cnx->hooks[egl_connection_t::GLESv2_INDEX] =
&gHooks[egl_connection_t::GLESv2_INDEX];

// >= 最后通过loader对象的open函数开始加载 OpenGL ES 和 EGL wrapper 库
cnx->dso = loader.open(cnx);
}

return cnx->dso ? EGL_TRUE : EGL_FALSE;
}

在这个函数中,有一个非常关键的 egl_connection_t 指针指向一个全局变量 gEGLImpl,当第一次初始化加载 OpenGL ES 实现库和 EGL 实现库时,还需要将 gEGLImpl 中的 hooks 数组中的两个指针指向一个全局的 gl_hooks_t 数组 gHooks(这就是两个指针钩子,最终初始化完成后将分别勾住 OpenGL ES 1.0 和 OpenGL ES 2.0 的实现库),接着调用 Loader 类的实例的 open() 函数完成从 OpenGL ES 实现库中完成符号解析工作。

通过 Loader 类加载和解析 OpenGL ES 库和 EGL 库 Loader::open() 函数的代码如下所示: /frameworks/native/opengl/libs/EGL/Loader.cpp

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
// >= Loader 类对象构造完成后,就在 /EGL/egl.cpp 文件中的 egl_init_drivers_locked() 中被调用
void* Loader::open(egl_connection_t* cnx)
{
ATRACE_CALL();

void* dso;
driver_t* hnd = 0;

setEmulatorGlesValue();

dso = load_driver("GLES", cnx, EGL | GLESv1_CM | GLESv2);
if (dso) {
hnd = new driver_t(dso);
} else {
// Always load EGL first
dso = load_driver("EGL", cnx, EGL);
if (dso) {
hnd = new driver_t(dso);
hnd->set( load_driver("GLESv1_CM", cnx, GLESv1_CM), GLESv1_CM );
hnd->set( load_driver("GLESv2", cnx, GLESv2), GLESv2 );
}
}

LOG_ALWAYS_FATAL_IF(!hnd, "couldn't find an OpenGL ES implementation");

cnx->libEgl = load_wrapper(EGL_WRAPPER_DIR "/libEGL.so");
cnx->libGles2 = load_wrapper(EGL_WRAPPER_DIR "/libGLESv2.so");
cnx->libGles1 = load_wrapper(EGL_WRAPPER_DIR "/libGLESv1_CM.so");

LOG_ALWAYS_FATAL_IF(!cnx->libEgl,
"couldn't load system EGL wrapper libraries");

LOG_ALWAYS_FATAL_IF(!cnx->libGles2 || !cnx->libGles1,
"couldn't load system OpenGL ES wrapper libraries");

return (void*)hnd;
}

open() 函数主要负责 OpenGL ES 库加载前的准备工作,具体的加载细节,则是通过调用 load_driver() 去完成的。

Loader::load_driver() 函数代码如下所示: /frameworks/native/opengl/libs/EGL/Loader.cpp

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
void *Loader::load_driver(const char* kind,
egl_connection_t* cnx, uint32_t mask)
{
ATRACE_CALL();

void* dso = nullptr;
if (mGetDriverNamespace) {
android_namespace_t* ns = mGetDriverNamespace();
if (ns) {
dso = load_updated_driver(kind, ns); //加载 OpenGL ES 实现库,放回打开的共享链接库的句柄
}
}
if (!dso) {
dso = load_system_driver(kind);
if (!dso)
return NULL;
}

// 解析 EGL 库,并将wrapper 库 libEGL.so 中的函数 API 指针和具体的实现绑定在一起
if (mask & EGL) {
getProcAddress = (getProcAddressType)dlsym(dso, "eglGetProcAddress");

ALOGE_IF(!getProcAddress,
"can't find eglGetProcAddress() in EGL driver library");

egl_t* egl = &cnx->egl; //将 egl 指针指向描述当前系统支持 OpenGL ES和 EGL 全局变量的 gEGLImpl
__eglMustCastToProperFunctionPointerType* curr =
(__eglMustCastToProperFunctionPointerType*)egl;
char const * const * api = egl_names; //egl_names 是定义在 egl.cpp 文件中的一个数组,数组中的元素是 EGL API 函数指针
while (*api) {
char const * name = *api;
__eglMustCastToProperFunctionPointerType f =
(__eglMustCastToProperFunctionPointerType)dlsym(dso, name);
if (f == NULL) {
// couldn't find the entry-point, use eglGetProcAddress()
f = getProcAddress(name);
if (f == NULL) {
f = (__eglMustCastToProperFunctionPointerType)0;
}
}
*curr++ = f; //这一步就是最关键的将共享链接库中的 EGL API 的实现和上层调用的 API 函数指针绑定在一起
api++; //指向下一个需要绑定的 api 函数
}
}

// 解析 OpenGL ES 库中的 OpenGL ES 1.x API 符号
if (mask & GLESv1_CM) {
// 调用 init_api 实现 OpenGL API 和对应实现函数的绑定
init_api(dso, gl_names, // gl_names 是定义在 egl.cpp 文件中的一个数组,数组中的元素是 OpenGL ES API 函数指针
(__eglMustCastToProperFunctionPointerType*)
&cnx->hooks[egl_connection_t::GLESv1_INDEX]->gl, //gl成员变量是一个结构体变量,结构体中的是 OpenGL ES API 函数指针
getProcAddress);
}

// 解析 OpenGL ES 库中的 OpenGL ES 2.0 API 符号
if (mask & GLESv2) {
init_api(dso, gl_names,
(__eglMustCastToProperFunctionPointerType*)
&cnx->hooks[egl_connection_t::GLESv2_INDEX]->gl,
getProcAddress);
}

return dso;
}

Loader::load_driver() 它主要实现了两个功能:

通过 load_system_driver() 函数查找 OpenGL ES/EGL 实现库,并在指定的存放路径中找到共享链接库文件并打开它。 调用 init_api()解析打开的 OpenGL ES/EGL 共享链接库,将 OpenGL ES/EGL API 函数指针和共享链接库中实现的对应的函数符号绑定在一起,这样调用 OpenGL ES/EGL API 就会调用到具体实现的OpenGL ES/EGL 共享链接库中对应函数。 具体 load_system_driver() 函数和 init_api() 函数的实现就不一一展开了,大家可以到 /frameworks/native/opengl/libs/EGL/Loader.cpp 文件中查看,应该写得非常直接清楚了。

整个加载和解析的目的就是将 OpenGL ES/EGL API 和具体实现函数绑定在一起,这样当程序调用 OpenGL ES/EGL API 时,就会调用到实际的实现函数了。然后,在硬件平台上的 Android 系统加载的是硬件厂商提供的 OpenGL ES/EGL 实现库,而在 qemu 模拟器中运行的 Android 系统中加载的则是软件模拟实现的 OpenGL ES/EGL 库或者是将 OpenGL ES 重定向到主机系统中进行硬件加速的库。

Android 系统图形栈(二):OpenGL ES 库和 EGL 库加载过程 How Android finds OpenGL libraries, and the death of egl.cfg

eglSwapBuffers接口实现说明

一般性嵌入式平台 iamge

利用双缓冲进行Swap的时候,Display和Surface进行实际意义上的地址交换,来实现eglSwapBuffers的标准, 如上图的右侧所示。上图的左侧表示,单缓冲Framebuffer的形式,Surface永远都在后端, 显示的永远是Display,在GPU出现后已不使用。

Android平台: 为了实现eglSwapBuffers, eglSurface其实代表了一个从NativeWindow 申请到的一个Buffer(Dequeue操作)。当调用eglSwapBuffers时,对于一般应用窗口而言,NativeWindow将该Surface的Buffer 提交回去给SurfaceFlinger(Queue操作),然后又重新从NativeWindow中重新Dequeue出来一个新的Buffer给eglSurface。而eglDisplay并不代表实际的意义。我们只是从接口上感觉是,surface和display进行了交换。

日常的

发表于 2019-01-30

Parameters were incorrect. We wanted {“required”:[“desiredCapabilities”], pip uninstall selenium pip install selenium==3.0.1

fbo与pbuffer区别

像zoom这样把多个流放到一个upd里究竟有什么好处? 节省端口 这个理由似乎不是很充分,付出的代价有点大,而且单机其实支持不了多少路,端口足够用了

付出什么代价了 单机支持一两千路足够视频会议用了\

猜测而已: 1. 避免NAT穿越时候多个Port的维护 2. 在UDP层面复用的话,保证UDP的流量足够,同时也避免了维护多个Socket,减小资源开销 当集群来应对大量客户的时候,这样做的话,机器之间会有很多的互相拉流的操作,

ICE打洞过程会快了,但是也得要支持动态更新啊,因为肯定会存在半路上有人进,有人出,SDP不是要变化吗?

没记错的话,RTP、RTCP曾经打算复用一个Port,避免一次使用两个 :)

和TS的统计复用很类似 所以我猜测Zoom并不是全部随意把流放到一个Port里面去,上层逻辑是做了控制的 TS的统计复用是为了适应CBR

我抓包看到ZOOM是哪个SFU入,就从那个SFU出流,但流被复接连

是不是好做集群,好做负载均衡,好做就近接入,还是好和SD-WAN结合,我想ZOOM体验那么好,一定是多方面优化的结果,究竟在哪些地方不一样达成的这种效果

Android性能

发表于 2019-01-30 | 分类于 autotest

性能标准

安卓绿色联盟应用性能标准主要基于主观体验、资源消耗和应用质量三个方面进行制定。

  1. 主观体验:主观体验主要是对应用启动时间和界面帧率制定标准,要求应用在视觉上足够流畅。其中应用启动时间又分为冷启动时间和热启动时间.安卓绿色联盟性能标准要求,应用冷启动时间需小于1000ms,热启动时间需小于500ms;普通应用帧率应大于55fps,游戏视频帧率应大于25fps。
  2. 资源消耗:资源消耗主要是要求应用不能占用过高的内存和CPU。安卓绿色联盟性能标准要求应用前台内存占用应小于500M,后台内存占用应小于400M;在CPU占用方面要求应用在后台灭屏5分钟后,CPU占用不超过2%。
  3. 应用质量:应用质量主要是对应用过度绘制和内存泄露的情况作出要求,规定应用不能存在过度绘制和内存泄露。安卓绿色联盟性能标准要求应用界面任意像素点不存在4x的绘制的情况, 3x绘制的区域不能超过界面面积的1/3,在Strict Mode中不允许有红框闪烁。

性能调试方法

在了解性能调试方法之前,我们可以先通过下图了解安卓应用性能与系统之间的关系。一个应用从应用绘制到最终显示在LCD上经历了一个漫长的路径,在任何一个阶段出现延时都会导致界面上的卡顿。 at-android-performance-201935151844

1、Strict Mode

Strict Mode意思为严格模式,是Android提供的一种运行时检测机制,一般用来检测在主线程发生的耗时动作,比如IO读写、数据库操作、复杂算法等。在手机设置开发者选项把Strict Mode打开,就可以在界面上把它打开了。

严格模式主要有2个策略,一个是线程策略,即ThreadPolicy,主要检测主线程中的一些耗时操作;另一个是虚拟机策略,即VmPolicy,主要检测一些对象的泄漏。

两大策略检测的内容和开启方法可以依据下图中的说明进行使用。 ThreadPolicy:

  • 自定义的耗时调用使用detectCustomSlowCalls()开启
  • 磁盘读取操作使用detectDiskReads()开启
  • 磁盘写入操作使用detectDiskWrites()开启
  • 网络操作使用detectNetwork()开启

VmPolicy:

  • Activity泄露使用detectActivityLeaks()开启
  • 未关闭的Closable对象泄露使用detectLeakedClosableObjects()开启
  • 泄露的Sqlite对象,使用detectLeakedSqlLiteObjects()开启
  • 检测实例数量,使用setClassInstanceLimit()开启 严格模式有三种惩罚模式:应用崩溃、弹窗警告和打印日志。在性能测试中,我们可以通过APPLogcat抓取Strict Mode的日志,同时利用代码启用Strict Mode,配合我们所需要的策略和惩罚,就可以及时定位应用的违规细节,并及时进行性能优化。

当我们碰到违规的行为时,该如何进行治理呢?建议将文件操作放到工作线程去完成,如果在主线程上提及操作,建议使用Apply和Commit去完成。如果存在对象未关闭的情况,可以通过对应的StackTrace进行关闭。

2、OverDraw DeBugger

Overdraw是指屏幕上的某个像素在同一帧的时间内被绘制了多次,这个工具使用色块来代表不同数量的过度绘制,我们可以使用这个工具来定位由过度绘制引起的用户界面卡顿问题。

在开发者选项中选择开启 Debug GPU Overdraw选项,即可在安卓设备上将过度绘制问题可视化。 at-android-performance-201935152838 左图为正常模式下显示的视图,右图为开启GPU Overdraw后显示的视图

3、Profile GPU Rendering

ProfileGPU Rendering 工具以滚动直方图的形式直观地显示渲染界面窗口帧所花费的相对时间(以每帧 16 毫秒的速度作为对比基准)。这个工具同样也是在安卓设备的开发者选项中开启。每个管线的高度表示时间,管线中各个彩色区段代表不同含义。

下表介绍了使用运行Android 6.0及更高版本的设备时分析器中不同竖条区段的含义。 at-android-performance-201935152931

4、Android Profiler

Android Profiler是一个Android Studio集成的应用性能分析器,可以实时查看CPU、Memory和Network的动态情况。以下重点介绍CPU Profiler:

CPU Profiler 可帮助您实时检查应用的 CPU 使用率和线程 Activity,并记录函数跟踪,方便大家优化和调试应用代码。

当打开 CPU Profiler 时,它将显示应用的 CPU 使用率和线程 Activity。 CPU Profiler可以选择不同的标签,并对应用线程进行跟踪。如:

  • Flame Chart标签会提供一个倒置的调用图表,汇总相同的调用堆栈,收集调用顺序完全一致的函数,并在火焰图中用一个较长的横条表示它们。
  • Top Down标签能够提供每个函数调用上所花费的CPU时间。Self表示函数调用在执行自己的代码上所花的时间;Children表示函数调用子方法所花费的时间;Total表示Self和Children时间的总和。

5、Systrace

Systrace是我们分析性能最常用的工具之一,它可以分析整机系统性能及动态场景的性能问题。

Systrace 允许您在系统级别收集和检查设备上运行的所有进程的计时信息。它将来自Android内核的数据(例如CPU调度程序,磁盘活动和应用程序线程)组合起来,以生成HTML报告。

at-android-performance-201935153057 上图左部是Systrace的界面,我们可以通过右边的代码抓取Systrace,观察进程的执行时间。在输入抓取命令时,时间参数一般选择5到10秒,因为时间过短可能会抓不到想要的数据,时间过长则可能抓取失败。

一般我们通过Chrome浏览器查看生成的trace文件,也可以通过DDMS图形界面去抓取Systrace。

at-android-performance-201935153130 拿到一个Systrace时主要考察哪些因素?首先看一下CPU的频率,找到对应的进程或者线程,查看相关信息;同时还要观察GPU的频率、Surface Flinger还有绘图的Buffer状态等。

当应用发生卡顿时,我们可以通过Systrace进行分析。在生成的trace文件中,找到主线程UI,每一帧都会标记一个带有F的圆形。当原型为绿色时,代表页面流畅,而黄色和红色则存在超时,我们可以点击去查看具体存在什么问题。

启动时间

应用启动时间是应用性能最重要的指标之一,分冷启动和热启动两种情况:

  • 冷启动:当APP启动时,后台没有该app的进程,这时系统会重新创建一个新的进程分配给该app,这个启动方式就叫做冷启动(后台不存在该APP进程)
  • 热启动:当APP已经被打开,但是被按下返回键,Home键等按钮时回到桌面或者是其他程序的时候,再重新打开该APP时,这种方式叫做热启动(后台已经存在该APP进程)

测试方法: 将手机root之后,使用adb工具连接手机.使用adb shell am start -S -W 命令获取应用冷启动时间,adb shell am Start -W命令获取应用冷启动时间. adb shell am start -W -n packagename/packageName.MainActivity aapt dump badging <apk路径>:搜package 的 launchable-activity

at-android-performance-20193512931 执行成功后将返回三个测量到的时间:

  1. ThisTime:一般和TotalTime时间一样,除非在应用启动时开了一个透明的Activity预先处理一些事再显示出主Activity,这样将比TotalTime小。
  2. TotalTime:应用的启动时间,包括创建进程+Application初始化+Activity初始化到界面显示。
  3. WaitTime:一般比TotalTime大点,包括系统影响的耗时。

关于ThisTime/TotalTime/WaitTime的区别,下面是其解释:“adb shell am start -W ”的实现在『frameworks\base\cmds\am\src\com\android\commands\am\Am.java』文件中。其实就是跨Binder调用ActivityManagerService.startActivityAndWait() 接口(后面将ActivityManagerService简称为AMS),这个接口返回的结果包含上面打印的ThisTime、TotalTime时间.

  • startTime记录的刚准备调用startActivityAndWait()的时间点
  • endTime记录的是startActivityAndWait()函数调用返回的时间点
  • WaitTime = startActivityAndWait()调用耗时。

ThisTime、TotalTime 的计算在 frameworks\base\services\core\java\com\android\server\am\ActivityRecord.java 文件的 reportLaunchTimeLocked() 函数中。

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
private void reportLaunchTimeLocked(final long curTime) {
final ActivityStack stack = task.stack;
final long thisTime = curTime - displayStartTime;
final long totalTime = stack.mLaunchStartTime != 0
? (curTime - stack.mLaunchStartTime) : thisTime;
if (ActivityManagerService.SHOW_ACTIVITY_START_TIME) {
Trace.asyncTraceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, "launching", 0);
EventLog.writeEvent(EventLogTags.AM_ACTIVITY_LAUNCH_TIME,
userId, System.identityHashCode(this), shortComponentName,
thisTime, totalTime);
StringBuilder sb = service.mStringBuilder;
sb.setLength(0);
sb.append("Displayed ");
sb.append(shortComponentName);
sb.append(": ");
TimeUtils.formatDuration(thisTime, sb);
if (thisTime != totalTime) {
sb.append(" (total ");
TimeUtils.formatDuration(totalTime, sb);
sb.append(")");
}
Log.i(ActivityManagerService.TAG, sb.toString());
}
mStackSupervisor.reportActivityLaunchedLocked(false, this, thisTime, totalTime);
if (totalTime > 0) {
//service.mUsageStatsService.noteLaunchTime(realActivity, (int)totalTime);
}
displayStartTime = 0;
stack.mLaunchStartTime = 0;
}
  • curTime表示该函数调用的时间点.
  • displayStartTime表示一连串启动Activity中的最后一个Activity的启动时间点.
  • mLaunchStartTime表示一连串启动Activity中第一个Activity的启动时间点.

正常情况下点击桌面图标只启动一个有界面的 Activity,此时 displayStartTime 与mLaunchStartTime 便指向同一时间点,此时 ThisTime=TotalTime。另一种情况是点击桌面图标应用会先启动一个无界面的 Activity 做逻辑处理,接着又启动一个有界面的Activity,在这种启动一连串 Activity 的情况下(知乎的启动就是属于这种情况),displayStartTime 便指向最后一个 Activity 的开始启动时间点,mLaunchStartTime 指向第一个无界面Activity的开始启动时间点,此时 ThisTime!=TotalTime。这两种情况如下图: at-android-performance-20193514439

在上面的图中,我用①②③分别标注了三个时间段,在这三个时间段内分别干了什么事呢?

  • 在第①个时间段内,AMS 创建 ActivityRecord 记录块和选择合理的 Task、将当前Resume 的 Activity 进行 pause
  • 在第②个时间段内,启动进程、调用无界面 Activity 的 onCreate() 等、 pause/finish 无界面的 Activity
  • 在第③个时间段内,调用有界面 Activity 的 onCreate、onResume

看到这里应该清楚 ThisTime、TotalTime、WaitTime 三个时间的关系了吧。WaitTime 就是总的耗时,包括前一个应用 Activity pause 的时间和新应用启动的时间;ThisTime 表示一连串启动 Activity 的最后一个 Activity 的启动耗时;TotalTime 表示新应用启动的耗时,包括新进程的启动和 Activity 的启动,但不包括前一个应用 Activity pause 的耗时。也就是说,开发者一般只要关心 TotalTime 即可,这个时间才是自己应用真正启动的耗时。

Event log中 TAG=am_activity_launch_time 中的两个值分表表示 ThisTime、TotalTime,跟通过 “adb shell am start -W ” 得到的值是一致的。

最后再说下系统根据什么来判断应用启动结束。我们知道应用启动包括进程启动、走 Activity生命周期 onCreate/onResume 等。在第一次 onResume 时添加窗口到WMS中,然后measure/layout/draw,窗口绘制完成后通知 WMS,WMS 在合适的时机控制界面开始显示(夹杂了界面切换动画逻辑)。记住是窗口界面显示出来后,WMS 才调用reportLaunchTimeLocked() 通知 AMS Activity 启动完成。

最后总结一下,如果只关心某个应用自身启动耗时,参考TotalTime;如果关心系统启动应用耗时,参考WaitTime;如果关心应用有界面Activity启动耗时,参考ThisTime。

指标

一般情况下,应用启动时间在1秒以内,用户反馈系统响应很快;1-3秒内完成启动,用户认为反应速度尚可;3-5秒内完成启动,用户会认为系统反应很慢,但是还可以接受;若启动时间超过5秒,则大部分用户会认为系统反应糟糕,甚至卸载应用。

安卓绿色联盟应用体验标准规定,应用冷启动时间应该不超过1000毫秒,热启动时间应该不超过500毫秒,若不满足,则无法获得绿色应用认证。

从上面的测评结果可以看出,参与测评的8款应用冷启动和热启动的时间都是非常快,也都符合安卓绿色联盟对绿色应用启动时间标准。其中QQ音乐和芒果TV的冷启动时间最为优秀,分别只有283毫秒和596毫秒。

Android系统上,APP无进程状态,启动流程见下: Application的构造器方法 ——>attachBaseContext() ——>onCreate() ——>Activity的构造方法 ——>onCreate() ——>配置主题中背景等属性 ——>onStart() ——>onResume() ——>测量布局绘制显示在界面上。

当点击APP的启动图标时,安卓系统会从Zygote进程中fork创建出一个新的进程分配给该应用,之后会依次创建和初始化Application类、创建MainActivity类、加载主题样式Theme中的windowBackground等属性设置给MainActivity以及配置Activity层级上的一些属性、再inflate布局、当onCreate/onStart/onResume方法都走完了后最后才进行contentView的measure/layout/draw显示在界面上,所以直到这里,应用的第一次启动才算完成,这时候我们看到的界面也就是所说的第一帧。

启动时间:

  1. 冷启动时间:当用户点击目标app图标的 timepoint到显示界面第一帧的时间段(当用户点击你的app那一刻到系统调用Activity.onCreate()之间的时间段),在这个时间段内,WindowManager会先加载app主题样式中的windowBackground做为app的预览元素,然后再真正去加载activity的layout布局。API19 之后,系统会出打印日志输出启动的时间:冷启动时间 = 应用启动(创建进程) —> 完成视图的第一次绘制(Activity内容对用户可见);
  2. 热启动时间:用户把目标app切换至后台后,点击app图标的timepoint到显示界面第一帧的时间段

优化建议

  1. 减少Application中过多的三方SDK初始化操作,在真正使用到功能时在进行初始化操作。
  2. 减少首界面布局文件的层级和嵌套,来减少界面的量测和绘制的时间。
  3. 减少首界面oncreate()方法中的复杂逻辑和工作量,从而降低启动时间。
  4. 不要以静态变量的方式在Application中保存数据

流畅度

人为感受的性能不好属于下面两种:

  1. 响应时间,界面跳转后响应时间
  2. 流畅度,界面操作时或动画展示的效果

流畅度的衡量指标:

  1. 帧率fps
  2. 丢帧SF(Skipped frame)
  3. 流畅度SM(SMoothness)

帧率计算:

  1. adb shell dumpsys gfxinfo <PACKAGE_NAME>
  2. adb shell dumpsys SurfaceFlinger --latency <window_activity>

https://blog.csdn.net/itfootball/article/details/43084527 https://developer.android.com/training/testing/performance?hl=zh-cn https://testerhome.com/topics/4441 https://testerhome.com/topics/4643

页面响应时间TTLB(Time To Last Byte)

页面响应时间是指从用户发出请求到客户端收到最后一个字节所耗费的时间.

性能案例分析

案例1:界面滑动卡顿

at-android-performance-201935154448

从图中可以看到,这是一个手动滑动事件,当deliverInput事件发生后,第一帧就发生了卡顿。从systrace看UI thread执行draw的时间相当耗时导致丢帧卡顿,而且大部分时间都在做decodeBitmap,共耗时99.045ms。这时,我们打开applog发现,有StrictMode相关的错误提示,从中可以定位到耗时函数。

at-android-performance-20193515453

从上图我们看出有一个网络访问违规,大概可以推测应用在从网络上下载了一个数据流,数据流里可能包含了一些图形,通过decodeBitmap把它解析出来展示在UI界面中。正产情况下,我们应该把网络访问放在工作线程里面去处理,将数据下载完了之后再放到主线程中去展示,避免这种问题的发生。

案例2:Strict Mode错误提示

at-android-performance-201935154542 从上图Strict Mode的日志可以看出:StrictMode policy violation耗时2秒左右。通过最下行蓝色的log,可以知道应用是在某一个目录里面寻找一个文件,判断文件是否存在。

面对这种问题,我们应该把IO操作放到工作线程。正常情况下IO的发生非常快,但是在系统繁忙时,IO放在主线程会产生较大的问题,因为它要等别的程序读写完成之后,才会下发,产生超时。

案例3:GPU调用不当导致的卡顿问题

at-android-performance-201935154612 这是一个GPU的例子,上图主要问题是GPU使用了太长时间处理应用传过来的buffer,例子中Surfaceflinger 使用GPU 做了图像叠加,说明图层比较多。使用GPU做叠加主要会产生功耗和唤醒耗时的问题。大家在做界面设计的时候,尽量不要使用GPU进行叠加。在上面的例子中,GPU叠加之后,导致了大概15ms左右的延时,因为GPU操作完成以后还需要交给Surfaceflinger把图像显示到屏幕上。

案例4:CPU调用不当导致的界面滑动卡顿问题

at-android-performance-201935154711 可以通过上图的红色条块了解messageloop RunTask信息,红色条块上的蓝色bar,表示线程在CPU上的状态。蓝色表示这个线程处于等待CPU调度的状态,可见等待超过8ms的时间,是正常调度周期好几倍。导致这种情况发生的原因有两个:CPU负载过大或CPU调度出现了问题。在上图中我们可以看出,CPU0和CPU1使用率100%,但是CPU2和CPU3是offline的状态,说明系统出现问题,导致CPU2和CPU3未能唤醒,帮助完成系统任务。

性能优化建议

1、避免内存泄露

在应用开发过程中,首先要避免内存泄露的问题,内存泄露是一种比较严重的性能问题,在安卓绿色联盟应用性能标准中也要求应用不允许发生内存泄露。 常见内存泄露:

  1. 手动关闭try/catch/finally中使用网络文件等流文件的对象,关注对象:HTTP,File, ContentProvider,Bitmap,Uri,Socket
  2. 注意关闭onDestroy()或者onPause()中未及时关闭对象,防止如下内存泄露:线程泄露,Handler泄露,广播泄露,第三方SDK/开源框架泄露,各种callBack/Listener的泄露

常见内存泄露坚持工具:

  • Memory Monitor
  • Allocation Tracker
  • Heap Viewer
  • LeakCanary

2、避免不良设计或程序算法导致CPU占有率持续偏高

  • 主要业务处理分散到不同线程,便于后续利用多核处理器的并行处理能力,避免一核累死,7核围观;
  • 使用top命令观察应用线程的CPU占有率,找出高负载的进程进行分析,并针对优化。

3、避免OnXXX 回调函数中进行耗时操作,避免主线程卡顿

Android系统中正常情况下所有onXXX类函数均运行在主线程中。 两帧中间有一个因为接收广播处理导致的158ms的卡顿。在这些函数中,我们应该避免网络通信操作、文件读写操作、数据库数据改动的操作、图形处理、文本分析等操作,将这些工作尽可能的移到工作线程中去,从而避免主线程卡顿。

4、合理使用系统资源

合理使用系统资源主要指的是软资源。下图是对广播资源调用的一些建议。

  • 避免同一广播在多个不同实例中重复注册
  • 对象释放时,必须保证注销广播,避免广播注册泄露
  • 尽量不要过度依赖广播机制进行通信,只注册必要的广播
  • 尽量不要注册使用频繁放生的系统广播
  • 不高频调用系统服务接口,避免引起系统互锁造成阻塞

性能治理

主线程卡顿

主线程卡顿是因为主线程的消息超过阈值,从而导致页面丢帧。手淘通过接管主线程消息的分发机制,获取消息的分发耗时和消息类型,从而定位触发主线程卡顿的具体业务并进行针对性治理。

另外手淘在使用系统的SharedPreferences时,发现页面跳转导致界面ANR的情况。通过阅读系统源码,发现它在做Receiver或者Service时会强制把所有SharedPreferences apply的内容写入文档,导致ANR。针对这个问题,手淘重写SharedPreferences提升性能,减少了这类ANR问题的发生。

at-android-performance-201935222118

内存泄漏

手淘团队投入了大量时间对内存泄露进行治理。一方面通过接管系统底层组件的生命周期,当组件的生命周期销毁时,对它进行一个WeakReference的引用,然后根据GC事件触发情况来判定该对象是否泄漏。另一方面,在Native层通过Hook操作系统底层malloc和free方法,计算每一个so处理内存的情况,根据malloc与free差值大小与白名单进行对比判断是否存在内存泄露。

内存使用不当

何为内存使用不当?举个例子,当开发一个大小为100×100 view,实际却使用了200200甚至更大的bitmap。比如在系统drawable目录下放置一张图片,在高清的设备上展示时,它会根据系统自身的原理对它进行拉伸。这时原本只需要单位1的内存(100100),可能变成单位16的内存(400*400),内存的浪费率达到90%以上。针对这种情况,手淘做了一个内存使用不当排查插件。

视频也同样存这种问题,在低分辨率设备上播放高质量的视频不仅不会给用户带来更好的体验,还可能让设备出现卡顿。还有就是图片持有的问题,当页面已经沉入栈底,最好不要保留之前页面的图片。这样可以保证有足够的内存给前台页面使用,否则随着页面层级的深入,很容易出现OOM。

资源泄露

手淘主要通过接管系统底层的open和close两个Native方法函数对资源泄露进行治理。当open和close没有成对出现,并且该业务并不是伴随整个应用生命周期(伴随整个生命周期的文件有白名单),可以判断该操作可能存在资源泄漏。平台会将该异常行为告知对应的开发同学检查和治理。数据库的治理同样也采用了这套方案。

线程问题

线程问题比较复杂。在线程创建时可能会触发一些意想不到的问题,比如Out Of Memory error。Out Of Memory error可能是由线程创建失败导致的。因此,手淘对线程创建进行了接管。业务在创建时,对它的方法调用栈进行聚类,就可以知道每个业务创建的线程数量,以及线程创建是否合理。建议应用开发者在创建线程时一定规范命名,以便快速定位具体的业务方。

流量监控

手淘主要是通过接管Socket协议,分析协议头部获取请求和回流数据内容的大小信息进行流量监控治理。如发现异常,可以让开发同学定位解决。同时也可以监听后台流量行为,观察APP切到后台以后是否还有大量的网络请求。

at-android-performance-201935222346

设备评级

安卓设备百花齐放,手淘对不同的设备采取了计分的方法进行评级,根据设备分数采用不同的策略,展示相应的图片、视频和业务,给用户带来最佳的性能体验。这个设备评级方案可以给开发同学提供指导建议,更好的展现业务形态。

布局性能

在开发的过程中,常常要通过HierarchyViewer的方法检查布局结构是否合理。手淘写了一套算法,检查页面结构是否合理,页面层级是否过深、页面层级是否还有继续优化的空间。同时还实现了一套OverDraw的算法,给开发同学提示具体哪个层级可以优化,怎么样的降层级,怎么样解决OverDraw的问题。

用户体验优化

手淘很关注用户的体验,包括启动时间、每个页面的打开耗时等。通过监控启动时各个子任务的耗时,以及对这些信息快速的分析,判断每一次发版质量变化的具体原因。

4.X设备体验优化

随着产品功能越来越丰富和产品体积逐渐的壮大,4.X设备出现了multidex越来越慢的情况。基于这个现象,手淘把谷歌的support multidex包进行了重构改造。经过重构以后的support multidex方案,4.X设备的Dex加载随着Dex越来越多,它性能提升越好。

内存容灾

内存容灾手淘一直很关注。当用户使用当前页面时,如果内存不足,手淘期望后台页面可以快速释放内存资源,为前台页面服务。手淘开发了内存容灾插件,监听JVM的GC事件以及轮寻物理页表计算实际使用物理内存,通过这个计算给手淘的业务方发送对应的内存水位事件,如果是属于非常高危的内存事件,就可以让后台快速的释放缓存资源,从而为可视的页面提供更好的服务

稳定性治理

稳定性的治理主要是两部分,Java Crash和Native Crash at-android-performance-201935222554

Java Crash治理

手淘通过接管UncaughtExceptionHandler,拿到具体的Java Crash的信息及它的堆栈来进行Java Crash治理。Java Crash治理还有一个经常遇到的OOM的情况,一方面可能是虚拟机的内存不足导致的,另一方面可能是线程创建失败导致的,可以使用前面讲到的线程创建插件解决。

Native Crash治理

手淘通过捕获信号量的方式对Native Crash进行治理。当Native发生异常时,创建一个子进程,通过ptrace的方式去dump Native Crash上下文的线程信息。 基于前面讲到的性能治理方式,OOM的Native Crash可以通过malloc和free的接管,定位具体是哪个SO导致这个问题的发生并进行治理。

python之基础语法

发表于 2019-01-18 | 分类于 language
python清空列表
  1. 在非函数参数的list中使用del list_new[]或者list_new = []来清空列表
  2. 对于作为函数中的list,方法1行不通,因为函数执行后,list长度是不变的,但是可以在函数中释放一个参数list所占内存:del list_new[:]或者list_new[:] = []达到清空列表的目的,速度快,并且彻底
查看对象占用内存大小

os.getsizeof(1.0)

类型转换
1
2
3
4
5
import struct
import math
print("1000.003 is %s" %(int(struct.pack('>f', 1000.003).encode('hex'), 16)))
print("1000.003 is %s" %(struct.pack('>I', 1000)))
#print("1000.003 is %d" %(int(floor(1000.003))))

IEEE 754标准是IEEE二进制浮点数算术标准(IEEE Standard for Floating-Point Arithmetic)的标准编号,它规定了浮点数在计算机当中的存储方式以及算术标准. 计算机中数据均是按二进制的方式存储的,浮点数的存储也是如此.但是由于浮点数的特殊性,无法采用整数的补码存储方式,浮点数需要有特定的存储方式.一个浮点数可以分成3部分存储:

  • sign(符号)
  • exponent(指数)
  • fraction(尾数)

https://en.wikipedia.org/wiki/IEEE_754-1985

模块导入

本地模块名和系统库模块名冲突报: ImportError: cannot import name AttrItem

1
2
import configparser 
print configparser.__file__

自定义configparser与系统冲突,通过打印file查看

python之函数

发表于 2019-01-17 | 分类于 language

函数参数

  • 位置参数: def power(x):
  • 默认参数: def power(x, n=2): 默认参数必须指向不变对象!
  • 可变参数: def calc(*numbers):
  • 关键字参数: def person(name, age, **kw):
  • 命名关键字参数:def person(name, age, *, city, job):

参数组合: 在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数

递归及尾递归优化

切片

迭代 默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k, v in d.items()。 通过collections模块的Iterable类型判断一个对象是否可迭代

1
2
3
4
5
6
7
>>> from collections import Iterable
>>> isinstance('abc', Iterable) # str是否可迭代
True
>>> isinstance([1,2,3], Iterable) # list是否可迭代
True
>>> isinstance(123, Iterable) # 整数是否可迭代
False

如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:

1
2
3
4
5
6
>>> for i, value in enumerate(['A', 'B', 'C']):
... print(i, value)
...
0 A
1 B
2 C

列表生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
list(range(1, 11))
[x * x for x in range(1, 11)]
[x * x for x in range(1, 11) if x % 2 == 0]
[m + n for m in 'ABC' for n in 'XYZ']

>>> import os
>>> [d for d in os.listdir('.')] # os.listdir可以列出文件和目录
['.emacs.d', '.ssh', '.Trash', 'Adlm', 'Applications', 'Desktop', 'Documents', 'Downloads', 'Library', 'Movies', 'Music', 'Pictures', 'Public', 'VirtualBox VMs', 'Workspace', 'XCode']

>>> d = {'x': 'A', 'y': 'B', 'z': 'C' }
>>> [k + '=' + v for k, v in d.items()]
['y=B', 'x=A', 'z=C']

>>> L = ['Hello', 'World', 'IBM', 'Apple']
>>> [s.lower() for s in L]
['hello', 'world', 'ibm', 'apple']

生成器:generator 不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制 方法一:只要把一个列表生成式的[]改成(),就创建了一个generator:

1
2
3
4
5
6
>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>

创建L和g的区别仅在于最外层的[]和(),L是一个list,而g是一个generator。我们可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢?如果要一个一个打印出来,可以通过next()函数获得generator的下一个返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
16
>>> next(g)
25
>>> next(g)
36
>>> next(g)
49
>>> next(g)
64
>>> next(g)
81
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration

断调用next(g)实在是太变态了,正确的方法是使用for循环,因为generator也是可迭代对象:

1
2
3
4
>>> g = (x * x for x in range(10))
>>> for n in g:
... print(n)
...

如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator: 函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

迭代器

高阶函数 map/reduce map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。 reduce把一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果就是:

filter: 用于过滤序列

sorted: 对list进行排序

返回函数

匿名函数

装饰器

1
2
3
4
5
def log(func):
def wrapper(*args, **kw):
print('call %s():' % func.__name__)
return func(*args, **kw)
return wrapper

偏函数 functools.partial

python之面向对象

发表于 2019-01-17 | 分类于 language
__slots__

限制类的绑定属性

__str__: print打印对象,返回用户看到的字符串 __repr__:返回程序开发者看到的字符串,如直接输入变量,是为调试服务的

1
2
3
4
5
class Student(object):
def __str__(self):
return 'Student object (name: %s)' % self.name

print(Student('hell0'))

可以直接:

1
__repr = __str__

__iter__: 一个类用于for…in循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1

def __iter__(self):
return self

def __next__(self):
self.a, self.b = self.b, self.a+self.b
if self.a > 100000:
raise StopIteration()
return self.a

for n in Fib():
print(n)

__getitem__:Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素:

1
2
3
4
>>> Fib()[5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'Fib' object does not support indexing

要表现得像list那样按照下标取出元素,需要实现getitem()方法:

1
2
3
4
5
6
class Fib(object):
def __getitem__(self, n):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a

但是list有个神奇的切片方法:

1
>>> list(range(100))[5:10]

对于Fib却报错,原因是getitem()传入的参数可能是一个int,也可能是一个切片对象slice,所以要做判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def __getitem__(self, n):
if isinstance(n, int): # n是索引
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
if isinstance(n, slice): # n是切片
start = n.start
stop = n.stop
if start is None:
start = 0
a, b = 1, 1
L = []
for x in range(stop):
if x >= start:
L.append(a)
a, b = b, a + b
return L

但是现在没有对step参数处理f[:10:2],也没有对负数做处理 此外,如果把对象看成dict,getitem()的参数也可能是一个可以作key的object,例如str。 与之对应的是setitem()方法,把对象视作list或dict来对集合赋值。最后,还有一个delitem()方法,用于删除某个元素。 总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。

__getattr__:动态返回一个属性。

1
2
3
4
5
6
7
8
class Student(object):

def __init__(self):
self.name = 'Michael'

def __getattr__(self, attr):
if attr=='score':
return 99

当调用不存在的属性时,比如score,Python解释器会试图调用getattr(self, ‘score’)来尝试获得属性,这样,我们就有机会返回score的值: 注意,只有在没有找到属性的情况下,才调用getattr,已有的属性,比如name,不会在getattr中查找

__call__:一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用。能不能直接在实例本身上调用呢?在Python中,答案是肯定的。

任何类,只需要定义一个call()方法,就可以直接对实例进行调用。请看示例

1
2
3
4
5
6
class Student(object):
def __init__(self, name):
self.name = name

def __call__(self):
print('My name is %s.' % self.name)

调用方式如下:

1
2
3
>>> s = Student('Michael')
>>> s() # self参数不要传入
My name is Michael.

__call__()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。

怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable对象,比如函数和我们上面定义的带有call()的类实例:

1
2
3
4
5
6
7
8
9
10
>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False

枚举类

1
2
3
4
5
6
from enum import Enum

Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value)

value属性则是自动赋给成员的int常量,默认从1开始计数。 如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:

1
2
3
4
5
6
7
8
9
10
11
from enum import Enum, unique

@unique
class Weekday(Enum):
Sun = 0 # Sun的value被设定为0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6

@unique装饰器可以帮助我们检查保证没有重复值。

访问这些枚举类型可以有若干种方法:

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
>>> day1 = Weekday.Mon
>>> print(day1)
Weekday.Mon
>>> print(Weekday.Tue)
Weekday.Tue
>>> print(Weekday['Tue'])
Weekday.Tue
>>> print(Weekday.Tue.value)
2
>>> print(day1 == Weekday.Mon)
True
>>> print(day1 == Weekday.Tue)
False
>>> print(Weekday(1))
Weekday.Mon
>>> print(day1 == Weekday(1))
True
>>> Weekday(7)
Traceback (most recent call last):
...
ValueError: 7 is not a valid Weekday
>>> for name, member in Weekday.__members__.items():
... print(name, '=>', member)
...
Sun => Weekday.Sun
Mon => Weekday.Mon
Tue => Weekday.Tue
Wed => Weekday.Wed
Thu => Weekday.Thu
Fri => Weekday.Fri
Sat => Weekday.Sat

vscode使用技巧

发表于 2019-01-15 | 分类于 env

配置

设置隐藏文件

用于在资源管理器隐藏固定后缀的文件,配置方式:

  1. 首选项->设置
  2. 搜索files.exclude,选择工作区设置
  3. 添加模式:**/*.pyc

Creating your own snippets

既然你点开了这个页面,那就说明要么你不知道 VSCode 上已有拓展「C/C++ Snippets」,要么你对这个拓展不甚满意。对于后者,本文将为你介绍如何在 VSCode 上设置 snippets,并为你提供一套可以直接用的 C 语言 snippets。

1. snippet 简介

snippet[ˈsnɪpɪt],或者说「code snippet」,也即代码段,指的是能够帮助输入重复代码模式,比如循环或条件语句,的模板。通过 snippet ,我们仅仅输入一小段字符串,就可以在代码段引擎的帮助下,生成预定义的模板代码,接着我们还可以通过在预定义的光标位置之间跳转,来快速补全模板。

当然,看图更易懂。下图将 aja 补全为 JQuery 的 ajax() 方法,并通过光标的跳转,快速补全了待填键值对: image

2. snippet 配置流程

进入 snippet 设置文件,这里提供了三种方法: 通过快捷键「Ctrl + Shift + P」打开命令窗口(All Command Window),输入「snippet」,点选「首选项:配置用户代码段片段」; 点击界面最左侧竖栏(也即活动栏)最下方的齿轮按钮,在弹出来的菜单中点选「用户代码片段」; 按下「Alt」键切换菜单栏,通过文件 > 首选项 > 用户代码片段; 填写 snippets

3. snippet 详细介绍

3.1 引子

设置文件头部的一个块注释给出了设置 snippet 的格式,了解过「json」就不会对此感到奇怪。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Place your snippets for C here. Each snippet is defined under a snippet name and has a prefix, body and 
// description. The prefix is what is used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. Placeholders with the
// same ids are connected.
// Example:
"Print to console": {
"prefix": "log",,
"body": [
"console.log('$1');",
"$2"
],
"description": "Log output to console"
}

上例定义了一个名为「Print to console」的 snippet,其功能为:在输入 log 并确认后,可将原文本替换为console.log(‘’);

3.2 语法结构

然而引子中只是最简单的功能,而 VSCode 的代码段引擎所能做的远不止这些。本文将以官方教程1为本,对其进行详实地阐述。

snippet 由三部分组成:

  1. prefix:前缀,定义了 snippets 从 IntelliSense 中呼出的关键字;
  2. body: 主体,即模板的主体内容,其中每个字符串表示一行;
  3. description:说明,会在 IntelliSense 候选栏中出现。未定义的情况下直接显示对象名,上例中将会显示 Print to console。 Snippet 的三个部分目前只有主体部分支持丰富的特性。接下来整篇文章都是拿来介绍主体部分的。主体部分的介绍将分为两部分:较为直接的基础用法,及结合正则表达式的高级用法。

3.3 Body - 基本用法

主体部分可以使用特殊语法结构,来控制光标和要插入的文本,其支持的基本结构如下:

  1. Tabstops:制表符 用「Tabstops」可以让编辑器的指针在 snippet 内跳转。使用 $1,$2 等指定光标位置。这些数字指定了光标跳转的顺序。特别地,$0表示最终光标位置。相同序号的「Tabstops」被链接在一起,将会同步更新,比如下列用于生成头文件封装的 snippet 被替换到编辑器上时,光标就将同时出现在所有$1位置。

    1
    2
    3
    "#ifndef $1"
    "#define $1"
    "#end // $1"
  2. Placeholders:占位符 「Placeholder」是带有默认值的「Tabstops」,如${1:foo}。「placeholder」文本将被插入「Tabstops」位置,并在跳转时被全选,以方便修改。占位符还可以嵌套,例如${1:another ${2:placeholder}}。 比如,结构体的 snippet 主体可以这样写:

    1
    struct ${1:name_t} {\n\t$2\n};

作为「Placeholder」的name_t一方面可以提供默认的结构名称,另一方面可以作为输入的提示。

  1. Choice:可选项 「Choice」是提供可选值的「Placeholder」。其语法为一系列用逗号隔开,并最终被两个竖线圈起来的枚举值,比如 ${1|one,two,three|}。当光标跳转到该位置的时候,用户将会被提供多个值(one 或 two 或 three)以供选择。

  2. Variables:变量 使用$name或${name:default}可以插入变量的值。当变量未赋值时(如),将插入其缺省值或空字符串。 当varibale未知(即,其名称未定义)时,将插入变量的名称,并将其转换为「Placeholder」。可以使用的「Variable」如下:

  • TM_SELECTED_TEXT:当前选定的文本或空字符串;

  • 注:选定后通过在命令窗口点选「插入代码片段」插入。

  • TM_CURRENT_LINE:当前行的内容;

  • TM_CURRENT_WORD:光标所处单词或空字符串

  • 注:所谓光标一般为文本输入处那条闪来闪去的竖线,该项可定制。单词使用 VSCode 选词(Word Wrap)器选择。你最好只用它选择英文单词,因为这个选择器明显没有针对宽字符优化过,它甚至无法识别宽字符的标点符号。

  • TM_LINE_INDEX:行号(从零开始);

  • TM_LINE_NUMBER:行号(从一开始);

  • TM_FILENAME:当前文档的文件名;

  • TM_FILENAME_BASE:当前文档的文件名(不含后缀名);

  • TM_DIRECTORY:当前文档所在目录;

  • TM_FILEPATH:当前文档的完整文件路径;

  • CLIPBOARD:当前剪贴板中内容。 此外,还有一些用于插入当前时间的变量,这里单独列出:

  • CURRENT_YEAR: 当前年份;

  • CURRENT_YEAR_SHORT: 当前年份的后两位;

  • CURRENT_MONTH: 格式化为两位数字的当前月份,如 02;

  • CURRENT_MONTH_NAME: 当前月份的全称,如 July;

  • CURRENT_MONTH_NAME_SHORT: 当前月份的简称,如 Jul;

  • CURRENT_DATE: 当天月份第几天;

  • CURRENT_DAY_NAME: 当天周几,如 Monday;

  • CURRENT_DAY_NAME_SHORT: 当天周几的简称,如 Mon;

  • CURRENT_HOUR: 当前小时(24 小时制);

  • CURRENT_MINUTE: 当前分钟;

  • CURRENT_SECOND: 当前秒数。 注:这些都是变量名,不是宏,在实际使用的时要加上 $ 符。

3.4 Body - 高级语法

3.4.1 变量转换

变量转换可将变量的值格式化处理后插入预定的位置。

3.4.1.1 语法结构

我们可以通过 ${var_name/regular_expression/format_string/options} 插入格式化后的代码段。显然,「variable transformations」由 4 部分构成:

  1. var_name:变量名;
  2. regular_expression:正则表达式;
  3. format_string:格式串;
  4. options:正则表达式匹配选项。 其中正则表达式的写法和匹配选项部分不在本篇博文的讲解范围之内,具体内容请分别参考 javascript 有关 RegExp(pattern [, flags]) 构造函数中的 pattern 及 flags 参数项的说明2。

本文只对 format_string 部分进行详细介绍。

3.4.1.2 format_string 部分

根据其 EBNF 范式,我们可以知道 format_string 其实是 format 或 text 的线性组合:

  1. text:也即没有任何作用的普通文本,你甚至可以使用汉字;
  2. format:格式串,分为 7 种:
  • $sn:表示插入匹配项;
  • ${sn}:同 $sn;
  • ${sn:/upcase} 或 ${sn:/downcase} 或 ${sn:/capitalize}:表示将匹配项变更为「所有字母均大写/所有字母均小写/首字母大写其余小写」后,插入;
  • ${sn:+if}:表示当匹配成功时,并且捕捉括号捕捉特定序号的捕捉项成功时,在捕捉项位置插入「if」所述语句;
  • ${sn:?if:else}:表示当匹配成功,并且捕捉括号捕捉特定序号的捕捉项成功时,在捕捉项位置插入「if」所述语句;否则当匹配成功,但当捕捉括号捕捉特定序号的捕捉项失败时,在捕捉项位置插入「else」所述语句;
  • ${sn:-else}:表示当匹配成功,但当捕捉括号捕捉特定序号的捕捉项失败时,在捕捉项位置插入「else」所述语句;
  • ${sn:else}:同 ${sn:-else}。 format 的后三条理解起来可能比较困难。这里我们以倒数第三条为例进行说明。假设我们有一个「make.c」文件,我们有这么一条 snippet: “body”: “${TM_FILENAME/make.c(pp|++)?/${1:?c++:clang}/}”。整个模式串匹配成功,但是捕捉括号捕捉后缀名中的 pp 或 ++ 失败,因此判断条件在捕捉括号的位置插入捕捉失败时应插入的字符串,也即「clang」。

    注: 其中 sn 表示捕捉项的序号 其中 if 表示捕捉项捕捉成功时替换的文本 其中 else 表示捕捉项捕失败时替换的文本

3.4.1.3 案例分析

下面笔者再介绍一个简单的例子,帮助大家理解「variable transformations」。

假设有一个名为「make.c」的文件中,并且我们已经定义如下 snippet。

1
2
3
4
"#ifndef HEADER … #define … #endif":{
"prefix": "defheader",
"body": "#ifndef ${1:${TM_FILENAME/(.*)\\.C$/${1:/upcase}_H/i}} \n#define $1 \n${2:header content}\n#endif\t// $1"
}

其中最复杂的模式为:${1:${TM_FILENAME/(.*)\.C$/${1:/upcase}_H/i}},我们将之拆解为如下五部分:

  1. ${1:…}:嵌套的 placeholder;
  2. ${TM_FILENAM/…/…/.}:「variable transformations」中的「var_name」,表示带后缀的文件名;
  3. ${…/(.*)\.C$/…/.}:「variable transformations」中的「regular_expression」,表示匹配任意以「.C」为后缀的字符串;
  4. ${…/…/${1:/upcase}_H/.}}:「variable transformations」中的「options」,表示将第一个捕捉项替换为大写的,并添加「_H」的后缀;
  5. ${…/…/…/i}:「variable transformations」中的「options」,表示无视大小写。
3.4.2 占位符转换
3.4.2.1 语法结构

我们可以通过 ${int/regular_expression/format_string/options} 插入格式化后的代码段。显然,与变量转换,「placeholder transformations」也由 4 部分构成:

  1. int:占位符相应光标序号;
  2. regular_expression:正则表达式;
  3. format_string:格式串;
  4. options:正则表达式匹配选项。 上述全部内容我们都在前文介绍过了,因此此处不做赘述。我们唯一需要关注的是转换触发的时机:占位符转换将在进行占位符跳转(假设 1→2)的时候自动适用到当前占位符(1)。
3.4.2.2 案例分析

假设我们已经这样的 Snippet:

1
2
3
4
"HelloWorld": {
"prefix": "say_hello",
"body": "${1} ${2} -> ${1/Hello/Hallo/} ${2/World/Welt/}"
}

那么我们在两个制表位同时键入 Hello 并跳转的时候,第一个制表位依然保持 Hello 不变,而第二个制表位(占位符)被替换为 Hallo。键入 Welt 亦然。

3.5 语法定义

官网给出了 snippet 的 EBNF 范式的正则文法,注意,作普通字符使用时,$ , } 和 \ 可使用 \(反斜杠)转义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
any         ::= tabstop | placeholder | choice | variable | text
tabstop ::= '$' int
| '${' int '}'
| '${' int transform '}'
placeholder ::= '${' int ':' any '}'
choice ::= '${' int '|' text (',' text)* '|}'
variable ::= '$' var | '${' var '}'
| '${' var ':' any '}'
| '${' var transform '}'
transform ::= '/' regex '/' (format | text)+ '/' options
format ::= '$' int | '${' int '}'
| '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}'
| '${' int ':+' if '}'
| '${' int ':?' if ':' else '}'
| '${' int ':-' else '}' | '${' int ':' else '}'
regex ::= JavaScript Regular Expression value (ctor-string)
options ::= JavaScript Regular Expression option (ctor-options)
var ::= [_a-zA-Z] [_a-zA-Z0-9]*
int ::= [0-9]+
text ::= .*

4. 一些建议

默认情况下 snippet 在 IntelliSense 中的显示优先级并不高,而且在 IntelliSense 中选择相应 snippet 需要按「enter」键,这对于手指短的人来说并不是什么很好的体验。

所幸,VSCode 意识到了这一点,并为我们提供了改进的方式。我们可以在 VSCode 的用户设置(「Ctrl+P」在输入框中写「user settings」后点选)中,检索代码段,然后根据提示修改代码段的相关设置。

我们可以设置在 IntelliSense 中优先显示代码段,并可以通过「TAB」补全。

修改后设置文件中会多出这两行:

1
2
"editor.snippetSuggestions": "top",
"editor.tabCompletion": true

参考:https://code.visualstudio.com/docs/editor/userdefinedsnippets https://blog.csdn.net/maokelong95/article/details/54379046?utm_source=blogxgwz0

1…567…19
轻口味

轻口味

190 日志
27 分类
63 标签
RSS
GitHub 微博 豆瓣 知乎
友情链接
  • SRS
© 2015 - 2019 轻口味
京ICP备17018543号
本站访客数 人次 本站总访问量 次