lvpengwei’s Blog

学习历程,生活点滴。

Mp3精准seek与比特池技术

| Comments

ffmpeg 的 seek flag AVSEEK_FLAG_ANY 并不精准。

起因

最近在做音频剪辑的功能,有下面的场景

一段音频,一个时间区间将它分成三段,前段和后段速度保持不变,中间一段变速2倍。

实现上,我分成了三个不同的 segment 来处理,segment.start 不等于 0 的,会执行一下 seek,使用的是 ffmpeg 的 AVSEEK_FLAG_ANY | AVSEEK_FLAG_BACKWARD,来精准 seek,完成之后发现段与段交接的地方声音并不连贯。

裁剪 frame

我已经做了一个处理,在段结尾的时候,裁掉多余的bytes,在段开始的时候也裁掉,保证段与段之间解码后的数据连续。但是声音还是不连续。

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
std::shared_ptr<SampleData> AudioSegmentReader::copyNextSample() {
    if (currentLength >= endLength) {
        return nullptr;
    }
    auto data = copyNextSampleInternal();
    if (data == nullptr) {
        return nullptr;
    }
    // 裁掉结尾多余的 bytes
    data->length = std::min(data->length, endLength -   currentLength);
    currentLength += data->length;
    return data;
}

// 解码出的数据判断是否需要裁掉开头的 bytes
data = decoder->onRenderFrame();
auto time = decoder->currentPresentationTime();
if (0 <= time && time < startTime) {
    auto delta = startLength - SampleTimeToLength(time, outputSetting.get());
    if (delta < data->length) {
        data->data += delta;
        data->length -= delta;
    } else {
        data->data = nullptr;
        data->length = 0;
    }
}

排查 packet 和 frame

打印了一下段与段连接地方的 packet 的 packetData 和 frameData,发现 packetData 正常,seek 之后的 frameData 中前面大部分是 0,和上一段结尾解出的 frameData 不一样。记得音频帧可以独立解码,不需要参考前面的帧数据,那问题出现在哪里?

一个测试:解封装连续,解码之前 flush 一下 decoder,会发现 frameData 基本都是有问题的。

了解 mp3 帧头格式

很多规则,但是没卵用。

比特池技术

最后去查 mp3 的解码过程实现,发现 mp3 使用了比特池技术,当前帧的主数据可能放在上一帧。。。。也就是要实现精准 seek,得往前多 seek 几帧,然后把前面的 frame 丢掉。 试了一下,结果如预期。

参考

mp3比特池技术
功耗高集成度MP3解码器IP核设计

如何获取VideoToolbox的reorder Size

| Comments

Decoder 的区别

FFmpeg 和 MediaCodec 解码的时候,送数据的顺序是 dts,出数据的顺序是 pts,而 VideoToolbox 是送一个出一个,没有按照 pts 来出数据,需要我们自己排序。

去网上查资料的时候,发现有很多不同的方式

  1. sps.max_num_ref_frames
  2. sps.vui.max_num_reorder_frames
  3. 通过 sps.level 计算
  4. 直接设置为4

通过测试几个文件的 sps 发现 max_num_ref_frames 不是很准

  1. max_num_ref_frames=0; max_num_reorder_frames=2
  2. max_num_ref_frames=9; max_num_reorder_frames=2

sps.max_num_ref_frames

max_num_ref_frames 的有两个播放器,ijkplayer 和 ThumbPlayer

ijkplayer

ijkplayer 的逻辑是先取 sps.max_num_ref_frames,然后再取最小值2,最大值5。

1
2
3
fmt_desc->max_ref_frames = FFMAX(fmt_desc->max_ref_frames, 2);

fmt_desc->max_ref_frames = FFMIN(fmt_desc->max_ref_frames, 5);

主要代码在下面两个文件。
IJKVideoToolBoxAsync.m
h264_sps_parser.h

ThumbPlayer

ThumbPlayer 的逻辑是取 sps.max_num_ref_frames,如果没有设置为 10。

