导读:

直播行业的竞争越来越激烈,进过18年这波洗牌后,已经度过了蛮荒暴力期,剩下的都是在不断追求体验。最近在帮做直播优化首开,通过多种方案并行,把首开降到500ms以下,希望能对大家有借鉴。

背景:

基于FFmpeg的ijkplayer,最新版本0.88版本。

拉流协议基于http-flv,http-flv更稳定些,国内大部分直播公司基本都是使用http-flv了,从我们实际数据来看,http-flv确实稍微更快些。但是考虑到会有rtmp源,这块也加了些优化。

IP直通车

简单理解就是,把域名替换成IP。比如https://www.baidu.com/,你可以直接换成14.215.177.39,这样做的目的是,省去了DNS解析的耗时,尤其在网络不好时,访问域名,域名要去解析,再给你返回。不仅仅有时间解析过长的问题,还有小运营商DNS劫持的问题。一般就是在启动应用时,就开始对拉流的域名进行预解析好,存到本地,然后在真正拉流时,直接用就行。典型的案列,就是很多人使用HTTPDNS,这个github上也有开源,可以自行去研究下。

需要注意的是,这种方案在使用 HTTPS 时,是会失败的。因为 HTTPS 在证书验证的过程,会出现 domain 不匹配导致 SSL/TLS 握手不成功。

服务端 GOP 缓存

除了客户端业务侧的优化外,我们还可以从流媒体服务器侧进行优化。我们都知道直播流中的图像帧分为:I 帧、P 帧、B 帧,其中只有 I 帧是能不依赖其他帧独立完成解码的,这就意味着当播放器接收到 I 帧它能马上渲染出来,而接收到 P 帧、B 帧则需要等待依赖的帧而不能立即完成解码和渲染,这个期间就是「黑屏」了。

所以,在服务器端可以通过缓存 GOP(在 H.264 中,GOP 是封闭的,是以 I 帧开头的一组图像帧序列),保证播放端在接入直播时能先获取到 I 帧马上渲染出画面来,从而优化首屏加载的体验。

这里有一个 IDR 帧的概念需要讲一下,所有的 IDR 帧都是 I 帧,但是并不是所有 I 帧都是 IDR 帧,IDR 帧是 I 帧的子集。I 帧严格定义是帧内编码帧,由于是一个全帧压缩编码帧,通常用 I 帧表示「关键帧」。IDR 是基于 I 帧的一个扩展,带了控制逻辑,IDR 图像都是 I 帧图像,当解码器解码到 IDR 图像时,会立即将参考帧队列清空,将已解码的数据全部输出或抛弃。重新查找参数集,开始一个新的序列。这样如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR 图像之后的图像永远不会使用 IDR 之前的图像的数据来解码。在 H.264 编码中,GOP 是封闭式的,一个 GOP 的第一帧都是 IDR 帧。

推流端设置

一般播放器需要拿到一个完整的GOP,才能记性播放。GOP是在推流端可以设置,比如下面这个图,是我dump一个流,看到的GOP情况。GOP大小是50,推流过来的fps设置是25,也就是1s内会显示25个Frame,50个Frame,刚好直播设置GOP 2S,但是直播一般fps不用设置这么高,可以随便dump任何一家直播公司的推流,设置fps在15-18之间就够了。

播放器相关耗时

当set一个源给播放器后,播放器需要open这个流,然后和服务端建立长连接,然后demux,codec,最后渲染。我们可以按照播放器的四大块,依次优化

  • 数据请求耗时
  • 解复用耗时
  • 解码耗时
  • 渲染出图耗时

数据请求

这里就是网络和协议相关。无论是http-flv,还是rtmp,都主要是基于tcp的,所以一定会有tcp三次握手,同时打开tcp.c分析。需要加日志在一些方法中,如下tcp_open方法。是已经改动过的

