Jacky Walker

用于记录工作和学习的博客


  • 首页

  • Objective-C

  • Cocoa/UIKit

  • net

  • OpenGL

  • 杂项

  • 设计模式

  • 分类

  • 标签

  • 归档

iOS富文本处理采坑记录(二)

发表于 2020-07-02 | 分类于 UIKit | | 阅读次数:

前言

系列文章

遍历字符串文字不连续

当从UITextView读取attributedText后,如果执行enumerateAttributes方法遍历,会出现unicode、中英文字符分开获取到的情况

示例

1
2
3
4
5
6
7
8
let attributeString = NSMutableAttributedString(string: "🔴🔴哈哈哈哈哈哈www.baidu.com", attributes: [.font : UIFont.systemFont(ofSize: 14)])
textView.attributedText = attributeString

if let amendRange = textView.attributedText.string.amendRange() {
textView.attributedText.enumerateAttributes(in: amendRange, options: []) { (_, range, stop) in
print(textView.attributedText.attributedSubstring(from: range))
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
🔴🔴{
NSFont = "<UICTFont: 0x7f9c90f03ff0> font-family: \".AppleColorEmojiUI\"; font-weight: normal; font-style: normal; font-size: 14.00pt";
NSOriginalFont = "<UICTFont: 0x7f9c90d08fd0> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 14.00pt";
}
哈哈哈哈哈哈{
NSFont = "<UICTFont: 0x7f9c90e03610> font-family: \".PingFangSC-Regular\"; font-weight: normal; font-style: normal; font-size: 14.00pt";
NSOriginalFont = "<UICTFont: 0x7f9c90d08fd0> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 14.00pt";
}
www.baidu.com{
NSFont = "<UICTFont: 0x7f9c90d08fd0> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; font-size: 14.00pt";
}

由运行结果可以看出,在我们进行遍历时,获取到的range是按照Unicode、中英文来断开的,正常情况这是没有什么影响的,但是对于文本编辑来说,因为有链接、普通文字、at等情况,所以需要将enumerate中属性相同的string拼接到一起。大概思路就是利用attribute来判断substring是否属于同一数据段,具体做法请看接下来介绍的富文本的选择、删除处理。

富文本选择、删除处理

需求介绍

在富文本编辑中,经常做的是链接、@信息、普通文本等编辑,这些类型当中,通常只有普通文本支持单字选择与删除,而链接与@信息的话一般是只能进行整体删除和选择(不能从segment中间开始选择)。

上文介绍到,当我们遍历从UITextView中获取到的富文本时,获取到的字符串并不是连续的,但是呢,我们的需求需要将相邻的相同的数据合并到一起。那我们根据什么条件来判断是否相邻的字符串是否是相同segment呢?有以下两个条件:

  1. range连续
  2. attribute相同

只要同时满足上诉两个条件,就可以认为是属于相同segment,应该合并到一起。

选择

效果:

TextEditExample

先上选择的代码:

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
func fixSelectRange(_ attributeString: NSAttributedString, targetRange: NSRange) -> NSRange {
var fixRange = targetRange

guard let range = attributeString.string.amendRange() else {
return fixRange
}

var prevAttribute: AttributeInfoProtocol!

attributeString.enumerateAttributes(in: range, options: []) { (attributes, atRange, stop) in
guard let attribute = getAttributeInfo(attributes) else {
return
}

let shoudStop = prevAttribute != nil && !prevAttribute.isEqual(attribute)
if shoudStop {
stop.pointee = true
return
}

if fixRange.location >= atRange.location && fixRange.location + fixRange.length <= atRange.location + atRange.length {
if attribute.type == .text {
fixRange = targetRange
stop.pointee = true
return
}

fixRange.location = atRange.location + atRange.length
fixRange.length = 0
prevAttribute = attribute
return
}
}
return fixRange
}

逻辑其实挺简单的,如果fixRange的location在获取atRange中,即:

1
fixRange.location >= atRange.location && fixRange.location + fixRange.length <= atRange.location + atRange.length

如果当前的attribute是text的话,就不需要进行修改

1
2
3
4
5
if attribute.type == .text {
fixRange = targetRange
stop.pointee = true
return
}

如果不是文本的话,那么退出条件为:

1
prevAttribute != nil && !prevAttribute.isEqual(attribute)

删除

删除与选择不同,选择的话只需要将location移动到segment前即可,删除的话 ,需要将整个segment删掉

效果

textDeleteExample

代码
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
func fixDeleteRange(_ attributeString: NSAttributedString, targetRange: NSRange) -> NSRange {
var currentMaxRange: NSRange?
guard let range = attributeString.string.amendRange() else {
return targetRange
}

var prevAttribute: AttributeInfoProtocol?
var didFound = false

attributeString.enumerateAttributes(in: range, options: [.reverse]) { (attributes, atRange, stop) in
print(attributeString.attributedSubstring(from: atRange).string)
guard let attribute = getAttributeInfo(attributes) else {
return
}

if let prevAttr = prevAttribute, prevAttr.isEqual(attribute) {
currentMaxRange?.concatenateRange(atRange)
} else {
if let maxRange = currentMaxRange, targetRange.location >= maxRange.location && targetRange.location <= maxRange.location + maxRange.length {
stop.pointee = true
didFound = true
return
}
currentMaxRange = atRange
prevAttribute = attribute
}
}

if didFound == false, let maxRange = currentMaxRange, targetRange.location >= maxRange.location && targetRange.location <= maxRange.location + maxRange.length {//处理删除目标在第一个info中的情况
didFound = true
}

if didFound {
if prevAttribute?.type == .text {
return targetRange
}
return currentMaxRange ?? targetRange
}
return targetRange
}

extension NSRange {
func isContinuous(_ range: NSRange) -> Bool{
return self.location + self.length == range.location
}

/// right concatenate left eg: self:(1, 3) concatenate left:(0, 1) -> (0, 4)
/// - Parameter left: left range
mutating func concatenateRange(_ left: NSRange) {
if left.location + left.length > location + length {
return
}

if left.location + left.length < location {
return
}

length = location + length - left.location
self.location = left.location
// length = (rightMax - leftMax) + (leftMax - rightMin) + (rightMin - leftMin)
}
}
原理:

逆序遍历attributes,将连续的attributes的range拼接起来,如果targetRange在currentMaxRange中并且不是text类型时,应该删除maxRange,否则直接删除targetRange

结语

以上就是富文本的选择删除处理了,你可以在这里下载demo,如果觉得对你有帮助的话,可以点一下star哦,thks

iOS富文本处理踩坑记录(一)

发表于 2020-06-20 | 分类于 UIKit | | 阅读次数:

前言

最近在做基于UITextView的富文本编辑功能,遇到了不少的坑,总感觉起来呢有以下几点:

  • 当字符串中含有Unicode编码时,直接获取的range不对
  • 在中英文混合的情况下,遍历字符串会不连续
  • 富文本中的选择、删除处理
  • 富文本中的链接点击处理

正文

range不对

场景

在富文本中有时候需要高亮文本,但是遇到了文本只有部分高亮的问题,代码如下:

1
2
3
4
5
let string = "🔴1🔴2🔴3内部文档1"
let attributeString = NSMutableAttributedString(string: string)
let range = NSRange(location: 0, length: string.count)
attributeString.addAttribute(.foregroundColor, value: UIColor.blue, range: range)
textView.attributedText = attributeString

效果:

上面的代码看起来是没有问题的,既然颜色值没有全部生效,那么问题一个是出在了rang上。通过查资料发现,在swift中使用了扩展字型集群,所以swift中会对可以组合在一起的两个字符,以下为示例:

1
2
3
4
5
6
7
let precomposed: Character = "\u{D55C}" // 한
let characters = ["\u{1112}", "\u{1161}", "\u{11AB}"] // ᄒ, ᅡ, ᆫ
var string = String()
for character in characters {
string.append(character)
print("string:\(string) count:\(string.count)")
}

运行结果:

1
2
3
string:ᄒ count:1
string:하 count:1
string:한 count:1

可以看出虽然上面的string已经append了三个character,但是count任然是1。那么我们应该怎么样做呢?主要有以下两种办法:

  1. string as NSString
  2. range convert to NSRange

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let string = "🔴1🔴2🔴3内部文档1"
//method 1 string as NSString
print("NSString length: \((string as NSString).length)")
print("String length: \(string.count)")

let wrongRange = NSRange(location: 0, length: string.count)
//method 2 range convert to NSRange
let r1 = string.range(of: string)
var correctRange: NSRange?
if let tR1 = r1 {
correctRange = NSRange(tR1, in: string)
}
print("wrongRange\(wrongRange)")
print("correctRange: \(String(describing: correctRange))")

运行结果:

1
2
3
4
NSString length: 14
String length: 11
wrongRange{0, 11}
correctRange: Optional({0, 14})

参考资料

https://stackoverflow.com/questions/25138339/nsrange-to-rangestring-index/30404532#30404532

https://stackoverflow.com/questions/42114872/nsattributedstring-and-emojis-issue-with-positions-and-lengths

GoogleTest使用

发表于 2019-12-08 | 分类于 sundry | | 阅读次数:

前言

因为项目是跨平台(Mac与Win)的,且是用C++编写的,所以在单元测试方面能用的框架就不多了。公司里其他项目组都是用的GTest来写单元测试的,所以呢,我们也觉得使用GTest与GMock。本文主要是讲在Mac环境下,对GTest的环境搭建及使用

正文

GTest与GMock介绍

GTest月GMock都是Google的开源C++测试框架,提供了一系列的断言与期望的宏,同时还提供了stub与mock的功能,这些都是单元测试不可或缺的。你可以在这里查看GTest的GitHub主页。

环境搭建

源码下载

首先需要在googletest下载最新的release源码

源码编译

解压后,会看到如下的目录结构:

因为官方提供了两种编译库的方式,一种的CMake,另外一种是Bazel。这里介绍用CMake的方式,所以你需要先安装CMake需要的环境,这里就不介绍安装方式了,请自行查询。CMake的使用方式可以参考IBM的这篇文章。

CMake的环境准备好后,从命令行进入到该目录,并在命令行分别执行以下命令:

1
2
3
$ mkdir mybuild
$ cd mybuild
$ cmake ..

执行成功后,应该是下面这个样子:

接着执行以下指令:

1
make

执行成功后就生成了对应的.a文件了

image-20191208161235027

在finder中可以看到新生成的文件

可以看到,一共生成了四个.a文件,其中有*_main.a表示文件中包含了main函数,所以使用此类文件时,工程里不能有其他的main函数。

工程创建,配置环境

在这里示例创建一个名为TestGoogletest的命令行工程。

接着导入编译好的.a文件,在这里我就不适用带有main函数的.a文件了。

直接将.a文件拖入工程

导入头文件

打开finder,进入到源码目录,将googlemck与googletest目录下的include目录下的文件夹gmock与gtest加入到工程中

配置头文件搜索路径

导入头文件后需要配置头文件搜索路径,打开工程文件,选择build setting,设置user header search path。

至此gtest与gmock框架添加完毕,接下来就是编写测试用例了

测试用例

main函数编写如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include "gtest/gtest.h"
#include <numeric>
#include <vector>

int main(int argc, const char * argv[]) {
::testing::InitGoogleTest(&argc, (char **)argv);
return RUN_ALL_TESTS();
}

TEST(TestSuiteName, TestName) {
EXPECT_EQ(1, 1);
}

接下来解释代码,GTest需要在main函数中启动,启动方式如下:需要注意的是,在mac平台下的main函数默认参数类型是==const char ==而gtest接收的类型是==char *==,所以需要做一个强制类型转换。同时return函数返回的是==RUN_ALL_TESTS==(),作用是执行所有的单元测试

1
2
3
4
int main(int argc, const char * argv[]) {
::testing::InitGoogleTest(&argc, (char **)argv);
return RUN_ALL_TESTS();
}

TEST是GTest提供的单元测试宏,第一个参数是模块名,第二个参数是测试名称

1
2
3
TEST(TestSuiteName, TestName) {
EXPECT_EQ(1, 1);
}

同时,GTest还提供了测试宏TEST_F。

以上就是googleTest的简单使用了,你可以在这里下载demo

要了解更多的话可以参考以下链接:

Googletest Primer

Googletest README

观察者模式

发表于 2019-10-29 | 分类于 DesignPattern | | 阅读次数:

观察者模式

定义

观察者模式定义了对象之间的一对多依赖,当一个对象改变状态时,它的所有依赖者都会受到通知并自动更新。

示例

有这样一个场景,侦察兵(Scout)负责侦查附近有没有敌人,如果发现敌人的话,侦察兵就需要通知将军(General)与士兵(Soldier)。当士兵不在需要知道附近是否有敌人时,侦察兵就只需要通知将军。如果参谋长也需要知道侦查结果的话,同样的需要通知参谋长(Gigas)。这就是典型的一对多并且依赖状态的情况,让我们利用观察者模式来实现吧。

设计

首先我们需要设计一个主题接口(Subject),这个接口定义对观察者的添加、删除、通知方法。Scout类实现Subject接口来完成可观察的功能。然后定义Observer接口,该接口定义update方法,用于接收通知,士兵、参谋长、将军分别实现此接口。类图如下:

代码

Subject.java

1
2
3
4
5
public interface Subject {
public void registerObserver(Observer o);
public void removeObserver(Observer o);
public void notifyObservers();
}

Scout.java

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
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Scout implements Subject{
private List<Observer> observers = new ArrayList<>();

@Override
public void registerObserver(Observer o) {
observers.add(o);
}

@Override
public void removeObserver(Observer o) {
if(observers.contains(o)) {
observers.remove(o);
}
}

@Override
public void notifyObservers() {
Iterator<Observer> iterable = observers.iterator();
while (iterable.hasNext()) {
Observer o = iterable.next();
o.update("The enemy is coming");
}
}
}

Observer.java

1
2
3
public interface Observer {
public void update(String msg);
}

General.java

1
2
3
4
5
6
public class General implements Observer {
@Override
public void update(String msg) {
System.out.println("General receive : " + msg);
}
}

Gigas.java

1
2
3
4
5
6
public class Gigas implements Observer {
@Override
public void update(String msg) {
System.out.println("Gigas receive : " + msg);
}
}

Soldier.java

1
2
3
4
5
6
public class Soldier implements Observer {
@Override
public void update(String msg) {
System.out.println("Soldier receive : " + msg);
}
}

测试代码

main.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {

public static void main(String[] args) {
Subject subject = new Scout();
Observer o1 = new General();
Observer o2 = new Soldier();

subject.registerObserver(o1);
subject.registerObserver(o2);
subject.notifyObservers();

System.out.println("-----register Gigas-----");
Observer o3 = new Gigas();
subject.registerObserver(o3);
subject.notifyObservers();

System.out.println("-----remove soldier-----");
subject.removeObserver(o2);
subject.notifyObservers();

}
}

运行结果

总结

观察者模式遵循了针对接口编程,观察者与主题都是接口,主题通过接口通知观察者,观察者通过接口向主题注册,保证了松耦合。同时观察者模式将变化的部分(Observer的注册与移除)抽离出来,有着良好的扩展性与维护性。

面向对象设计原则

发表于 2019-10-28 | 分类于 DesignPattern | | 阅读次数:

OO基础:

  • 抽象
  • 封装
  • 继承
  • 多态

七大原则:

  1. 单一职责原则(Single Responsibility Principle)

    每一个类应该专注于做一件事情。

  2. 里氏替换原则(Liskov Substitution Principle)

    超类存在的地方,子类是可以替换的。

  3. 依赖倒置原则(Dependence Inversion Principle)

    实现尽量依赖抽象,不依赖具体实现。

  4. 接口隔离原则(Interface Segregation Principle)

    应当为客户端提供尽可能小的单独的接口,而不是提供大的总的接口。

  5. 迪米特法则(Law Of Demeter)

    又叫最少知识原则,一个软件实体应当尽可能少的与其他实体发生相互作用。

  6. 开闭原则(Open Close Principle)

    面向扩展开放,面向修改关闭。

  7. 组合/聚合复用原则(Composite/Aggregate Reuse Principle CARP)

    尽量使用合成/聚合达到复用,尽量少用继承。原则: 一个类中有另一个类的对象。

总结

面向对象设计原则的目的是为了提高程序的可扩展性、可维护性以及可扩展性。简要的做法可以总结为一下三点:

  1. 抽取可变化的部分并封装起来,让其他部分不受影响
  2. 针对接口编程,而不是针对实现编程
  3. 多用组合,少用继承
  4. 为了交互对象之间的松耦合而努力

Mac 设置socket5代理

发表于 2019-10-23 | 分类于 sundry | | 阅读次数:

Mac 设置socket5代理

Mac端可以在设置->网络->高级->代理中设置http代理与socket代理,但是在这里设置的代理都是http的,并不能直接设置socket的代理。如果想要设置socket的代理的话,需要通过其他的软件来使用。在这里介绍利用==proxifier==来实现socket的代理。

步骤如下:点击proxies,进入proxy列表

点击add进入到添加界面

在添加界面输入代理服务器的address与port,并且选择协议类型为http5,点击ok

这就设置好socket5的代理了。

iOS RGBA转YV12

发表于 2019-10-16 | 分类于 sundry | | 阅读次数:

引言

因为项目中要做画面共享,所以需要学一点图像相关的知识,首当其冲就是RGB转YUV了,因为图像处理压缩这一块是由专业对口的同事做的,所以呢,我这就是写一下自己的理解,如有不对的地方,还望指正,谢谢。

正文

知识准备

RGB

三原色光模式(RGB color model),又称RGB颜色模型或红绿蓝颜色模型,是一种加色模型,将红(Red)、绿(Green)、蓝(Blue)三原色的色光以不同的比例相加,以合成产生各种色彩光。

RGB32

RGB32使用32位来表示一个像素,RGB分量各用去8位,剩下的8位用作Alpha通道或者不用。(ARGB32就是带Alpha通道的RGB24。)注意在内存中RGB各分量的排列顺序为:BGRA BGRA BGRA…。通常可以使用RGBQUAD数据结构来操作一个像素,它的定义为:

1
2
3
4
5
6
typedef struct tagRGBQUAD {
BYTE rgbBlue; // 蓝色分量
BYTE rgbGreen; // 绿色分量
BYTE rgbRed; // 红色分量
BYTE rgbReserved; // 保留字节(用作Alpha通道或忽略)
} RGBQUAD。

YUV

YUV,是一种颜色编码方法。常使用在各个影像处理组件中。 YUV在对照片或影片编码时,考虑到人类的感知能力,允许降低色度的带宽。

YUV是编译true-color颜色空间(color space)的种类,Y’UV, YUV, YCbCr,YPbPr等专有名词都可以称为YUV,彼此有重叠。“Y”表示明亮度(Luminance、Luma),“U”和“V”则是色度、浓度(Chrominance、Chroma)。

YUV Formats分成两个格式:

  • 紧缩格式(packed formats):将Y、U、V值存储成Macro Pixels数组,和RGB的存放方式类似。
  • 平面格式(planar formats):将Y、U、V的三个分量分别存放在不同的矩阵中。

YV12

YV12是每个像素都提取Y,在UV提取时,将图像2 x 2的矩阵,每个矩阵提取一个U和一个V。YV12格式和I420格式的不同处在V平面和U平面的位置不同。在YV12格式中,V平面紧跟在Y平面之后,然后才是U平面(即:YVU);但I420则是相反(即:YUV)。NV12与YV12类似,效果一样,YV12中U和V是连续排列的,而在NV12中,U和V就交错排列的。

排列举例: 2*2图像YYYYVU; 4*4图像YYYYYYYYYYYYYYYYVVVVUUUU。

ps:以上介绍摘自于维基百科、百度百科。

进入正题

获取RGBA数据

在这里主要介绍从Image中获取RGBA数据,会用到CoreGraphics库。

首先我们需要创建bitmap context

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
+ (CGContextRef) newBitmapRGBA8ContextFromImage:(CGImageRef) image {
CGContextRef context = NULL;
CGColorSpaceRef colorSpace;
uint32_t *bitmapData;

size_t bitsPerPixel = 32; //每一个像素 由4个通道构成(RGBA),每一个通道都是1个byte,4个通道也就是32个bit
size_t bitsPerComponent = 8; //可以理解为每个通道的bit数
size_t bytesPerPixel = bitsPerPixel / bitsPerComponent; //每个像素点的byte大小

size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);

size_t bytesPerRow = width * bytesPerPixel; //每一行的字节数
size_t bufferLength = bytesPerRow * height; //整个buffer的size

colorSpace = CGColorSpaceCreateDeviceRGB(); //指定颜色空间为RGB

if(!colorSpace) {
NSLog(@"Error allocating color space RGB\n");
return NULL;
}

// 开辟存储位图的内存
bitmapData = (uint32_t *)malloc(bufferLength);

if(!bitmapData) {
NSLog(@"Error allocating memory for bitmap\n");
CGColorSpaceRelease(colorSpace);
return NULL;
}

// 创建bitmap context
context = CGBitmapContextCreate(bitmapData,
width,
height,
bitsPerComponent,
bytesPerRow,
colorSpace,
kCGImageAlphaPremultipliedLast); // RGBA

if(!context) {
free(bitmapData);
NSLog(@"Bitmap context not created");
}

CGColorSpaceRelease(colorSpace);
return context;
}

接下来需要向image绘制到bitmap context,然后从context中获取bitmap data。代码如下:

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
+ (unsigned char *) convertUIImageToBitmapRGBA8:(UIImage *) image {

CGImageRef imageRef = image.CGImage;

// 创建bitmap context
CGContextRef context = [self newBitmapRGBA8ContextFromImage:imageRef];

if(!context) {
return NULL;
}

size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);

CGRect rect = CGRectMake(0, 0, width, height);

// 将image绘制到bitmap context
CGContextDrawImage(context, rect, imageRef);

// 获取bitmap context 中的数据指针
unsigned char *bitmapData = (unsigned char *)CGBitmapContextGetData(context);

// 拷贝 bitmap text中的数据
size_t bytesPerRow = CGBitmapContextGetBytesPerRow(context);
size_t bufferLength = bytesPerRow * height;

unsigned char *newBitmap = NULL;

if(bitmapData) {
newBitmap = (unsigned char *)malloc(sizeof(unsigned char) * bytesPerRow * height);

if(newBitmap) { // 拷贝数据
memcpy(newBitmap, bitmapData, bufferLength);
}

free(bitmapData);

} else {
NSLog(@"Error getting bitmap pixel data\n");
}

CGContextRelease(context);

return newBitmap;
}