sps.vui.max_num_reorder_frames

max_num_reorder_frames 的有三个

  1. Chrome
  2. vlc
  3. MediaCodec

Chrome

Chrome 的主要代码如下,代码文件在vt_video_decode_accelerator_mac.cc

  1. 先判断 pocType,为 2 直接返回不需要排序
  2. 再判断是否有 vuiParameters,取 max_num_reorder_frames
  3. 然后是特定的 profile,不需要排序
  4. 最后返回 max_dpb_frames 的 默认值16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int32_t ComputeReorderWindow(const H264SPS* sps) {
  // When |pic_order_cnt_type| == 2, decode order always matches presentation
  // order.
  // TODO(sandersd): For |pic_order_cnt_type| == 1, analyze the delta cycle to
  // find the minimum required reorder window.
  if (sps->pic_order_cnt_type == 2)
    return 0;

  // TODO(sandersd): Compute MaxDpbFrames.
  int32_t max_dpb_frames = 16;

  // See AVC spec section E.2.1 definition of |max_num_reorder_frames|.
  if (sps->vui_parameters_present_flag && sps->bitstream_restriction_flag) {
    return std::min(sps->max_num_reorder_frames, max_dpb_frames);
  } else if (sps->constraint_set3_flag) {
    if (sps->profile_idc == 44 || sps->profile_idc == 86 ||
        sps->profile_idc == 100 || sps->profile_idc == 110 ||
        sps->profile_idc == 122 || sps->profile_idc == 244) {
      return 0;
    }
  }
  return max_dpb_frames;
}

vlc

vlc 的逻辑和 chrome 类似,多了一个根据 level 计算 max_dpb_frames

  1. 判断是否有 vuiParameters,取 max_num_reorder_frames
  2. 然后是特定的 profile,不需要排序
  3. 最后计算 max_dpb_frames

代码文件在h264_nal.c

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
static uint8_t h264_get_max_dpb_frames( const h264_sequence_parameter_set_t *p_sps )
{
    const h264_level_limits_t *limits = h264_get_level_limits( p_sps );
    if( limits )
    {
        unsigned i_frame_height_in_mbs = ( p_sps->pic_height_in_map_units_minus1 + 1 ) *
                                         ( 2 - p_sps->frame_mbs_only_flag );
        unsigned i_den = ( p_sps->pic_width_in_mbs_minus1 + 1 ) * i_frame_height_in_mbs;
        uint8_t i_max_dpb_frames = limits->i_max_dpb_mbs / i_den;
        if( i_max_dpb_frames < 16 )
            return i_max_dpb_frames;
    }
    return 16;
}

bool h264_get_dpb_values( const h264_sequence_parameter_set_t *p_sps,
                          uint8_t *pi_depth, unsigned *pi_delay )
{
    uint8_t i_max_num_reorder_frames = p_sps->vui.i_max_num_reorder_frames;
    if( !p_sps->vui.b_bitstream_restriction_flag )
    {
        switch( p_sps->i_profile ) /* E-2.1 */
        {
            case PROFILE_H264_BASELINE:
                i_max_num_reorder_frames = 0; /* only I & P */
                break;
            case PROFILE_H264_CAVLC_INTRA:
            case PROFILE_H264_SVC_HIGH:
            case PROFILE_H264_HIGH:
            case PROFILE_H264_HIGH_10:
            case PROFILE_H264_HIGH_422:
            case PROFILE_H264_HIGH_444_PREDICTIVE:
                if( p_sps->i_constraint_set_flags & H264_CONSTRAINT_SET_FLAG(3) )
                {
                    i_max_num_reorder_frames = 0; /* all IDR */
                    break;
                }
                /* fallthrough */
            default:
                i_max_num_reorder_frames = h264_get_max_dpb_frames( p_sps );
                break;
        }
    }

    *pi_depth = i_max_num_reorder_frames;
    *pi_delay = 0;

    return true;
}

MediaCodec