/* return non zero if error */
static int tcp_open(URLContext *h, const char *uri, int flags)
{
av_log(NULL, AV_LOG_INFO, "tcp_open begin");
...省略部分代码
if (!dns_entry) {
#ifdef HAVE_PTHREADS
av_log(h, AV_LOG_INFO, "ijk_tcp_getaddrinfo_nonblock begin.\n");
ret = ijk_tcp_getaddrinfo_nonblock(hostname, portstr, &hints, &ai, s->addrinfo_timeout, &h->interrupt_callback, s->addrinfo_one_by_one);
av_log(h, AV_LOG_INFO, "ijk_tcp_getaddrinfo_nonblock end.\n");
#else
if (s->addrinfo_timeout > 0)
av_log(h, AV_LOG_WARNING, "Ignore addrinfo_timeout without pthreads support.\n");
av_log(h, AV_LOG_INFO, "getaddrinfo begin.\n");
if (!hostname[0])
ret = getaddrinfo(NULL, portstr, &hints, &ai);
else
ret = getaddrinfo(hostname, portstr, &hints, &ai);
av_log(h, AV_LOG_INFO, "getaddrinfo end.\n");
#endif if (ret) {
av_log(h, AV_LOG_ERROR,
"Failed to resolve hostname %s: %s\n",
hostname, gai_strerror(ret));
return AVERROR(EIO);
} cur_ai = ai;
} else {
av_log(NULL, AV_LOG_INFO, "Hit DNS cache hostname = %s\n", hostname);
cur_ai = dns_entry->res;
} restart:
#if HAVE_STRUCT_SOCKADDR_IN6
// workaround for IOS9 getaddrinfo in IPv6 only network use hardcode IPv4 address can not resolve port number.
if (cur_ai->ai_family == AF_INET6){
struct sockaddr_in6 * sockaddr_v6 = (struct sockaddr_in6 *)cur_ai->ai_addr;
if (!sockaddr_v6->sin6_port){
sockaddr_v6->sin6_port = htons(port);
}
}
#endif fd = ff_socket(cur_ai->ai_family,
cur_ai->ai_socktype,
cur_ai->ai_protocol);
if (fd < 0) {
ret = ff_neterrno();
goto fail;
} /* Set the socket's send or receive buffer sizes, if specified.
If unspecified or setting fails, system default is used. */
if (s->recv_buffer_size > 0) {
setsockopt (fd, SOL_SOCKET, SO_RCVBUF, &s->recv_buffer_size, sizeof (s->recv_buffer_size));
}
if (s->send_buffer_size > 0) {
setsockopt (fd, SOL_SOCKET, SO_SNDBUF, &s->send_buffer_size, sizeof (s->send_buffer_size));
} if (s->listen == 2) {
// multi-client
if ((ret = ff_listen(fd, cur_ai->ai_addr, cur_ai->ai_addrlen)) < 0)
goto fail1;
} else if (s->listen == 1) {
// single client
if ((ret = ff_listen_bind(fd, cur_ai->ai_addr, cur_ai->ai_addrlen,
s->listen_timeout, h)) < 0)
goto fail1;
// Socket descriptor already closed here. Safe to overwrite to client one.
fd = ret;
} else {
ret = av_application_on_tcp_will_open(s->app_ctx);
if (ret) {
av_log(NULL, AV_LOG_WARNING, "terminated by application in AVAPP_CTRL_WILL_TCP_OPEN");
goto fail1;
} if ((ret = ff_listen_connect(fd, cur_ai->ai_addr, cur_ai->ai_addrlen,
s->open_timeout / 1000, h, !!cur_ai->ai_next)) < 0) {
if (av_application_on_tcp_did_open(s->app_ctx, ret, fd, &control))
goto fail1;
if (ret == AVERROR_EXIT)
goto fail1;
else
goto fail;
} else {
ret = av_application_on_tcp_did_open(s->app_ctx, 0, fd, &control);
if (ret) {
av_log(NULL, AV_LOG_WARNING, "terminated by application in AVAPP_CTRL_DID_TCP_OPEN");
goto fail1;
} else if (!dns_entry && strcmp(control.ip, hostname_bak)) {
add_dns_cache_entry(hostname_bak, cur_ai, s->dns_cache_timeout);
av_log(NULL, AV_LOG_INFO, "Add dns cache hostname = %s, ip = %s\n", hostname_bak , control.ip);
}
}
} h->is_streamed = 1;
s->fd = fd; if (dns_entry) {
release_dns_cache_reference(hostname_bak, &dns_entry);
} else {
freeaddrinfo(ai);
}
av_log(NULL, AV_LOG_INFO, "tcp_open end");
return 0;
// 省略部分代码
}