看了这段代码,你可能会有疑问,为什么不直接返回bitmapData呢?这是因为在我们释放bitmap context后,会释放掉bitmapData,所以这就需要我们从新申请空间将数据拷贝到重新开辟的空间了。

格式化图像数据

因为YV12要求以像素的2 * 2矩阵来做转换,所以在做RGB转换YV12之前,我们需要先格式化图像数据,以满足要求。

首先需要格式化图片size,在这里我们以8来对齐,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (CGSize)fromatImageSizeToYV12Size:(CGSize)originalSize
{
CGSize targetSize = CGSizeMake(originalSize.width, originalSize.height);

//除以8是为了位对齐
int widthRemainder = (int)originalSize.width % 8;

if (widthRemainder != 0) {
targetSize.width -= widthRemainder;
}

int heightRemainder = (int)originalSize.height % 8;
if (heightRemainder != 0) {
targetSize.height -= heightRemainder;
}

return targetSize;
}

在上个步骤我们计算得到了满足YV12格式的size,在这里还需要根据计算到的size来格式化image data,以满足YV12格式要求,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (void)formatImageDataToYV12:(unsigned char *)originalData outputData:(unsigned char *)outputData originalWidth:(CGFloat)originalWidth targetSize:(CGSize)targetSize
{
CGFloat targetHeight = targetSize.height;
CGFloat targetWidth = targetSize.width;

unsigned char *pTemp = outputData;

//按照targetSize逐行拷贝数据, 乘以4是因为每个size表示4个通道
for (int i = 0; i < targetHeight; i++) {
memcpy(pTemp, originalData, targetWidth * 4);
originalData += (int)originalWidth * 4;
pTemp += (int)targetWidth * 4;
}
}