MediaCodecvlc/Chrome也差不多,计算max_dpb_frames的时候考虑了max_num_ref_framesmax_dec_frame_buffering

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
bool H264Decoder::ProcessSPS(int sps_id, bool* need_new_buffers) {
  DVLOG(4) << "Processing SPS id:" << sps_id;

  const H264SPS* sps = parser_.GetSPS(sps_id);
  if (!sps)
    return false;

  *need_new_buffers = false;

  if (sps->frame_mbs_only_flag == 0) {
    DVLOG(1) << "frame_mbs_only_flag != 1 not supported";
    return false;
  }

  Size new_pic_size = sps->GetCodedSize().value_or(Size());
  if (new_pic_size.IsEmpty()) {
    DVLOG(1) << "Invalid picture size";
    return false;
  }

  int width_mb = new_pic_size.width() / 16;
  int height_mb = new_pic_size.height() / 16;

  // Verify that the values are not too large before multiplying.
  if (std::numeric_limits<int>::max() / width_mb < height_mb) {
    DVLOG(1) << "Picture size is too big: " << new_pic_size.ToString();
    return false;
  }

  int level = sps->level_idc;
  int max_dpb_mbs = LevelToMaxDpbMbs(level);
  if (max_dpb_mbs == 0)
    return false;

  // MaxDpbFrames from level limits per spec.
  size_t max_dpb_frames = std::min(max_dpb_mbs / (width_mb * height_mb),
                                   static_cast<int>(H264DPB::kDPBMaxSize));
  DVLOG(1) << "MaxDpbFrames: " << max_dpb_frames
           << ", max_num_ref_frames: " << sps->max_num_ref_frames
           << ", max_dec_frame_buffering: " << sps->max_dec_frame_buffering;

  // Set DPB size to at least the level limit, or what the stream requires.
  size_t max_dpb_size =
      std::max(static_cast<int>(max_dpb_frames),
               std::max(sps->max_num_ref_frames, sps->max_dec_frame_buffering));
  // Some non-conforming streams specify more frames are needed than the current
  // level limit. Allow this, but only up to the maximum number of reference
  // frames allowed per spec.
  DVLOG_IF(1, max_dpb_size > max_dpb_frames)
      << "Invalid stream, DPB size > MaxDpbFrames";
  if (max_dpb_size == 0 || max_dpb_size > H264DPB::kDPBMaxSize) {
    DVLOG(1) << "Invalid DPB size: " << max_dpb_size;
    return false;
  }

  if ((pic_size_ != new_pic_size) || (dpb_.max_num_pics() != max_dpb_size)) {
    if (!Flush())
      return false;
    DVLOG(1) << "Codec level: " << level << ", DPB size: " << max_dpb_size
             << ", Picture size: " << new_pic_size.ToString();
    *need_new_buffers = true;
    pic_size_ = new_pic_size;
    dpb_.set_max_num_pics(max_dpb_size);
  }

  Rect new_visible_rect = sps->GetVisibleRect().value_or(Rect());
  if (visible_rect_ != new_visible_rect) {
    DVLOG(2) << "New visible rect: " << new_visible_rect.ToString();
    visible_rect_ = new_visible_rect;
  }

  if (!UpdateMaxNumReorderFrames(sps))
    return false;
  DVLOG(1) << "max_num_reorder_frames: " << max_num_reorder_frames_;

  return true;
}

bool H264Decoder::UpdateMaxNumReorderFrames(const H264SPS* sps) {
  if (sps->vui_parameters_present_flag && sps->bitstream_restriction_flag) {
    max_num_reorder_frames_ =
        base::checked_cast<size_t>(sps->max_num_reorder_frames);
    if (max_num_reorder_frames_ > dpb_.max_num_pics()) {
      DVLOG(1)
          << "max_num_reorder_frames present, but larger than MaxDpbFrames ("
          << max_num_reorder_frames_ << " > " << dpb_.max_num_pics() << ")";
      max_num_reorder_frames_ = 0;
      return false;
    }
    return true;
  }

  // max_num_reorder_frames not present, infer from profile/constraints
  // (see VUI semantics in spec).
  if (sps->constraint_set3_flag) {
    switch (sps->profile_idc) {
      case 44:
      case 86:
      case 100:
      case 110:
      case 122:
      case 244:
        max_num_reorder_frames_ = 0;
        break;
      default:
        max_num_reorder_frames_ = dpb_.max_num_pics();
        break;
    }
  } else {
    max_num_reorder_frames_ = dpb_.max_num_pics();
  }

  return true;
}