改动地方主要是hints.ai_family = AF_INET;,原来是 hints.ai_family = AF_UNSPEC;,原来设计是一个兼容IPv4和IPv6的配置,如果修改成AF_INET,那么就不会有AAAA的查询包了。如果只有IPv4的请求,就可以改成AF_INET。当然有IPv6,这里就不要动了。这么看是否有,可以通过抓包工具看。

接着分析,我们发现tcp_read函数是个阻塞式的,会非常耗时,我们又不能设置短一点中断时间,因为短了的话,造成读取不到数据,就中断,后续播放就直接失败了,这里只能让它等。不过还是优化的点时下面部分

static int tcp_read(URLContext *h, uint8_t *buf, int size)
{
av_log(NULL, AV_LOG_INFO, "tcp_read begin %d\n", size);
TCPContext *s = h->priv_data;
int ret; if (!(h->flags & AVIO_FLAG_NONBLOCK)) {
ret = ff_network_wait_fd_timeout(s->fd, 0, h->rw_timeout, &h->interrupt_callback);
if (ret)
return ret;
}
ret = recv(s->fd, buf, size, 0);
if (ret == 0)
return AVERROR_EOF;
//if (ret > 0)
// av_application_did_io_tcp_read(s->app_ctx, (void*)h, ret);
av_log(NULL, AV_LOG_INFO, "tcp_read end %d\n", ret);
return ret < 0 ? ff_neterrno() : ret;
}

我们可以把上面两行注释掉,因为在ff_network_wait_fd_timeout等回来后,数据可以放到buf中,下面av_application_did_io_tcp_read就没必要去执行了。原来每次ret>0,都会执行av_application_did_io_tcp_read这个函数。

解复用耗时