数据准备好了,接下来,可以进入真正的主题,RGBA转换为YV12了。

RGBA转YV12

对于RGB转换为对应的YV12,转换规则都是大佬们研究出来的,只是实现的方式各有不同,我在这里罗列了我找到的几种方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void rgb2yuv(int r, int g, int b, int *y, int *u, int *v){
// 1 常规转换标准 - 浮点运算,精度高
*y = 0.29882 * r + 0.58681 * g + 0.114363 * b;
*u = -0.172485 * r - 0.338718 * g + 0.511207 * b;
*v = 0.51155 * r - 0.42811 * g - 0.08343 * b;

// 2 常规转换标准 通过位移来避免浮点运算,精度低
*y = ( 76 * r + 150 * g + 29 * b)>>8;
*u = (-44 * r - 87 * g + 131 * b)>>8;
*v = ( 131 * r - 110 * g - 21 * b)>>8;
// 3 常规转换标准 通过位移来避免乘法运算,精度低
*y = ( (r<<6) + (r<<3) + (r<<2) + (g<<7) + (g<<4) + (g<<2) + (g<<1) + (b<<4) + (b<<3) + (b<<2) + b)>>8;
*u = (-(r<<5) - (r<<3) - (r<<2) - (g<<6) - (g<<4) - (g<<2) - (g<<1) - g + (b<<7) + (b<<1) + b)>>8;
*v = ((r<<7) + (r<<1) + r - (g<<6) - (g<<5) - (g<<3) - (g<<2) - (g<<1) - (b<<4) - (b<<2) - b)>>8;

// 4 高清电视标准:BT.709 常规方法:浮点运算,精度高
*y = 0.2126 * r + 0.7152 * g + 0.0722 * b;
*u = -0.09991 * r - 0.33609 * g + 0.436 * b;
*v = 0.615 * r - 0.55861 * g - 0.05639 * b;

*v += 128;
*u += 128;
}