sps.level 计算

vlcMediaCodec 都计算得出dpb.max_num_pics,拿这个值保底
gst-plugins-bad 只通过 level 计算,计算部分和 MediaCodec一样。

设置为4

iOS解码关于视频中带B帧排序问题

HEVC

vlc 中还有 HEVC(H265) 视频获取 max_num_reorder 的方式,代码文件在hevc_nal.c

FFmpeg

h264
h265

总结

  • ChromevlcMediaCodec的策略几乎一致,MediaCodec逻辑最完整。
  • vlc还处理了hevcmax_num_reorder

Link

ijkplayer
Chrome
vlc
Android
FFmpeg
iOS解码关于视频中带B帧排序问题

iOS NV12转SkImage颜色不正常的问题

| Comments

环境

设备:iPhone 6s
系统:13.1
Skia版本:m62
视频的YUV ColorSpace:ITU-R BT.601

现象

VideoToolbox 配置的 pixelFormat 是kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,然后把输出的 pixelBuffer 用下面的代码片段1转成 NV12,再使用代码片段2转成 SkImage,在 SkCanvas 上 draw 出来如图1,视频原图如图2。

1
uint32_t pixelFormatType = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;
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
// 代码片段1
// Y 数据
CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                             textCache,
                                             pixelBuffer,
                                             NULL,
                                             GL_TEXTURE_2D,
                                             GL_LUMINANCE,
                                             width,
                                             height,
                                             GL_LUMINANCE,
                                             GL_UNSIGNED_BYTE,
                                             0,
                                             &outputTextureLuma);
// UV 数据
CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                             textCache,
                                             pixelBuffer,
                                             NULL,
                                             GL_TEXTURE_2D,
                                             GL_LUMINANCE_ALPHA,
                                             width / 2,
                                             height / 2,
                                             GL_LUMINANCE_ALPHA,
                                             GL_UNSIGNED_BYTE,
                                             1,
                                             &outputTextureChroma);
1
2
3
4
5
6
7
8
9
10
11
12
13
// 代码片段2
GrGLTextureInfo textureInfo1 = {videoImage->textureTarget(), videoImage->getTextureID(0)};
GrGLTextureInfo textureInfo2 = {videoImage->textureTarget(), videoImage->getTextureID(1)};
GrBackendObject nv12TextureHandles[] = {reinterpret_cast<GrBackendObject>(&textureInfo1),
                                        reinterpret_cast<GrBackendObject>(&textureInfo2)};
SkISize nv12Sizes[] = \{\{videoImage->width(), videoImage->height()\},
                       \{videoImage->width(), videoImage->height()\}\};
skImage = SkImage::MakeFromNV12TexturesCopy(grContext,
                                            kRec601_SkYUVColorSpace,
                                            nv12TextureHandles,
                                            nv12Sizes,
                                            kTopLeft_GrSurfaceOrigin,
                                            nullptr);

图1 图2

查问题

1. 查视频的 YUV ColorSpace 是否和 SkImage 对应

是一致的,但输出的图像还是有问题。

2.试试把 VideoToolbox 的输出格式换成 RGBA

配置 VideoToolbox 的 pixelFormat 为 kCVPixelFormatType_32BGRA,使用代码片段3把 pixelBuffer 转成 RGBA 纹理,然后使用代码片段4转成 SkImage,图像是正常的。