在日志中发现,数据请求到后,进行音视频分离时,首先需要匹配对应demuxer,其中ffmpeg的av_find_input_format和avformat_find_stream_info非常耗时,前者简单理解就是打开某中请求到数据,后者就是探测流的一些信息,做一些样本检测,读取一定长度的码流数据,来分析码流的基本信息,为视频中各个媒体流的 AVStream 结构体填充好相应的数据。这个函数中做了查找合适的解码器、打开解码器、读取一定的音视频帧数据、尝试解码音视频帧等工作,基本上完成了解码的整个流程。这时一个同步调用,在不清楚视频数据的格式又要做到较好的兼容性时,这个过程是比较耗时的,从而会影响到播放器首屏秒开。这两个函数调用都在ff_ffplay.c的read_thread函数中:

    if (ffp->iformat_name) {
av_log(ffp, AV_LOG_INFO, "av_find_input_format noraml begin");
is->iformat = av_find_input_format(ffp->iformat_name);
av_log(ffp, AV_LOG_INFO, "av_find_input_format normal end");
}
else if (av_stristart(is->filename, "rtmp", NULL)) {
av_log(ffp, AV_LOG_INFO, "av_find_input_format rtmp begin");
is->iformat = av_find_input_format("flv");
av_log(ffp, AV_LOG_INFO, "av_find_input_format rtmp end");
ic->probesize = 4096;
ic->max_analyze_duration = 2000000;
ic->flags |= AVFMT_FLAG_NOBUFFER;
}
av_log(ffp, AV_LOG_INFO, "avformat_open_input begin");
err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
av_log(ffp, AV_LOG_INFO, "avformat_open_input end");
if (err < 0) {
print_error(is->filename, err);
ret = -1;
goto fail;
}
ffp_notify_msg1(ffp, FFP_MSG_OPEN_INPUT); if (scan_all_pmts_set)
av_dict_set(&ffp->format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE); if ((t = av_dict_get(ffp->format_opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {
av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);
#ifdef FFP_MERGE
ret = AVERROR_OPTION_NOT_FOUND;
goto fail;
#endif
}
is->ic = ic; if (ffp->genpts)
ic->flags |= AVFMT_FLAG_GENPTS; av_format_inject_global_side_data(ic); if (ffp->find_stream_info) {
AVDictionary **opts = setup_find_stream_info_opts(ic, ffp->codec_opts);
int orig_nb_streams = ic->nb_streams; do {
if (av_stristart(is->filename, "data:", NULL) && orig_nb_streams > 0) {
for (i = 0; i < orig_nb_streams; i++) {
if (!ic->streams[i] || !ic->streams[i]->codecpar || ic->streams[i]->codecpar->profile == FF_PROFILE_UNKNOWN) {
break;
}
} if (i == orig_nb_streams) {
break;
}
}
ic->probesize=100*1024;
ic->max_analyze_duration=5*AV_TIME_BASE;
ic->fps_probe_size=0;
av_log(ffp, AV_LOG_INFO, "avformat_find_stream_info begin");
err = avformat_find_stream_info(ic, opts);
av_log(ffp, AV_LOG_INFO, "avformat_find_stream_info end");
} while(0);
ffp_notify_msg1(ffp, FFP_MSG_FIND_STREAM_INFO);

最终改的如上,主要是对rtmp增加了,指定format为‘flv’,以及样本大小。

同时在外部可以通过设置 probesize 和 analyzeduration 两个参数来控制该函数读取的数据量大小和分析时长为比较小的值来降低 avformat_find_stream_info的耗时,从而优化播放器首屏秒开。但是,需要注意的是这两个参数设置过小时,可能会造成预读数据不足,无法解析出码流信息,从而导致播放失败、无音频或无视频的情况。所以,在服务端对视频格式进行标准化转码,从而确定视频格式,进而再去推算 avformat_find_stream_info分析码流信息所兼容的最小的probesize和 analyzeduration,就能在保证播放成功率的情况下最大限度地区优化首屏秒开。

在 FFmpeg 中的 utils.c 文件中的函数实现中有一行代码是 int fps_analyze_framecount = 20;,这行代码的大概用处是,如果外部没有额外设置这个值,那么 avformat_find_stream_info 需要获取至少 20 帧视频数据,这对于首屏来说耗时就比较长了,一般都要 1s 左右。而且直播还有实时性的需求,所以没必要至少取 20 帧。将这个值初始化为2,看看效果。

/* check if one codec still needs to be handled */
for (i = 0; i < ic->nb_streams; i++) {
int fps_analyze_framecount = 2; st = ic->streams[i];
if (!has_codec_parameters(st, NULL))
break; if (ic->metadata) {
AVDictionaryEntry *t = av_dict_get(ic->metadata, "skip-calc-frame-rate", NULL, AV_DICT_MATCH_CASE);
if (t) {
int fps_flag = (int) strtol(t->value, NULL, 10);
if (!st->r_frame_rate.num && st->avg_frame_rate.num > 0 && st->avg_frame_rate.den > 0 && fps_flag > 0) {
int avg_fps = st->avg_frame_rate.num / st->avg_frame_rate.den;
if (avg_fps > 0 && avg_fps <= 120) {
st->r_frame_rate.num = st->avg_frame_rate.num;
st->r_frame_rate.den = st->avg_frame_rate.den;
}
}
}
}

这样,avformat_find_stream_info 的耗时就可以缩减到 100ms 以内。

最后就是解码耗时和渲染出图耗时,这块优化空间很少,大头都在前面。

有人开始抛出问题了,你这个起播快是快,但是后面网络不好,卡顿怎么办?直播中会引起卡顿,主要是网络有抖动的时候,没有足够的数据来播放,ijkplayer会激发其缓冲机制,主要是有几个宏控制

  • DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS:网络差时首次去唤醒read_thread函数去读取数据。
  • DEFAULT_NEXT_HIGH_WATER_MARK_IN_MS:第二次去唤醒read_thread函数去读取数据。
  • DEFAULT_LAST_HIGH_WATER_MARK_IN_MS这个宏的意思是最后的机会去唤醒read_thread函数去读取数据。

可以设置DEFAULT_LAST_HIGH_WATER_MARK_IN_MS为1 * 1000,也即缓冲1秒后开始通知缓冲完成去读取数据,默认是5秒,如果过大,会让用户等太久,那么每次读取的bytes也可以少些。可以设置DEFAULT_HIGH_WATER_MARK_IN_BYTES小一些,设置为30 * 1024,默认是256 * 1024。把BUFFERING_CHECK_PER_MILLISECONDS设置为50,默认是500

#define DEFAULT_HIGH_WATER_MARK_IN_BYTES        (30 * 1024)

#define DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS     (100)
#define DEFAULT_NEXT_HIGH_WATER_MARK_IN_MS (1 * 1000)
#define DEFAULT_LAST_HIGH_WATER_MARK_IN_MS (1 * 1000) #define BUFFERING_CHECK_PER_BYTES (512)
#define BUFFERING_CHECK_PER_MILLISECONDS (50)

可以看下这些宏使用的地方

inline static void ffp_reset_demux_cache_control(FFDemuxCacheControl *dcc)
{
dcc->min_frames = DEFAULT_MIN_FRAMES;
dcc->max_buffer_size = MAX_QUEUE_SIZE;
dcc->high_water_mark_in_bytes = DEFAULT_HIGH_WATER_MARK_IN_BYTES; dcc->first_high_water_mark_in_ms = DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS;
dcc->next_high_water_mark_in_ms = DEFAULT_NEXT_HIGH_WATER_MARK_IN_MS;
dcc->last_high_water_mark_in_ms = DEFAULT_LAST_HIGH_WATER_MARK_IN_MS;
dcc->current_high_water_mark_in_ms = DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS;
}

最后优化的点,是设置一些参数值,也能优化一部分,实际上很多直播用软件用低分辨率240,甚至360,来达到秒开,可以可以作为一个减少耗时点来展开的,因为分辨率越低,数据量越少,首开越快。

mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 0);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http-detect-range-support", 0);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "max_delay", 0); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "max-buffer-size", 4 * 1024);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", 50);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probsize", "1024");
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", "100");
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_clear", 1);
//静音
//mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "an", 1);
//重连模式,如果中途服务器断开了连接,让它重新连接
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1);

以上完了后,就可以看下测试数据,分辨率在540p以下基本秒开,在4G网络下测试:

1、河北卫视直播源,测试10组,平均下来300ms。一组数据386ms,如下:

11-17 14:17:46.659 9896 10147 D IJKMEDIA: IjkMediaPlayer_native_setup

11-17 14:17:46.663 9896 10147 V IJKMEDIA: setDataSource: path http://weblive.hebtv.com/live/hbws_bq/index.m3u8

11-17 14:17:46.666 9896 10177 I FFMPEG : [FFPlayer @ 0xe070d400] avformat_open_input begin

11-17 14:17:46.841 9896 10177 I FFMPEG : [FFPlayer @ 0xe070d400] avformat_open_input end

11-17 14:17:46.841 9896 10177 I FFMPEG : [FFPlayer @ 0xe070d400] avformat_find_stream_info begin

11-17 14:17:46.894 9896 10177 I FFMPEG : [FFPlayer @ 0xe070d400] avformat_find_stream_info end

11-17 14:17:47.045 9896 10191 D IJKMEDIA: Video: first frame decoded

11-17 14:17:47.046 9896 10175 D IJKMEDIA: FFP_MSG_VIDEO_DECODED_START:

2、映客直播秀场源,测试10组,平均下来400ms。一组数据418ms,如下:

11-17 14:21:32.908 11464 11788 D IJKMEDIA: IjkMediaPlayer_native_setup

11-17 14:21:32.952 11464 11788 V IJKMEDIA: setDataSource: path http://14.215.100.45/hw.pull.inke.cn/live/1542433669916866_0_ud.flv?ikDnsOp=1001&ikHost=hw&ikOp=0&codecInfo=8192&ikLog=1&ikSyncBeta=1&dpSrc=6&push_host=trans.push.cls.inke.cn&ikMinBuf=2900&ikMaxBuf=3600&ikSlowRate=0.9&ikFastRate=1.1

11-17 14:21:32.996 11464 11818 I FFMPEG : [FFPlayer @ 0xc2575c00] avformat_open_input begin

11-17 14:21:33.161 11464 11818 I FFMPEG : [FFPlayer @ 0xc2575c00] avformat_open_input end

11-17 14:21:33.326 11464 11829 D IJKMEDIA: Video: first frame decoded

3、熊猫直播游戏直播源,测试10组,平均下来350ms。一组数据373ms,如下:

11-17 14:29:17.615 15801 16053 D IJKMEDIA: IjkMediaPlayer_native_setup

11-17 14:29:17.645 15801 16053 V IJKMEDIA: setDataSource: path http://flv-live-qn.xingxiu.panda.tv/panda-xingxiu/dc7eb0c2e78c96646591aae3a20b0686.flv

11-17 14:29:17.649 15801 16079 I FFMPEG : [FFPlayer @ 0xeb5ef000] avformat_open_input begin

11-17 14:29:17.731 15801 16079 I FFMPEG : [FFPlayer @ 0xeb5ef000] avformat_open_input end

11-17 14:29:17.988 15801 16090 D IJKMEDIA: Video: first frame decoded



欢迎关注我的微信公众号「码农突围」,分享Python、Java、大数据、机器学习、人工智能等技术,关注码农技术提升•职场突围•思维跃迁,20万+码农成长充电第一站,陪有梦想的你一起成长。

吐血干货,直播首屏耗时400ms以下的优化实践的更多相关文章

  1. 技术干货:实时视频直播首屏耗时400ms内的优化实践

    本文由“逆流的鱼yuiop”原创分享于“何俊林”公众号,感谢作者的无私分享. 1.引言 直播行业的竞争越来越激烈,进过2018年这波洗牌后,已经度过了蛮荒暴力期,剩下的都是在不断追求体验.最近正好在做 ...

  2. 页面切换提速30%!京东商城APP首屏耗时监控及优化实践

    https://mp.weixin.qq.com/s/vIG_x1MWC33HKV1GxalVHg 原创 平台研发朱跃棕等 京东零售技术 2020-12-09 网络接口请求可以从接口结构合理性及多接口 ...

  3. 【腾讯Bugly干货分享】微信iOS SQLite源码优化实践

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57b58022433221be01499480 作者:张三华 前言 随着微信iO ...

  4. 腾讯优测优分享 | 探索react native首屏渲染最佳实践

    腾讯优测是专业的移动云测试平台,旗下的优分享不定时提供大量移动研发及测试相关的干货~ 此文主要与以下内容相关,希望对大家有帮助. react native给了我们使用javascript开发原生app ...

  5. 探索react native首屏渲染最佳实践

    文 / 腾讯 龚麒 0.前言 react native给了我们使用javascript开发原生app的能力,在使用react native完成兴趣部落安卓端发现tab改造后,我们开始对由react n ...

  6. 手游录屏直播技术详解 | 直播 SDK 性能优化实践

    在上期<直播推流端弱网优化策略 >中,我们介绍了直播推流端是如何优化的.本期,将介绍手游直播中录屏的实现方式. 直播经过一年左右的快速发展,衍生出越来越丰富的业务形式,也覆盖越来越广的应用 ...

  7. 【腾讯bugly干货】QQ空间直播秒开优化实践

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址为:http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=1204&am ...

  8. Vue SPA 首屏加载优化实践

    写在前面 本文记录笔者在Vue SPA项目首屏加载优化过程中遇到的一些坑及优化方案! 我们以 vue-cli 工具为例,使用 vue-router 搭建SPA应用,UI框架选用 element-ui ...

  9. Vue优化首屏加载

    背景: 使用vue + iview搭建的一个后台管理系统,路由已经用了懒加载,加载登陆页面,居然还是需要18S左右,刚到一个新公司,项目经理很委婉的说,看看能不能优化了一下.然后就开始了网上一大堆'v ...

随机推荐

  1. Promise的用法(js&java)

    Promise(onsuccess , on fail) f1.then(f2.then(), f3.then()) java CompletableFuture.thenAccept

  2. 使json或字典输出更美观

    这里是选取的项目中部分代码,但是关于json或字典格式化输出的代码是完整的def send_post(url, data): 使用json.dumps()后数据被转成了str类型,如果还要对该数据像字 ...

  3. git 第一次上传本地代码到远程仓库,解决 ! [rejected] master -> master (non-fast-forward)错误

    使用git想GitHub远程仓库上传代码的基本步骤一般是 初始化为git仓库 git init 添加所有要提交的文件 git add . 本次提交说明 git commit -m '提交说明' 关联G ...

  4. mvn测试常用命令

    -Dmaven.test.failure.ignore=true  测试报错忽略 例子: mvn package -DAPP_ENV=dev -Dmaven.test.failure.ignore=t ...

  5. OCR:慧眼读世界

    作者:微软亚洲研究院首席研究员 霍强 把手机摄像头对准菜单上的法语菜名,屏幕上实时显示出翻译好的中文菜名:将全世界图书馆的藏书转化为电子书:街景车游走于大街小巷,拍摄街景的同时也从街景图像中自动提取文 ...

  6. form组件及cookie和session

    多对多关系表的三种创建方式 1.全自动创建 优势:不需要你手动创建第三张表 不足:由于第三张表不是你手动创建的,也就意味着,第三张表字段是固定的无法更改 class Book(models.Model ...

  7. 2018.2最新-Scrapy+elasticSearch+Django打造搜索引擎(二)

    请问您今天要来个引擎吗? 工欲善其事必先利其器 最终项目上线演示地址: http://search.mtianyan.cn 第二节:我们搞搞比利,搞搞环境的搭建.Github地址: https://g ...

  8. 传智播客学习之Android运行原理 (转)

    传智播客学习之Android运行原理 (2010-03-20 22:45:15) 转载▼ 今天终于忙里偷闲,和大家探讨一下android技术,第一次听到3G应该追溯到大学三年级的时候了,记得当时现代通 ...

  9. Spring Boot从入门到精通(五)多数据源配置实现及源码分析

    多数据源配置在项目软件中是比较常见的开发需求,Spring和Spring Boot中对此都有相应的解决方案可供大家参考.在Spring Boot中,如MyBatis.JdbcTemplate以及Jpa ...

  10. 一天速成Python教程

    一.Python基础 Python是对象有类型,变量无类型的动态类型语言,追求简单优雅易读.可以在终端中逐行运行,也可以编写成大型的面向对象的工程.在开始写之前,注意Python 2.X中,开头要写上 ...