以上的转换方式都是可行的,当然要提升效率的话,还有查表法什么的,这些有兴趣的可以自行搜索。既然转换规则固定了,那么不考虑效率的前提下,我们需要做的就是如何从rgba数组中按照2 * 2矩阵来获取YUV数据并存储下来了。

在这里介绍采用紧缩格式存储YV12的数据。为了便于理解,在这里先举个栗子:

下图中的表示一张图中的像素排列,每个像素都包含RGBA通道,将RGBA转换为YV12需要按照2 * 2的像素矩阵为单位来处理,在图中就是按颜色分块中的像素来获取YUV数据,YV12是每4个像素获取一次U、V分量的数据,每个像素都要获取Y分量。

要看转换后的YUV的样子,这里以2 * 2为例,8 个像素生成的YUV如下:

1
YYYY YYYY VV UU

有了上面的知识,就可以直接上代码看看了。在这里我们以数组的方式来存储yuv数据,输入的rgba数据我们设置为int型数组,因为每个像素包含4个通道,每个通道都是一个byte,这样每个像素是4个byte,正好是一个int。需要注意的是,在int数组中,按理说每个int的数据应该是:RGBA,但是在iOS上,由于iOS是小端,所以实际上每个int的内容为ABGR,所以在取数据的时候需要注意,避免弄错顺序。还有需要注意的是,计算出来的UV数据需要避免负数,不然颜色值会有问题。

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
void rgbaConvert2YV12(int *rgbData, uint8_t *yuv, int width, int height) {
int frameSize = width * height;
int yIndex = 0;
int vIndex = frameSize;
int uIndex = frameSize * 1.25;

int R, G, B, Y, U, V, A;
int index = 0;
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {//RGBA
//样式为ABGR iOS设备为小端
A = (rgbData[index] >> 24) & 0xff;
B = (rgbData[index] >> 16) & 0xff;
G = (rgbData[index] >> 8) & 0xff;
R = rgbData[index] & 0xff;

//转换
rgb2yuv(R, G, B, &Y, &U, &V);
//避免负数
U += 128;
V += 128;

Y = RANG_CONTROL(Y, 0, 255);
U = RANG_CONTROL(U, 0, 255);
V = RANG_CONTROL(V, 0, 255);

yuv[yIndex++] = Y;

if (j % 2 == 0 && i % 2 == 0) {//按2 * 2矩阵取
yuv[vIndex++] = V;
yuv[uIndex++] = U;
}
index ++;
}
}
}