1
uint32_t pixelFormatType = kCVPixelFormatType_32BGRA;
1
2
3
4
5
6
7
8
9
10
11
12
13
// 代码片段3
CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                             textCache,
                                             pixelBuffer,
                                             NULL,
                                             GL_TEXTURE_2D,
                                             GL_RGBA,
                                             width,
                                             height,
                                             GL_BGRA,
                                             GL_UNSIGNED_BYTE,
                                             0,
                                             &outputTextureLuma);
1
2
3
4
5
6
// 代码片段4
GrGLTextureInfo textureInfo = {videoImage->textureTarget(), videoImage->getTextureID(0)};
GrBackendTexture backendTexture(videoImage->width(), videoImage->height(), kRGBA_8888_GrPixelConfig,
                                textureInfo);
skImage =  SkImage::MakeFromTexture(grContext, backendTexture, kTopLeft_GrSurfaceOrigin,
                                    kPremul_SkAlphaType, nullptr);

3.查 Skia 源码

1
2
3
4
// SkImage_Gpu.cpp
// SkImage::MakeFromNV12TexturesCopy -> make_from_yuv_textures_copy
// GrYUVEffect.cpp
// GrYUVEffect::MakeYUVToRGB -> YUVtoRGBEffect::Make -> YUVtoRGBEffect() -> onCreateGLSLInstance() -> GLSLProcessor -> shader '.rg'

从 Skia 的源码中一直跟下去,发现最后 shader 使用的是 rg 通道,而因为我们是用 GL_LUMINANCE_ALPHA 来获取 UV 数据,在 GLSL 中应该使用 ra 通道,所以出现了不一致。当使用GL_RG获取UV数据的时候(代码片段5),SkImage 输出的图片就正常了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 代码片段5
// UV 数据
CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                             textCache,
                                             pixelBuffer,
                                             NULL,
                                             GL_TEXTURE_2D,
                                             GL_RG,
                                             width / 2,
                                             height / 2,
                                             GL_RG,
                                             GL_UNSIGNED_BYTE,
                                             1,
                                             &outputTextureChroma);

Link

skia
GL 移植到 Metal 的小细节

App 多区域皮肤(主题)的实现

| Comments

需求

App 里可以皮肤化的 UI 区域分为 3 块(App 皮肤/阅读器主题/其他),这 3 块又和黑夜模式有重叠。

  • App 皮肤区域有首页 4 个 tab bar 页面(还没有实现主题化,在规划中)
  • 阅读器主题区域主要是阅读器相关的设置和菜单页面
  • 其他指不需要主题化的区域

第一种方案

主要思路就是给 UIKit 的 Category 增加一些属性,配置每一类主题的 key,在第一次赋值或者收到主题改变的通知时进行 apply。

qd 是黑夜模式前缀,readerTheme 是阅读器主题前缀

这种方案的问题是 - 如果要增加一类主题(比如 App 皮肤类),需要把所有之前增加的属性都 copy 一下(包括 apply 部分的代码),工作量很大。 - 如果要增加一个新的属性,需要添加多遍(前缀分别是qd/readerTheme/app)。

重构之后的方案

借助 OC 的泛型和 KVC 的一次重构

重构前后对比

代码实现样例:LVThemeKit

iOS&Android 播放透明视频

| Comments

在 iOS & Android 上播放带 alpha 的 mp4 视频,用来代替序列帧动画(png/webp),素材小,播放也流畅。

用滤镜就可以完成,Demo 地址:
iOS
Android

如何使用 AE 制作带 alpha 的 mp4

原理:因为视频只有 rgb,没有 a,所以制作两个视频,第一个是原视频,第二个是纯黑白的边界视频,然后把两个视频合成一个,目的是保证帧对应。第一个视频的 rgb 是 0~1 的 float 值,第二个视频的 rgb 要么全是 1,要么全是 0。在客户端进行渲染的时候,写一个滤镜,目标像素 rgba 在取值的时候,rgb 使用第一个视频的 rgb,a 使用第二个视频的 r,就可以完成播放透明视频。

1
gl_FragColor = vec4(color1.rgb, color2.r);