YV12转RGBA

首先还是yuv转rgb的方法,这与RGB转YV12是对应的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void yuv2rgb(int y, int u, int v, int *r, int *g, int *b){
u -= 128;
v -= 128;
// 1 常规转换标准 - 浮点运算,精度高
*r = y + (1.370705 * v);
*g = y - (0.337633 * u) - (0.698001 * v);
*b = y + (1.732446 * u);

// 2 常规转换标准 通过位移来避免浮点运算,精度低
*r = ((256 * y + (351 * v))>>8);
*g = ((256 * y - (86 * u) - (179 * v))>>8);
*b = ((256 * y + (444 * u)) >>8);
// 3 常规转换标准 通过位移来避免乘法运算,精度低
*r = (((y<<8) + (v<<8) + (v<<6) + (v<<4) + (v<<3) + (v<<2) + (v<<1) + v) >> 8);
*g = (((y<<8) - (u<<6) - (u<<4) - (u<<2) - (u<<1) - (v<<7) - (v<<5) - (v<<4) - (v<<1) - v) >> 8);
*b = (((y<<8) + (u<<8) + (u<<7) + (u<<5) + (u<<4) + (u<<3) + (u<<2)) >> 8);
// 4 高清电视标准:BT.709 常规方法:浮点运算,精度高
*r = (y + 1.28033 * v);
*g = (y - 0.21482 * u - 0.38059 * v);
*b = (y + 2.12798 * u);
}

重点来了,将YUV数组恢复为RGBA数组

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
void YV12Convert2RGB(uint8_t *yuv, uint8_t *rgb, int width, int height){
int frameSize = width * height;
int rgbIndex = 0;
int yIndex = 0;
int uvOffset = 0;
int vIndex = frameSize;
int uIndex = frameSize * 1.25;
int R, G, B, Y, U, V;

for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
Y = yuv[yIndex++];
uvOffset = i / 2 * width / 2 + j / 2;//按2 * 2矩阵 还原

V = yuv[vIndex + uvOffset];
U = yuv[uIndex + uvOffset];

yuv2rgb(Y, U, V, &R, &G, &B);

R = RANG_CONTROL(R, 0, 255);
G = RANG_CONTROL(G, 0, 255);
B = RANG_CONTROL(B, 0, 255);

rgb[rgbIndex++] = R;
rgb[rgbIndex++] = G;
rgb[rgbIndex++] = B;
rgb[rgbIndex++] = 255;
}
}
}

以上就是iOS中RGB与YUV互转的方式了,你可以在这里下载demo

参考

三元光模式-维基百科

RGB百度百科

IOS rgb yuv 转换

YUV和RGB互相转换及OpenGL显示YUV数据

YUV颜色编码解析

YUV与RGB格式转化

ARC详解

发表于 2019-10-14 | 分类于 Objective-C | | 阅读次数:

引言:

Objective-C(以下简称OC)的内存管理是通过引用计数来完成的,具体来说有两种方式。以前呢,我们是通过MRC(Manual Reference Counting 手动引用计数)的方式来管理,而现在呢,现在苹果官方是推荐我们使用ARC(Automatic Reference Counting 自动引用计数)。

正文

内存管理基础知识

oc中对对象的引用计数retain count有影响的方法有:retain、release、autorelease等,其中retain会使对象的引用计数+1,而release与autorelease会使对象的引用计数减一。当执行release后,如果对象的引用计数减少为0,则执行对象的dealloc方法,来释放内存。总结起来就是:

  • retain 递增引用计数
  • release 递减引用计数
  • autorelease 在自动释放池 pop时,再递减引用计数

MRC 手动引用计数

在MRC管理方式中,管理原则是谁创建谁释放,即一个对象或者一个方法alloc或者retain了对象了,在使用完毕后,一定要执行release或者autorelease方法。如:

1
2
3
NSObject *obj = [[NSObject alloc] init];
//do something ...
[obj release];

在平时的编码过程中,我们需要创建和销毁非常多的对象,如果通过MRC的方式来管理的话,工作量会非常的繁琐,于是乎苹果的工程师为我们提供了更便捷的管理方式ARC。

ARC 自动引用计数

ARC简介

简单来说ARC的作用是编译器在编译的时候,帮我们加入适当的release和retain调用。例如在ARC中的如下代码:

1
2
3
4
5
- (void)foo
{
NSObject *object = [[NSObject alloc] init];
NSLog(@"%@", object);
}

上面这段代码,在ARC中会被改写为如下形式:

1
2
3
4
5
6
- (void)foo
{
NSObject *object = [[NSObject alloc] init];
NSLog(@"%@", object);
[object release]; //ARC添加的
}

由此可见,在ARC的管理方式下,引用计数任然是有效的,只是release与retain操作交给ARC自动添加了。因为ARC会自动为我们添加内存管理语义,所以在ARC下是不能主动调用内存管理语义的,具体如下:

  • retain
  • release
  • autorelease
  • dealloc

熟悉MRC的人可能会觉得ARC只是替我们调用了release与retain等方法,其实ARC并不是直接通过消息派发机制来调用的,而是直接通过调用底层的C语言版本。C语言对应的方法如下:

  • retain -> objc_retain
  • release -> objc_release
  • autorelease -> objc_autorelease

ARC命名规则

总的来说ARC下的内存管理是通过命名规则来决定对象的管理权的归属,具体来说通过以下名称开头产生的对象管理权归调用者所有:

  • alloc
  • new
  • copy
  • mutableCopy

如果不是通过以上语句开头产生的,那么则有被调用方管理,ARC会在创建方法末尾添加调用autorelease。

示例如下:

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
@implementation Student
+ (id)newStudent:(NSString *)name
{
Student *stu = [[Student alloc] init];
stu.name = name;
//管理权归被调用方所有
return stu;
}

+ (id)studentWithName:(NSString *)name
{
Student *stu = [[Student alloc] init];
/**
在arc下管理权归被调用者
return [stu autorelease];
*/
stu.name = name;
return stu;
}

- (void)dealloc
{
NSLog(@"%@ dealloc", self.name);
}



@end

void foo2()
{
NSLog(@"%s begin", __func__);
{
Student *stu1, *stu2;
stu1 = [Student newStudent:@"Jacky"];
/**
[stu1 release];
*/
stu2 = [Student studentWithName:@"Nacy"];
}
NSLog(@"%s end", __func__);
}

运行结果:

1
2
3
4
2019-10-14 18:06:19.802799+0800 Test_ARC[9252:11351467] foo2 begin
2019-10-14 18:06:19.802865+0800 Test_ARC[9252:11351467] Jacky dealloc
2019-10-14 18:06:30.592471+0800 Test_ARC[9252:11351467] foo2 end
2019-10-14 18:06:33.308012+0800 Test_ARC[9252:11351467] Nacy dealloc

可能你会对这个运行结果存在疑惑,正常来说stu1与stu2的生命周期都应该在=={}==中,出了中括号这两个变量都应该销毁,变量销毁后,指向的对象也应该马上销毁。但是只有stu1在生命周期结束后销毁了,而stu2却延迟销毁了。为什么会这样呢,就像之前介绍说到的,stu2是通过==studentWithName==创建的,这不符合ARC中管理规则,所以stu2是以autorelease的方式返回的,内存管理方式是交给被调用方的。同理,就可以明白stu1会在变量作用于结束时销毁的原理了

ARC的优化

优化一 成对的retain与autorelease优化

上面讲到,如果对象是通过不符合ARC规则的方法创建的,那么ARC会为其加上autorelease,如果这时候接收的变量是强指针的话,那么ARC会为其加上retain调用。示例如下:

1
2
3
4
5
6
Student *stu = [Student studentWithName:@"Nacy"];
/**
在arc处理后代码变为
Student *stu = [Student studentWithName:@"Nacy"];
[stu retain];
*/

这样看来的话,在这里studentWithName中的autorelease与这里的retain就是多余的了,可以删除。但是为了兼容性,ARC没有直接删除。ARC不会直接调用autorelease,而是执行另外的函数objc_autoreleaseReturnValue。这个函数会检查返回值之后的代码,如果之后的代码有执行retain操作,那么就设置全局结构,不执行autorelease操作。同样的,在调用retain方法时,会调用objc_retainAutoreleasedReturnValue。这个方法会检查上面提到的标志位,如果标志位被设置了的话,那么就不会执行retain操作,并重置检测标志位。

优化二 设置实例变量

在MRC情况下,setter方法的内存管理是这样的

1
2
3
4
5
6
- (void)setObj:(id)obj
{
id newObj = [obj retain]; //保留新值
[_obj release]; //释放旧值
_obj = newObj; //设置实例变量
}

但是在ARC下面,只需要设置实例变量即可,ARC会自动保留新值,释放旧值,最后才设置实例变量,就像是上面这样写的一样,ARC代码如下:

1
2
3
4
- (void)setObj:(id)obj
{
_obj = obj; //设置实例变量
}

总结

  • ARC通过方法名来管理方法返回的对象
  • ARC会优化成对的retain与release操作
  • ARC只管理OC对象

参考

Effective Objective-C 2.0

mysql重置密码

发表于 2019-09-18 | 分类于 sundry | | 阅读次数:

参考:mysql 官方重置密码

  1. 杀掉mysql进程