导出迪斯尼乐拍通里的照片

| Comments

迪斯尼乐拍通App里面的照片导出收费,而且费用很高,我通过抓包拿到App里面的图片链接然后使用脚本下载解析得到高清的图片(非原图) 代码:Disneyphotopass-export

过程

  1. 抓包:使用Charles拿到某张图片的链接,下载下来之后发现是另外一个图片,如下图,这时候就蛋疼了,那我需要的那张图去哪了,而且这张图片的分辨率不高,按理说不会这么大,那么猜测我需要的那张图就藏在这返回的数据里面
    enter image description here
  2. 隐写术:之前不知道这个概念,所以我就进行了各种尝试去找
    • 是否是gif
    • 使用iOS ImageIO的API去看是不是在图片的exif里面
    • 然后问了伟哥,是否有方法把一张图片放在另外一张图片里,伟哥说“有好多图片隐写的方法你可以搜一下”
      算是找到了这种方案的一个术语-隐写,然后发现其中一种隐写方法的解析工具binwork,拿出之前下载的图片试一下,果然里面放了两张图片,如下图 enter image description here
  3. 解析:使用命令可以解出想要的图片dd if=image-download/1 bs=1 skip=10103 of=image/1.jpg enter image description here

脚本

然后把这个过程写成一个脚本,输入是一行一个链接的文本,输出是想要的图片,脚本来做下载和解析。

tokenId获取

脚本可以自动收集链接,需要抓包获取tokenId

  1. 使用浏览器(Chrome):登录网页版乐拍通,然后检查(Inspect)网络(Network)里找
  2. 乐拍通App抓包

链接的收集(Deprecated)

给手机设上代理,打开乐拍通,进入图片详情,然后一张一张滑过去,然后过滤一下,全选,右击,Copy URLs,然后粘在一个disneyphotopass文档里,执行脚本,就可以了。 enter image description here

iOS APP的简单Web Interface应用

| Comments

  最近接了一个内部工具开发的需求:运营人员需要生成商品尺码图片,之前他们是用PS做的,现在想让我们做个程序工具化这个过程。

刚拿到这个需求,分析之后,想了两个方案:

  1. APP生成图片,导出到电脑
  2. h5生成图片save到电脑
  3. 脚本生成,因为UI比较复杂,需要获取数据,所以排除

由于对APP开发比较熟悉,所以尝试着用客户端去做,生成图片不难,关键是如何导出到电脑上,当时的思路是

  1. iOS的话保存到相册用AirDrop去传
  2. Android保存到SD卡上,用PC上的软件导出
  3. 打成zip包,客户端起一个server,把zip download下来

本着操作简洁的原则,开始探索第三种方案。

一个程序,对用户来说只要有输入和输出就行了,中间不需要什么过多的介入,这才是优化流程的意义所在。

在这个例子中,输入是商品id数组,输出是一个zip包。

设计流程是:接收一个商品id数组,开始进行处理,最终输出一张图片save到Documents的一个文件夹下,然后进行zip打包,最后进行下载。

遇到的问题:

1.iOS的AutoLayout,图片的模板是用AutoLayout实现,变高(基于AutoLayoutviewContentSize概念)。问题就是截图的时候会报警告,然后出的图是空的,debug之后,发现是view赋值之后没有进行layout,所以size是CGSizeZero的,截图时得到的context是0x0,之前在项目中,没有出现此问题,是因为view一般都会有superview,而superview在layout的时候subview也会layout。这里的view没有superview,所以没人触发它的layout过程,加了两行代码,完成。

1
2
[self setNeedsUpdateConstraints];
[self layoutIfNeeded];

2.在提交商品id数组之后,如果处理过程是异步的,那么如何通知用户是个问题,本来打算是手机local notification通知,但是我们这个APP直接部署在局域网的Mac Pro的模拟器里面,不需要用户安装,而web页面做push操作比较麻烦,所以采用同步处理,也就是说,提交过商品id数组之后页面一直loading,直到处理完毕,给出下一个页面。在APP端,商品信息的加载和商品主图的加载也是异步的,那么怎么才能在一个请求中做同步呢,也就是收到web请求的时候,提交到一个manager中去处理这部分商品,但是这里sleep掉,等那边处理完毕之后,再唤醒接着返回response,于是就想到了iOS的Runloop这个机制。代码如下:

MyHTTPConnection.m

1
2
3
4
5
6
7
8
9
10
11
12
NSPort *port = [NSMachPort port];
[[NSRunLoop currentRunLoop] addPort:port forMode:NSDefaultRunLoopMode];
[ExportImageManager sharedInstance].thread = [NSThread currentThread];
[[ExportImageManager sharedInstance] createImageWithGoodsIds:goodsIdArr];
while (![ExportImageManager sharedInstance].completed) {
    NSLog(@"runloop start......");
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    NSLog(@"runloop end......");
}
[[NSRunLoop currentRunLoop] removePort:port forMode:NSDefaultRunLoopMode];
CFRunLoopStop(CFRunLoopGetCurrent());
[ExportImageManager sharedInstance].thread = nil;

注解: 这块代码执行的环境是在HTTPServer的队列里面,所以在哪个线程中是未知的,线程中的runloop默认是不开启的,所以在这里,做了几件事

  1. 声明一个port放在runloop中,防止runloop中没有任何输入源会直接退出
  2. 把当前所在的thread赋给manager,当处理完成的时候,在这个thread上performSelector,唤醒runloop
  3. 在while循环中启动runloop
  4. 把之前的port remove掉
  5. 把runloop停掉
  6. 之前引用的thread清空

最终实现,3个操作步骤

  1. localhost:8081/create?id,id,id,id 图片生成请求,response返回的时候处理结束,会在Documents/export/的文件夹下生成以商品id命名的图片
  2. localhost:8081/zipDocuments/export打包成Documents/export.zip 并删除Documents/export
  3. localhost:8081/export.zip 下载zip

其实可以把打包操作合并在第一步中。缩减为两步

思路来源是,QQ阅读传pdf的时候是在电脑上打开一个网页,然后把文件拖进去就可以同步到手机。实现之后发现,这个功能和CharlesWeb Interface功能一样。。

项目地址:https://github.com/lvpengwei/ExportGoodsImage

Simulator Screen Shot Jun 13, 2016, 17.00.33.png

Android坐标系学习案例(与iOS进行对比)

| Comments

问题: 判断parent view中的手势是否出现在旋转之后的subview的范围中的解决方法(iOS&Android)

解决思路: 取出点击位置的point, 然后translate到subview的坐标系中, 判断是否在subview的矩形中即可.

iOS:

现象:

7DBEE162-CB37-44E7-8560-4E50556532DA.png

代码:

0FDD8A27-5C7F-48EB-A0DD-853FDA4B846C.png

结果:

78067F60-E64B-4FFB-AA67-68783D824B32.png

部分解释: iOS中的view有frame/bounds/center/transform等坐标属性,

四者的区别(具体可查看文档, 非常详细)

  • frame: 相对于parent view的坐标系
  • bounds: 相对于自己的坐标系
  • center: view的中心点(跟layer的anchorPoint有关, anchorPoint默认值是(0.5, 0.5), 所以是中心点)
  • transform: 相对于center的缩放/旋转等二维操作. layer中有个transform是三维的.

三者之间的关系是-bounds/center/transform会影响frame的值.

大部分情况, 我们使用的frame(x, y, width, height)只影响到了boundscenter, 此时transform是初始值CGAffineTransformIdentity; 而当我们要设置transform的时候, frame的值就会变得很奇怪. 其实frame还是那个矩形, 不过它代表的含义是能包含这个view的最小矩形而已. 因为transform涉及到scale/rotation等操作, 所以frame看起来和我们真正想要的值不太一样, 而这时候就是需要分开使用center/bounds/transform的时候, 不能单独设置frame.

layer的相关属性不介绍, 有兴趣可以直接查看API文档.