1
2
# shell> kill `cat /mysql-data-directory/host_name.pid`
sudo kill `sudo cat /usr/local/mysql/data/localhost.pid`
  1. 重写init.sql

  2. 1
    2
    3
    4
    --MySQL 5.7.6 and later:
    ALTER USER 'root'@'localhost' IDENTIFIED BY 'MyNewPass';
    --MySQL 5.7.5 and earlier:
    --SET PASSWORD FOR 'root'@'localhost' = PASSWORD('MyNewPass');

    用mysql-init启动mysql

    1
    2
    # mysqld --init-file=/initFilePath &
    shell> sudo mysqld --init-file=/Users/lineworks/Documents/utilities/mysql/mysql-init &

C、C++格式化字符串

发表于 2019-09-09 | 分类于 sundry | | 阅读次数:

引言

在C和C++开发中,我们经常会用到printf来进行字符串的格式化,例如printf("format string %d, %d", 1, 2);,这样的格式化只是用于打印调试信息。printf函数实现的是接收可变参数,然后解析格式化的字符串,最后输出到控制台。那么问题来了,当我们需要实现一个函数,根据传入的可变参数来生成格式化的字符串,应该怎么办呢?

正文

可变参数

首先来一个可变参数使用示例,testVariadic方法接收int行的可变参数,并以可变参数为-1表示结束。va_list用于遍历可变参数,va_start方法接收两个参数,第一个为va_list,第二个为可变参数前一个参数,下面的例子里该参数为a。

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
/**
下面是 <stdarg.h> 里面重要的几个宏定义如下:
typedef char* va_list;
void va_start ( va_list ap, prev_param ); // ANSI version
type va_arg ( va_list ap, type );
void va_end ( va_list ap );
va_list 是一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。
<Step 1> 在调用参数表之前,定义一个 va_list 类型的变量,(假设va_list 类型变量被定义为ap);
<Step 2> 然后应该对ap 进行初始化,让它指向可变参数表里面的第一个参数,这是通过 va_start 来实现的,第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数;
<Step 3> 然后是获取参数,调用va_arg,它的第一个参数是ap,第二个参数是要获取的参数的指定类型,然后返回这个指定类型的值,并且把 ap 的位置指向变参表的下一个变量位置;
<Step 4> 获取所有的参数之后,我们有必要将这个 ap 指针关掉,以免发生危险,方法是调用 va_end,他是输入的参数 ap 置为 NULL,应该养成获取完参数表之后关闭指针的习惯。说白了,就是让我们的程序具有健壮性。通常va_start和va_end是成对出现。
*/
//-1表示可变参数结束
void receiveVariadic(int a, ...) {
va_list list;
va_start(list, a);
int arg = a;
while (arg != -1) {
arg = va_arg(list, int);
printf("%d ", arg);
}
printf("\n");
va_end(list);
}

//test
void testVari()
{
printf("------%s------\n", __FUNCTION__);
//-1表示可变参数结束
receiveVariadic(1, 2, 3, 4, 5, 6, -1);
}

运行结果

1
2
------testVari------
2 3 4 5 6 -1

格式化字符串

好了,我们已经介绍了怎样实现一个接收可变参数的C函数,接下来介绍根据接收的可变参数来格式化字符串。这里介绍两种方式,第一种是利用宏定义,第二种通过函数的方式来实现。

通过宏定义的方式

en…让咱们先来看看第一个版本的宏,这个宏定义对于不熟悉宏的人来说可能看着有点费劲,不过不要怕,稍后会做解释,代码如下:

1
2
3
4
5
6
7
8
9
#define myFormatStringByMacro_WithoutReturn(format, ...) \
do { \
int size = snprintf(NULL, 0, format, ##__VA_ARGS__);\
size++; \
char *buf = (char *)malloc(size); \
snprintf(buf, size, format, ##__VA_ARGS__); \
printf("%s", buf); \
free(buf); \
} while(0)

宏基础知识

首先需要介绍宏用到的知识:\, 这个\的作用是可换行定义宏,毕竟如果一行很长的宏可读性很差,使用方式在换行时加上\即可。第二个是介绍(format, ...),这里的...是预定义的宏,用于接收可变参数,就像是printf函数一样。接着介绍##__VA_ARGS__,同样的__VA_ARGS__也是预定义的宏,表示接收到的...传入的可变参数。##的作用是用来处理未传入可变参数的情况,当没有传入可变参数的时候,编译器或通过优化将snprintf(NULL, 0, format, ##__VA_ARGS__);优化为snprintf(NULL, 0, format);。你可以理解为没有可变参数时,##前的逗号,与__VA_ARGS__都被“干掉了”。

你一定会觉得困惑,为什么要写do-while语句呢?这是为了宏的健壮性,如果使用宏的人像下面这样使用的话,就会出问题

1
2
3
4
5
6
7
8
9
10
#define testMarco(a, b) \
int _a = a + 1; \
int _b = b + 1; \
printf("\n%d", _a + _b); \

void test()
{
if (1 > 0)
testMarco(1, 2);
}

上面的代码连编译都不会通过, 会报错如下:

如果手动展开这个宏的话,会变成这个样子,问题就显而易见了。但是如果if语句加上了{}的话,就不会有问题,可以看出规范写法是多么的重要🐶(皮一下很开心)。

1
2
3
4
5
void test()
{
if (1 > 0)
int _a = 1 + 1; int _b = 2 + 1; printf("\n%d", _a + _b);;
}

加上do-while以后就不一样,加上do-while后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
#define testMarco(a, b) \
do { \
int _a = a + 1; \
int _b = b + 1; \
printf("\n%d", _a + _b); \
} while(0)

void test()
{
if (1 > 0)
testMarco(1, 2);
}

预处理之后代码如下:

1
2
3
4
5
6
//展开后的代码 
void test()
{
if (1 > 0)
do { int _a = 1 + 1; int _b = 2 + 1; printf("\n%d", _a + _b); } while(0);
}

好了,宏的基础知识就介绍这么多了,接下来进入正题。

代码解析

为了方便阅读,原谅我在这里再贴一遍宏定义的代码:

1
2
3
4
5
6
7
8
9
#define myFormatStringByMacro_WithoutReturn(format, ...) \
do { \
int size = snprintf(NULL, 0, format, ##__VA_ARGS__);\
size++; \
char *buf = (char *)malloc(size); \
snprintf(buf, size, format, ##__VA_ARGS__); \
printf("%s", buf); \
free(buf); \
} while(0)

首先,介绍一下snprintf()函数,此函数的定义如下:

1
2
3
4
5
6
7
8
9
/**

@param __str 接收格式化结果的指针
@param __size 接收的size
@param __format 格式化的字符串
@param ... 可变参数
@return 返回格式化后实际上写入的大小a,a <= __size
*/
int snprintf(char * __restrict __str, size_t __size, const char * __restrict __format, ...) __printflike(3, 4);

为了方便理解,使用方式是这个样子的:

1
2
3
4
5
6
7
void testSnprintf()
{
printf("------%s------\n", __FUNCTION__);
char des[50];
int size = snprintf(des, 50, "less length %d", 50);
printf("size:%d\n", size);
}

运行结果:

1
2
------testSnprintf------
size:14

snprintf函数还有一个用法是__str和__size分别传入NULL和0,返回值会是格式化字符串的实际长度,可以通过这个方式来获取正确的格式化size,从而避免malloc多余的空间,造成空间浪费。同时返回的size是不包含结束符\0的,所以真正写入要buffer时,需要对size + 1。

相信通过我的解释,你一定能看懂上面这段代码了吧。哦,对了malloc的代码一定要记得free(敲重点)。

到了这里,如果细心思考的同学一定会问?这个宏根本没有实际用途好不好,我要的是能够把格式化的字符串作为返回值返回的,仅仅打印直接用printf不就好了。其实,这样的宏还是有作用的,比如说当你要记录日志时,你可以像这样使用:

1
2
3
4
5
6
7
8
9
#define Log_Debug(format, ...) \
do { \
int size = snprintf(NULL, 0, format, ##__VA_ARGS__);\
size++; \
char *buf = (char *)malloc(size); \
snprintf(buf, size, format, ##__VA_ARGS__); \
doLog(buf); \
free(buf); \
} while(0)

要将结果字符串返回的话,需要用到GNU C的赋值扩展,使用方式如下:

1
2
3
4
5
int a = ({
int b = 2;
int c = 4;
b + c;
});

这段代码变量a最终值会是6。利用gnu这个扩展,将之前的宏改造一下就能实现我们的需求,改造完成后是这个样子的:

1
2
3
4
5
6
7
8
#define myFormatStringByMacro_ReturnFormatString(format, ...) \
({ \
int size = snprintf(NULL, 0, format, ##__VA_ARGS__);\
size++; \
char *buf = (char *)malloc(size); \
snprintf(buf, size, format, ##__VA_ARGS__); \
buf; \
});

调用宏的代码:

1
2
3
4
5
6
7
void testByMacro1()
{
printf("------%s------\n", __FUNCTION__);
char *a = myFormatStringByMacro_ReturnFormatString("format by macro, %d %s", 123, "well done");
printf("%s\n", a);
free(a);
}

原谅我的啰嗦,malloc开辟的空间一定要记得free。运行结果:

1
2
------testByMacro1------
format by macro, 123 well done

至此利用宏的方式就介绍完了。

通过函数的方式

老规矩先上代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
char *myFormatStringByFun(char *format, ...)
{
va_list list;
//1. 先获取格式化后字符串的长度
va_start(list, format);
int size = vsnprintf(NULL, 0, format, list);
va_end(list);
if(size <= 0) {
return NULL;
}
size++;

//2. 复位va_list,将格式化字符串写入到buf
va_start(list, format);
char *buf = (char *)malloc(size);
vsnprintf(buf, size, format, list);
va_end(list);
return buf;
}

这里利用的是vsnprintf函数,此函数的定义在stdio.h中的定义如下:

1
2
3
4
5
6
7
8
9
/**

@param __str 目标字符串
@param __size 要赋值的大小
@param __format 格式化字符串
@param va_list 可变参数列表
@return 返回格式化后实际上写入的大小a,a <= __size
*/
int vsnprintf(char * __restrict __str, size_t __size, const char * __restrict __format, va_list) __printflike(3, 0);

vsnprintf的具体使用方式和之前介绍的snprintf是差不多的,这里就不再详细介绍了,不大明白的同学可以看看上面的介绍。哦,对了,这两个函数都是定义在stdio.h这个头文件下的

接下来就是试一下我们封装的函数了

1
2
3
4
5
6
void testByFun()
{
printf("------%s------\n", __FUNCTION__);
char *b = myFormatStringByFun("format by fun %d %s", 321, "nice");
printf("%s\n", b);
}

运行结果:

1
2
------testByFun------
format by fun 321 nice

格式化字符串的方法差不多介绍完了,不知道善于思考的你有没想到直接用宏定义来调用我们封装的函数呢?我就在这直接给出宏定义和使用方式了

1
2
3
4
5
6
#define myFormatStringByFunQuick(format, ...) myFormatStringByFun(format, ##__VA_ARGS__);
void testMyFormatStringByFunQuick() {
printf("------%s------\n", __FUNCTION__);
char *formatString = myFormatStringByFunQuick("amazing happen, %s", "cool");
printf("%s\n", formatString);
}

运行结果:

1
2
------testMyFormatStringByFunQuick------
amazing happen

C++版本

对了,最初实现是用的C++版本,这里使用的是泛型,代码是这个样子的:

1
2
3
4
5
6
7
8
9
10
11
12
template< typename... Args >
std::string string_sprintf( const char* format, Args... args ) {
int length = std::snprintf( nullptr, 0, format, args... );
assert( length >= 0 );

char* buf = new char[length + 1];
std::snprintf( buf, length + 1, format, args... );

std::string str( buf );
delete[] buf;
return str;
}

其实和C语言版本的没什么差别,只是多了泛型的东西而已,相信聪明的你一定能看懂,看不懂的话,就去看看C++的泛型知识吧,哈哈哈。

结语

终于介绍完了,你可以在这里下载代码。写博客是真的有点累人,不过对于最近被面试打击的我来说,写博客能够让我对知识理解的更加透彻,毕竟要自己认真思考后才能够写的明白(至少我觉得讲明白了,哈哈哈)。如果有什么说的不对的地方,还请指出,感谢你的阅读,thks。

参考资料

std::string formatting like sprintf

宏定义的黑魔法 - 宏菜鸟起飞手册

整理:C/C++可变参数,“## VA_ARGS”宏的介绍和使用

12

吴珂

机会是留给有准备的人?不对,准备留给有机会的人,没机会的就别瞎准备了。

14 日志
7 分类
1 标签
GitHub
© 2019 — 2020 吴珂
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4