Android:

类似的, 我开始在Android中寻找相应的解决方案. 首先需要明白的几个点

  • Android的每一个view也有自己的坐标系, 左上角是原点(0, 0)
  • Android的view的基础坐标属性是left/top/right/bottom. width和height是由前四个值推算得来.
  • Android的view的matrix作用对应于iOS的view的transform.

解决思路还是上面所述, 但是取出点击的point容易, translate到subview的坐标系中遇到了一些问题, iOS中此api存在于UIView中, 而Android的View却没有. 搜索一圈, 还是在Android的事件分发的源码中获取答案. 现象:

A5809894-B554-433E-B603-172DC01AEB93.png

代码:

B7B8A792-4D22-4EDF-B672-A96C1A5FCCA4.png

布局代码:

007A1AED-36E2-4A51-B5CA-7970D56DC40C.png

结果:

2C8C9B9F-FD20-4986-BE8B-BAAA346943FD.png

部分解释: 代码部分借鉴了Android源码.

069605CE-BF37-4BDA-B054-0514E4090EA4.png

  1. 先deep copy出一份新的event.
  2. 设置event的offsetLocation.
  3. 给event应用subview的inverse matrix.
  4. 还有很多类似对应的属性(后边继续学习, 本次没有用到)

然后event就被成功的转换到旋转后的view的坐标系中(斜着的), 原点是左上角.

遇到的问题:

  • ev.offsetLocation: 因为我是在activity中override onTouchEvent, 所以在计算deltaX和deltaY的时候先减去了relativeLayout距离window的x和y, 再减去了demoTextView的left和top. 1.为什么不直接减去demoTextView距离window的x和y? 因为我发现, demoTextView被旋转之后, 它的left/top/right/bottom并没有改变, 从现象的截图里我们也可以看出. 而demoTextView.getLocationInWindow获取的point却是旋转之后的值, 从log中可以看出. 2.为什么不减去relativeLayout的left和top, 而是减去距离window的x和y? 因为relativeLayout只是activity的contentView, 外层还有多少ViewGroup是未知, 所以直接减去距离window的x和y.
  • ev.transform: 源码里面的写法是transformedEvent.transform(child.getInverseMatrix());, 而我自己去调用demoTextView.getInverseMatrix()的时候, 一直报错, 而且API文档中并没有这个方法, 所以就直接去Matrix的类里面去搜关键词inverse, 发现有这样的方法可以使用.

Charles+Surge解决抓包和翻墙的冲突问题

| Comments

平常做iOS开发的时候经常使用ShadowsocksX来翻墙查资料, 使用Charles来抓包debug. 但是两个软件不能同时开, 一直想不到什么好的解决办法.

Surge特点是: 支持翻墙, 但是抓包的request和response不够详细. Mac版的Surge还要自己配置Web Proxy(HTTP)Secure Web Proxy(HTTPS).

然后我就查了一下Charles的菜单, 发现了External Proxy Setting, 然后发现刚好支持Web Proxy(HTTP)Secure Web Proxy(HTTPS), 配好调试, 果然成功. 然后手机上再设置成CharlesWeb Proxy(HTTP), 也是可以抓包和翻墙的.

Charles特点是: 抓包的request和response很详细, 但是External Proxy Setting支持的protocol比较少.

CharlesSurge结合起来就很完美的解决了我这个问题.

再简化一步, 就直接用手机上的Surge生成一个Web Proxy(HTTP)的config, 这样就不用每次去系统设置里手动设置了(每次敲好麻烦), 这样每次进入公司, 电脑上开着Charles和Mac版的Surge, 手机上起着Surge, 两个设备都可以被抓包和翻墙, 妈妈再也不用担心我在ShadowsocksXCharles这两个软件之间来回切换了.

附几张比较重要的图: CharlesExternal Proxy Setting 51F279D2-6924-4BB8-A230-48C693F2CE96.png

手机上的Surge的Web Proxy(HTTP)的config IMG_1261.png