codeFragment

欢迎来到召唤师峡谷,我们将带领你一起去探寻最远古的英雄史诗!

iOS

iOS WKWebView:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (webView != _webView) { return; }

NSURL *url = navigationAction.request.URL;
NSString* host = url.host.lowercaseString;

if ([host isEqualToString:@"ldaqiangl"]) {

// do js event tings
} else if ([host isEqualToString:@"http"] || [host isEqualToString:@"https"]) {

// do normal tings
}
}

iOS WebView:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
if (webView != _webView) { return YES; }

NSURL *url = [request URL];
NSString* host = url.host.lowercaseString;
if ([host isEqualToString:@"ldaqiangl"]) {

// do js event tings
} else if ([host isEqualToString:@"http"] || [host isEqualToString:@"https"]) {

// do normal tings
}
}

Android

Android WebView UrlSheme Intercept

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
public class BridgeWebViewClient extends WebViewClient {

private BridgeWebView webView;
private Context mContext;

public BridgeWebViewClient(BridgeWebView webView) {
this.webView = webView;
}

public BridgeWebViewClient(BridgeWebView webView, Context mContext) {
this.webView = webView;
this.mContext = mContext;
}

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
try {
url = URLDecoder.decode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}

if (url.startsWith("ldaqiangl://")) {
//do js something
return false;
} else {
// 如果需要进行token认证
Map<String, String> additionalHttpHeaders = new HashMap<>();
additionalHttpHeaders.put("Authorization", "Bearer " + UserInfoUtils.getAccessToken(mContext));
view.loadUrl(url, additionalHttpHeaders);
return true;
}
}

@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
}

@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
}

@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
}
}

宏定义基本知识

一、#

名称:字符串化操作符

其作:将宏定义中的传入参数名转换成用一对双引号括起来参数名字符串。

使用条件:只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前

举例:

#define example1(instr) #instr

string str=example1(abc); 将会展成:string str=”abc”;

注意:对空格的处理

a、忽略传入参数名前面和后面的空格。

如:str=example1( abc ); 将会被扩展成 str=”abc”;

b、当传入参数名间存在空格时,编译器将会自动连接各个子字符串,用每个子字符串中只以一个空格连接,忽略其中多余一个的空格。

如:str=exapme1( abc def); 将会被扩展成 str=”abc def”;

二、##

名称:符号连接操作符

作用:将宏定义的多个形参名连接成一个实际参数名

使用条件:只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前

举例:

#define exampleNum(n) num##n

int num=exampleNum(9); 将会扩展成 int num=num9;

注意:

1、当用##连接形参时,##前后的空格可有可无。

如:#define exampleNum(n) num ## n 相当于 #define exampleNum(n) num##n

2、连接后的实际参数名,必须为实际存在的参数名或是编译器已知的宏定义

三、@#

名称:字符化操作符

作用:将传入的单字符参数名转换成字符,以一对单引用括起来。

使用条件:只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前。

举例:

#define makechar(x) @#X

a = makechar(b); 展开后变成了:a= ‘b’;

四、\

名称:行继续操作符

作用:当定义的宏不能用一行表达完整时,可以用”\”表示下一行继续此宏的定义。

注意:换行不能切断单词,只能在空格的地方进行

使用symbolicatecrash分析crash日志

问:App Store审核有时候会反馈给一些crash日志信息文件,但是自己又无法轻易复现该bug,需要怎么样去分析这些crash文件呢?

答:使用苹果提供的symbolicatecrash工具来有效分析和定位bug的堆栈信息,具体步骤如下(当然也包含若干使用上遇到的问题):

1、收集到上线对应应用的符号表文件(xxx.app.dSYM)和二进制文件(xxx.app)

打开Xcode -> Window -> Organizer,选中相应的应用,如图所示:
symbolicatecrash001
然后,右击,Show in Finder
然后,右击,显示包内容:
在其他地方(我是在桌面)新建一个文件,我命名为:Crash
然后,在上述包内容文件夹里分别找到:/dSYMs/xxx.app.dSYM/Products/Applications/xxx.app 文件,并将其拷贝至桌面的Crash文件夹内
symbolicatecrash001_1
symbolicatecrash001_2

2、查找和提取到symbolicatecrash工具

(1)使用 find /Applications/Xcode.app -name symbolicatecrash -type f 命令找到要使用的symbolicatecrash 工具,结果如图:
symbolicatecrash002

说明:symbolicatecrash 的工具文件夹在Xcode8.3里是这个路径下的位置,但是在之前的版本中,会有不同的位置,请是具体情况自行查找到目标文件

(2)然后将工具拷贝到 上一步骤中的 Crash文件夹中:

1
cp /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash /Users/dongfuqiang/Desktop/Crash

结果如图:
symbolicatecrash003

3、分析Crash日志

(1)将要分析的文件放进Crash文件夹,如果苹果反馈的文件为.txt文件,请把后缀名改为.crash
(2)使用命令,分析目标文件:

1
2
3
4
./symbolicatecrash /Users/dongfuqiang/Desktop/Crash/at
tachment-2984565074812063208crashlog-410C678E-284A-4D9A-AB75-1B4585459D11.crash
/Users/dongfuqiang/Desktop/Crash/kangs100Doc.app.dSYM > kangs100Doc_symbol01.c
rash

(3)可能会出现如图错误:Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash line 69.
symbolicatecrash004
解决办法是:

1
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"

(4)然后再次执行分析命令,如果再次报错如图:
xcrun: error: invalid DEVELOPER_DIR path (/Applications/Xcode.app/Contents/SharedFrameworks), missing xcrun at: /Applications/Xcode.app/Contents/SharedFrameworks/usr/bin/xcrun Error: can't find tool named 'otool' in the macosx SDK or any fallback SDKs at
symbolicatecrash005
请参考:异常处理

4、查看和对比结果:

最终生成的文件如图:
symbolicatecrash006
我的两个Crash日志文件 都分别做了一次分析,

最终结果对比如下图:
symbolicatecrash007
可以看到,Crash的位置的堆栈信息得到了分析结果(对比图右侧),然后,我们可以在项目源码中查看相应的方法有什么问题,并解决。

5、其他

看到有人不使用symbolicatecrash工具手动敲命令的方式解析Crash日志结果,就可以进行分析,表示
学习了
别用symbolicatecrash来解析crash Log了

ssh 多账户配置

ssh 多账户配置 步骤:

1、新建不同用户的SSH Key

1
2
3
4
5
#新建SSH key:
$ cd ~/.ssh # 切换到C:\Users\Administrator\.ssh
ssh-keygen -t rsa -C "mywork@email.com" # 新建工作的SSH key
# 设置名称为id_rsa_work
Enter file in which to save the key (/c/Users/Administrator/.ssh/id_rsa): id_rsa_work

2、新密钥添加到SSH agent中

因为默认只读取id_rsa,为了让SSH识别新的私钥,需将其添加到SSH agent中:

1
ssh-add ~/.ssh/id_rsa_work

如果出现Could not open a connection to your authentication agent的错误,就试着用以下命令:

1
2
ssh-agent bash
ssh-add ~/.ssh/id_rsa_work

3、修改config文件 将账户以及git服务器与对应的密钥关联。在~/.ssh目录下找到config文件,如果没有就创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 该文件用于配置私钥对应的服务器
# Default github user(first@mail.com)
Host github.com
HostName github.com
User git
IdentityFile C:/Users/Administrator/.ssh/id_rsa

# second user(second@mail.com)
# 建一个github别名,新建的帐号使用这个别名做克隆和更新
Host github2
HostName github.com
User git
IdentityFile C:/Users/Administrator/.ssh/id_rsa_work

其规则就是:从上至下读取config的内容,在每个Host下寻找对应的私钥。这里将GitHub SSH仓库地址中的git@github.com替换成新建的Host别名如:github2,那么原地址是:git@github.com:funpeng/Mywork.git,替换后应该是:github2:funpeng/Mywork.git.

4、用记事本打开新生成的~/.ssh/id_rsa2.pub文件,将里面的内容添加到GitHub后台。

可不要忘了添加到你的另一个github帐号下的SSH Key中。

5、测试:

1
2
3
$ ssh -T git@github.com
Hi BeginMan! You've successfully authenticated, but GitHub does not provide shel
l access.
1
2
$ ssh -T github2
Hi kunkun01! You've successfully authenticated, but GitHub does not provide shell access.

6、应用

1
2
3
4
5
6
$ git clone github2:kunkun01/Mywork.git
Cloning into 'Mywork'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (3/3), done.
Checking connectivity... done

如果你只是通过这篇文章中所述配置了Host,那么你多个账号下面的提交用户会是一个人,所以需要通过命令git config –global –unset user.email删除用户账户设置,在每一个repo下面使用git config –local user.email ‘你的github邮箱@mail.com’ 命令单独设置用户账户信息

“宏”荒之力

这个月的总结、学习就从最基本而大多数人都不怎么深入的“宏定义”开始吧!关于宏,可以说从底层框架到高级开发,从大牛到菜鸟,都无处不在。它的优点多多:跨平台、编译优化方便、节省工作量、增强代码可读性等等等,所以熟练掌握这项技能并能写的一手好宏,将会为我们阅读开源代码或者平时开发工作带来实用高效的价值!

基本知识

宏,一般分为:对象宏(object-like macro)和函数宏(function-like macro)。

1. 对象宏

对象宏一般定义一些常数常量,如:

1
#define MAXFLOAT 0x1.fffffep+127f

#define关键字表明即将开始定义一个宏,紧接着的MAXFLOAT是宏的名字,空格之后是内容。类似这样的#define X Content很简单,在编译时编译器会在语义分析完成后,将X替换为Content,这个过程称为宏的展开。

2. 函数宏

函数宏的作用类似于函数,可以接受传递进去的参数。定义:在宏名字后面跟上一对括号,这个宏就变成了函数宏。
如:

1
#define BSADD(A,B) A+B

在认识了宏的基本定义后,我们都会感觉,这个还是蛮简单的嘛!那么,就让我们一起看看宏是不是想你想的那样->

实例学习

根据上面所述,我们知道宏展开其实是编辑器的预处理,所以它可以在更高层级和更早的去控制程序源码本身和编译流程,这么便利和强大的工具使用起来其实并没有那么顺利,我们先实现一个自己的函数宏来一探究竟吧。

一、函数宏实践

下面要自己动手实现MAX这个求两者之间最大值得函数,话说这个简单的很,那我们先来看看,这个函数宏如果想要足够健壮,需要解决几个问题:

  • 该宏和其他数值有运算关系时,保证结果
  • 该宏内部传入表达式参数时,保证结果
  • 该宏内部传入如变量的自增,自减等运算时,保证结果
  • 特殊命名的变量作为参数传入,如_a等,保证结果

1. 我们先实现最简单的,只取得两数之间,那个最大

1
#define MAX(A,B) A > B ? A : B

2. 更进一步,实现宏外和其他数值或者宏发生关系,上面的就无法满足,需要增加括号提升优先级

1
2
3
4
int a = 4 * MAX(5,10);  
// 4 * MAX(5,10) <=> 4 * 5 > 10 ? 5 : 10
NSLog(@"a = %d",a);
// 输出为 a = 5

改成:

1
#define MAX(A,B) (A > B ? A : B )

3. 再进一步,放入的参数如果不是数值呢,如果是一个表达式:

1
2
3
4
5
6
int a = MAX(5, 4 > 3 ? 4 : 3);  
// <=> (5 > 4 > 3 ? 4 : 3 ? 5 : 4 > 3 ? 4 : 3)
// <=> ((5 > (4 > 3 ? 4 : 3) ? 5 : 4) > 3 ? 4 : 3)
// <=> 4
NSLog(@"a = %d",a);
// 输出 4

看来还是限定的优先级不够呢,改成如下层层限定优先级:

1
#define MAX(A,B) ((A) > (B) ? (A) : (B))

4. 但是,如果放入特殊运算符如自增运算,会发现新的问题出现了:

1
2
3
4
5
int a = 5;  
int b = MAX(a--, 4);
// <=> ((a--) > (4) ? (a--) : (4))
NSLog(@"a = %d, b = %d",a,b);
// => a = 3, b = 4

可以看到,上面a先和4进行比较,然后-1,变成4。然后,取得真值(a--)再次进行-1,变成3,所以a进行了两次自减运算,显然,This is not my wish!
这个问题,并不好解决,所以,我们需要借助GNU C的一个赋值扩展,即:({...})的形式。它的作用很像脚本语言,顺次执行之后,会将最后一次的表达式的赋值作为返回。{}中的代码会在执行完毕之后,进行返回,举例说明如下:

1
2
3
4
5
6
7
int a = ({
int b = 4;
int c = 5;
b * c;
});
NSLog(@"a = %d",a);
// 输出 a = 20

所以我们终于升级到GNU C中的写法:

1
#define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; })

代码域中定义了三个段语句,分别重新声明了__a__b,并且把这两个变量作为输入值,进行比较,这样一来,就会让上面的自减运算只进行一次,然后再进入比较操作。而且,在比较操作中,就不会存在优先级的问题了,最终把值返回,达到终极目的~

这么一来,我们还有一个很少见的情况,就是变量名的命名有时候会比较特殊,如:在同一个函数域内已经有__a或者__b的定义的话,这个宏可能出现问题。在声明后赋值将因为定义重复而无法被初始化,导致宏的行为不可预知!

所以,我们看下苹果的clang编译器是怎么彻底解决这个问题的:

1
2
3
4
5
6
7
8
9
10
11
#if __clang__

#define __NSX_PASTE__(A,B) A##B
#define __NSMAX_IMPL__(A,B,L) ({ \
__typeof__(A) __NSX_PASTE__(__a,L) = (A); \
__typeof__(B) __NSX_PASTE__(__b,L) = (B); \
(__NSX_PASTE__(__a,L) < __NSX_PASTE__(__b,L)) ? __NSX_PASTE__(__b,L) : __NSX_PASTE__(__a,L); })
#define MAX(A,B) __NSMAX_IMPL__(A,B,__COUNTER__)

else
......

我们先分析其中看似并不熟悉的几个符号:
首先,##是宏中的连接(拼接)符号,把两个参数连接起来进行运算。注意函数宏必须是有意义的运算,因此你不能直接写AB来连接两个参数,而需要写成例子中的A##B;
其次,__COUNTER__是一个预定义的宏,这个值在编译过程中会从0开始计数,每次被调用时都会加1。因为唯一性,所以很多时候被用来构造独立的变量名称。
最终,我们结合GUN C的写法可以看到:变量名__a__b添加了一个计数后缀,这样就在很大程度上避免了变量名相同而导致问题的可能性。

到这里,也许我们会终于知道,看似简单的东西,其实含有很多需要克服和学习的点。-。-

二、日志打印Log自定义实践

在iOS开发中,我们接触最多(没有之一)就是日志打印输出了,但是问题在于系统提供的NSLog显然是被阉割版的,完全不能满足我们作为一个右逼哥的程序猿的需求,再加上,我们在debugrelease的状态下,需要打印和不打印日志的,所以,宏的作用就可以显出它的优势了!
来个万能Log:

1
#define BSLog(format, ...) do {fprintf(stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],__LINE__, __func__);(NSLog)((format), ##__VA_ARGS__);fprintf(stderr, "-------\n");} while (0)

分析下这个宏定义:

  1. ...表示可变参数(variadic),我们在JavaScript中也经常使用到,未知参数的个数的情况下使用;
  2. 我刚开始看到这个外层的do {} while(0)循环的时候,就感觉到有点多余,去掉以后就会发现的确是有问题的,例如,放到下面的代码中,错误立现:
    1
    2
    if (isTure)
    BSLog(@"This is ture excute!");

这样不写{}if语句肯定有人写,宏展开后变成:

1
2
if (isTure)
fprintf(stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],__LINE__, __func__);(NSLog)((format), ##__VA_ARGS__);fprintf(stderr, "-------\n");

很明显,真正的NSLog被调用了,出现了不符合我们预期的情况,所以要做一个改善,加一个{}在外面是不是会好一些呢:

1
2
3
4
5
6
7
8
#define BSLog(format, ...) { \
fprintf(stderr, "<%s : %d> %s\n", \
[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], \
__LINE__, \
__func__); \
(NSLog)((format), ##__VA_ARGS__); \
fprintf(stderr, "-------\n"); \
}

再次碰壁,因为发现放到if ()后面的时候,我们写的BSLog语句最后按照OC语法会追加一个;号,造成了以下事情的发生:

1
2
3
4
5
if (isTure)
{
fprintf(stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],__LINE__, __func__);(NSLog)((format), ##__VA_ARGS__);fprintf(stderr, "-------\n");
}; else
//Excute...

显然,编译器不会通过这种写法的嘛!
然而,伟大的程序猿将是无所不能的,自然难不倒我们,后面把这个进化到加上do {} while(0)循环执行一次代码,就等价于上面操作了,而且还隔离出一块安全区域来,巧妙而强大!
更奇特的是,while(0)在编译器编译的时候会进行优化,把这部分内容像空气一样忽略掉,而不会有一点效率上的损失,妙哉,妙哉!

  1. 现在可以开始一句一句地分析里面的内容了,看到fprintf(stderr, "<%s : %d> %s\n", \,就是一个格式化的输出,关于stderr属于输入/输出类型stdin、stdout、stderr(标准输入从键盘读取,同时标准输出和标准错误会打印到屏幕)之一了
  2. 再往下,就是三个value了,我们在其中又遇到3个预定义的宏:__FILE__,__LINE____func__(还记得上一个是我们的__COUNTER__计数的宏么),分别简单解释:__FILE__得到当前文件的绝对路径;__LINE__返回展开该宏时在文件中的行数;__func__返回是改宏所在函数域的函数名称;如果输出日志带上这几个参数,就可以快速定位相应的位置。在这里我们提供两个查看编译器实现的机制和方式的两个地址:GCC的PreDefine和clang的Builtin Macro,进行深入学习,因为文件的绝对路径很长很丑,所以输出了文件名的最后一个部分;
  3. 接下来的工作是还原原来的NSLog,(NSLog)((format), ##__VA_ARGS__);再次出现一个预定义的宏,__VA_ARGS__,即表示:宏定义中的...中的所有剩余参数。所以,在宏展开后,编译器会将__VA_ARGS__直接替换为输入中从第二个参数开始的剩余参数。再一个就是其中##的含义了,其实跟上面出现的##含义差不多,把前面的格式化字符串和后面的参数列表合并,这样我们就得到了一个完整的NSLog方法了。

现在我们分析完了这个宏定义,可以小试牛刀一下了:
macro001

实验完成后,我们还有几个疑点需要整明白:

  1. 为什么我们要把format单独写出来,然后吧其他参数作为可变参数传递呢?如果我们不要那个format,而直接写成BSLog(...)会不会有问题?对于这个例子来说是没有变化的,但是我们需要记住的是...是可变参数列表,它可以代表一个、两个,或者是很多个参数,但同时它也能代表零个参数。如果我们在声明这个宏的时候没有指定format参数,而直接使用参数列表,那么在使用中不写参数的NSLog()也将被匹配到这个宏中,导致编译无法通过。我们可以看看Cocoa中真正的NSLog方法的实现:FOUNDATION_EXPORT void NSLog(NSString *format, ...),可以看到它也是接收一个格式参数和一个参数列表的形式,我们在宏里这么定义,正是为了其传入正确合适的参数,从而保证使用者可以按照原来的方式正确使用这个宏;
  2. 既然可变参数可以接受任意个输入,那么在只有一个format输入,而可变参数个数为零的时候会发生什么呢?展开看一看,现在##之后的可变参数是空:
    1
    2
    3
    4
    5
    6
    BSLog(@"http://www.ldaqiangl.com");
    <=> do {
    fprintf((stderr, "<%s : %d> %s\n",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String], __LINE__, __func__);
    (NSLog)((@"http://www.ldaqiangl.com"), );
    fprintf(stderr, "-------\n");
    } while (0)

这里有个特殊的规则,在逗号和__VA_ARGS__之间的##号,除了拼接前后文本之外,还有一个功能,那就是如果后方文本为空,那么它会将前面一个逗号抹掉。这个特性当且仅当上面说的条件成立时才会生效,因此算是特例。加上这条规则后,就可以将刚才的式子展开为正确的(NSLog)((@"http://www.ldaqiangl.com"));了。

  1. 第三个值得论证的就是,(NSLog)((format), ##__VA_ARGS__);中括号的用法,是不是可以去掉看似多余的括号,写成:NSLog(format, ##__VA_ARGS__);可不可以?经过验证,是可以的,因为首先format不会被调用多次也不太存在误用的可能性(因为最后编译器会检查NSLog的输入是否正确)。另外你也不用担心展开以后式子里的NSLog会再次被自己展开,虽然展开式中NSLog也满足了我们的宏定义,但是宏的展开非常聪明,展开后会自身无限循环的情况,就不会再次被展开了。

作为上述的扩展,我们可以写更多好用的宏定义:

1
2
3
#define BSLogRect(rect) BSLog(@"%s x:%.4f, y:%.4f, w:%.4f, h:%.4f", #rect, rect.origin.x, rect.origin.y, rect.size.width, rect.size.height)
#define BSLogSize(size) BSLog(@"%s w:%.4f, h:%.4f", #size, size.width, size.height)
#define BSLogPoint(point) BSLog(@"%s x:%.4f, y:%.4f", #point, point.x, point.y)

进阶实例

看到这里,我想我们已经可以对宏进行一个客观的评价,最起码能感受到它的一点魅力所在。那么,下一步我们就看看各大牛X框架里宏的深度用法,从这些用法中我们肯定会得到更多好玩的东西!

1.ReactNative中的宏

ReactNative是Facebook的可以用JavaScript开发原生APP的开源框架,内部包含有太多思想、方法和理论,其中也当然无可避免的涉及到宏的黑魔法运用
ReactNative中,JS和OC的互相调用时基于JavaScriptCore的,但是只是用到其中的对JavaScript的解释执行的功能,而OC和JS的互相调用的机制,则是由其自行完成的,同样用WebView去解析JS也是可以完成同样的功能,进而能让他做到同事兼容iOS6以及之前的版本!
了解ReactNative的话大致知道其是根据模块配置表的方式进行方法查找和传值的,但是这个配置表并不需要我们开发者去维护,而是自动生成的,这个过程当中就用到一个宏的黑魔法,让我们一探究竟:
要想让想暴露给JS调用的方法被识别,必须得有某种手段来标识,而ReactNative是怎么做的呢,其实只是用了编译属性__attribute__
(以下引用bang的调研结果说明)
我们现在知道,只要在方法中写上一个宏RCT_EXPORT(),像下面一样,就可以做到暴露方法给JS:

OC

1
2
3
4
5
6
7
@implement RCTManager
- (void)excuteSometing:(NSString *)excuteString successCallback:(RCTResponseSenderBlock)responseSender {
RCT_EXPORT();
NSString *ret = @"ret"
responseSender(ret);
}
@en

JS

1
2
3
RCTManager.excuteSometing("invoke a method", function(result) {
//result == "ret";
});

而进入RCT_EXPORT()中我们看到:

1
2
#define RCT_EXPORT(JS_name) __attribute__((used, section("__DATA,RCTExport" \
))) static const char *__rct_export_entry__[] = { __func__, #JS_name }

这个宏的作用是用编译属性attribute给二进制文件新建一个section,属于DATA数据段,名字为RCTExport,并在这个段里加入当前方法名。编译器在编译时会找到attribute__进行处理,为生成的可执行文件加入相应的内容。效果可以从linkmap看出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Sections:
# Address Size Segment Section
0x100001670 0x000C0180 __TEXT __text
...
0x10011EFA0 0x00000330 __DATA RCTExport
0x10011F2D0 0x00000010 __DATA __common
0x10011F2E0 0x000003B8 __DATA __bss
...

0x10011EFA0 0x00000010 [ 4] -[RCTStatusBarManager setStyle:animated:].__rct_export_entry__
0x10011EFB0 0x00000010 [ 4] -[RCTStatusBarManager setHidden:withAnimation:].__rct_export_entry__
0x10011EFC0 0x00000010 [ 5] -[RCTSourceCode getScriptText:failureCallback:].__rct_export_entry__
0x10011EFD0 0x00000010 [ 7] -[RCTAlertManager alertWithArgs:callback:].__rct_export_entry__
...

可以看到可执行文件数据段多了个RCTExport段,内容就是各个要暴露给JS的方法。这些内容是可以在运行时获取到的,在RCTBridge.m的RCTExportedMethodsByModuleID()方法里获取这些内容,提取每个方法的类名和方法名,就完成了提取模块里暴露给JS方法的工作。
整体的模块类/方法提取实现在RCTRemoteModulesConfig()方法里。

2.ReactCocoa中的宏

RACObserve,它通过KVC来为对象的某个属性创建一个信号返回

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
//调用 RACSignal是类的名字
RACSignal *signal = RACObserve(self, currentLocation);

//以下开始是宏定义
//rac_valuesForKeyPath:observer:是方法名
#define RACObserve(TARGET, KEYPATH) \
[(id)(TARGET) rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]

#define keypath(...) \
metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))

//这个宏在取得keypath的同时在编译期间判断keypath是否存在,避免误写
//您可以先不用介意这里面的巫术..
#define keypath1(PATH) \
(((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))

#define keypath2(OBJ, PATH) \
(((void)(NO && ((void)OBJ.PATH, NO)), # PATH))

//A和B是否相等,若相等则展开为后面的第一项,否则展开为后面的第二项
//eg. metamacro_if_eq(0, 0)(true)(false) => true
// metamacro_if_eq(0, 1)(true)(false) => false
#define metamacro_if_eq(A, B) \
metamacro_concat(metamacro_if_eq, A)(B)

#define metamacro_if_eq1(VALUE) metamacro_if_eq0(metamacro_dec(VALUE))

#define metamacro_if_eq0(VALUE) \
metamacro_concat(metamacro_if_eq0_, VALUE)

#define metamacro_if_eq0_1(...) metamacro_expand_

#define metamacro_expand_(...) __VA_ARGS__

#define metamacro_argcount(...) \
metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

#define metamacro_at(N, ...) \
metamacro_concat(metamacro_at, N)(__VA_ARGS__)

#define metamacro_concat(A, B) \
metamacro_concat_(A, B)

#define metamacro_concat_(A, B) A ## B

#define metamacro_at2(_0, _1, ...) metamacro_head(__VA_ARGS__)

#define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__)

#define metamacro_head(...) \
metamacro_head_(__VA_ARGS__, 0)

#define metamacro_head_(FIRST, ...) FIRST

#define metamacro_dec(VAL) \
metamacro_at(VAL, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19)

3.断言宏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define XCTAssertTrue(expression, format...) \
_XCTPrimitiveAssertTrue(expression, ## format)

#define _XCTPrimitiveAssertTrue(expression, format...) \
({ \
@try { \
BOOL _evaluatedExpression = !!(expression); \
if (!_evaluatedExpression) { \
_XCTRegisterFailure(_XCTFailureDescription(_XCTAssertion_True, 0, @#expression),format); \
} \
} \
@catch (id exception) { \
_XCTRegisterFailure(_XCTFailureDescription(_XCTAssertion_True, 1, @#expression, [exception reason]),format); \
}\
})

4.方法过期宏

1
BS_DEPRECATED(VERSION, METHOD) __attribute__((deprecated("This method has been deprecated and will be removed in BSKit " VERSION ". Please use `" METHOD "` instead.")))

cocopod异常记录

1.

CocoaPods正确升级后,pod install后出现了is not used in any concrete target的提示结果,就是不能成功安装第三方的类库,查询后得知原理最新的cocoapods需要添加两行说明
如:

1
2
3
4
5
6
7
8
platform :ios, ‘7.0‘

target "AspectsDemo" do #加上这一行,引号内的事工程名称

pod 'Aspects'
pod 'AFNetworking'

end #再加上这一行

然后一切恢复正常!

案例积累(1)

今天项目有一个比较特殊的需求,想特此记录一下解决过程,进行备注并有机会进行优化更新!

大致是这样:

在一个导航控制器的栈顶控制器开始计数,从这里开始往后push的控制器都叫单品控制器(需求说是叫这个,实际就是一个商品详情页面),如果单品控制器超过2个后,用户点按返回的时候,只能看到倒数第二个单品控制器,到了倒数第二个单品控制器,再返回,就返回到开始计数前的那个控制器了。
需求图如下:
experience001

最后实现了的效果图:
experience003

开始,写了个demo,写了个解决方法

  • demo内容有:
  1. 建立一个根控制器充当开始计数之前的那个控制器(RootViewController)
  2. 建立单品控制器(MainViewController),也就是个商品详情页
  3. keyWindow的根控制器为一个导航控制器
    如图所示:
    experience002
  • demo的参数和方法设置:
  1. AppDelegate里面设置了一个全局变量goodsCount用于计数单品的数量

    1
    2
    /** 记录工程中有多少个单品页 */
    @property (nonatomic, assign) NSInteger goodsCount;
  2. RootViewController里面有一个按钮可以push到第一个单品页

    1
    2
    MainViewController * mainVc = [[MainViewController alloc] init];
    [self.navigationController pushViewController:mainVc animated:YES];
  3. 单品控制器(MainViewController)里重写init方法,使全局变量goodsCount++

    1
    2
    3
    4
    5
    6
    7
    - (instancetype)init{
    self = [super init];
    if (self) {
    [AppDelegate delegate].goodsCount++;
    }
    return self;
    }
  4. MainViewController 页面有一个按钮可以push到下一个控制器
    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    MainViewController * mainVc = [[MainViewController alloc] init];
    NSLog(@"【Push前单品页的个数】= %ld", [AppDelegate delegate].goodsCount);

    if ([AppDelegate delegate].goodsCount > 2) {
    for (NSInteger i = 0; i < [AppDelegate delegate].goodsCount - 1; i++) {
    [self.navigationController popViewControllerAnimated:NO];
    }
    [AppDelegate delegate].goodsCount = 1;
    }
    [self.navigationController pushViewController:mainVc animated:YES];
  5. 单品返回(pop)的时候代码如下:

    1
    2
    [AppDelegate delegate].goodsCount--;
    [self.navigationController popViewControllerAnimated:YES];

先不去求证这个解决方法的算法本身,刚看的直觉来说,这种某一个具体业务功能的需求最好还是不用AppDelegate这种全局单例去做一些辅助性的工作,觉得不太好,最好是业务本身就能解决自己的问题,本着这个思想,又具体看了下哥们的实现算法的思路,首先发现两个问题所在:

问题一,MainViewController中push到下一个控制器,循环pop中的self在pop出去之后应该会释放掉
问题二,这个方法实现不了需求中的只是留下最后两个控制器如图:

experience004

可见,如果把1、2都pop出去并没有达到预期的目的:剩最后两个

改进的解决方案:

让每下一个单品创建出来之后,有一个preVc的属性可以引用它的上一个单品控制器
先上图:
experience005
此时我们在MainViewController的.h文件中加上对上一个控制器的引用preVc:

1
@property (nonatomic, strong) MainViewController *preVc;

然后更改MainViewController跳转到下一个控制器的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
UINavigationController *nav = (UINavigationController *)[AppDelegate delegate].window.rootViewController;

MainViewController * mainVc = [[MainViewController alloc] init];
mainVc.preVc = self;

if (nav.childViewControllers.count-1 == 2) {

[nav popViewControllerAnimated:NO];
[nav popViewControllerAnimated:NO];

[nav pushViewController:mainVc.preVc animated:NO];
[nav pushViewController:mainVc animated:YES];

mainVc.preVc.preVc = nil;
} else {
[nav pushViewController:mainVc animated:YES];
}

最后,因为push的控制器都会引用前一个控制器,所以需要mainVc.preVc.preVc = nil;进行释放。
至此,完成需求,大功告成!^ _ ^

再次当爸的感觉

  • 2016年6月16日,初诊俺家媳妇儿怀孕了,我这个心情啊,激动、幸福、担忧、顾虑各种情绪涌动,想想我们刚领证一个月,半年前那次怀孕最后决定没要后悔的要死,媳妇儿也是创伤了很久,这次如果能确诊媳妇儿怀了小宝宝,我真的真的感谢上苍TV,我会更加努力为国家多交税,为人民多服务的!

  • 从2012年4月27日,我大学的恋爱史开始这天,到2014年8月11日我踏上天朝京都的北漂之路为自己的未来而奋斗,再到2015年7月媳妇儿也来到这里,再到今天,我们的感情史诗不能说惊天动地,轰轰烈烈,但的确也是分分合合,历久弥坚,爱深情重,最终修成正果,实属不易,每次想到这里都是一小段感慨人生。

  • 不管怎么说,我有熊孩子了,像媳妇儿说的,生个熊孩子玩玩儿,😄,想来奶爸的感觉也是很不错的,我要给他起个跟我一样霸气的名字去^_^

制作静态库

简单记录一下 静态库 的制作过程,静态库可以有.a和.framework两种形态:

  1. 直接制作
  2. 在已有工程添加依赖时生成
    本次配置:XCode 7.3.1, OS 10.11.5

.a静态库制作

0.建立静态库工程

  • 打开Xcode,并新建(File->New->Project或者Shift+N)工程,选择静态库,如图:
    newLib01
  • 下一步:
    newLib02
  • 完成:
    newLib03
  • 给建立好的工程导入一个我自己写的坐标视图的库
    newLib04
  • 默认会有一个跟工程同名的类,可以直接删掉不用,也可以进行使用,我就不删了,并往里面添加了两个方法:
    newLib04

1.设置配置参数

Debug:Yes表示只编译选中模拟器对应的架构,No则为编译所有模拟器支持的cpu架构(Debug的Yes状态改为No即可)
newLib13

2.开发API和各种功能函数

可以导入或者新建开发出要封装起来的功能函数,并提供API到头文件中去
newLib05
newLib06
newLib07
newLib08

3.编译并生成静态库

Command+B 在模拟器环境和真机环境下的debug和release状态下分别编译,生成一共是4个.a静态库文件,如图:
newLib09
(1)分别选择真机和模拟器进行编译
(2)首先找到真机编译好的文件,Show in Finder
(3)再找到模拟器编译的文件
newLib10

4.使用生成的静态库

如果直接使用一个环境下(比如是模拟器、debug环境下编译的)静态库文件,在其他环境下会报错,因为架构不同,所以要进行下一步操作才行

5.合并.a文件制作通用静态库

(1)终端命令
lipo -create libStaticTest.a(真机.a文件路径) libStaticTest.a(Iphone6模拟器.a文件路径) -output 保存路径/StaticTest.a
(2)生成的.a文件就是要用的文件了

6.注意事项

.frame 静态库的生成

1.

2.

3.

runtime从原理到实践之原理篇(1)

概要

这个系列的博文我们依次会接触到 Objective-C类对象、对象、元类、isa、IMP、KVO/KVC、runtime等等,每个概念都会去探根问底,因为每个小细节都可能让人云里雾里,这次我们要一起把底层原理层面的东西搞搞清楚😂

runtime基本认知

从开始从事iOS那天起,就开始听说runtime,后来有点时间就会去看看这方面的博客、论坛和demo,但不造是时间有限还是原理太深,每次看都是蜻蜓点水,浅尝辄止,也从来没在实际开发总用到过 (当然,唯一用到就是KVO) ,这一次想花时间系统归纳整理以往的知识加以总结,runtime就是不可回避的一个专题了。。。闲话不多说了,先捋清楚几个相关的基本概念

1. 运行时

在苹果的Cocoa和Fundation框架里,运行时指的即是runtime了,但是,从代码从编写到加载到内存并且跑起来的角度看,其实就是指加载到内存后开始运行的一个阶段(编译-连接-运行)

2. 动态编程语言(像OC这种)

动态编程语言不单单有我们的Objective-C,其他有啥大家自行Google,总之有一点,就是:运行时再决定对象的类型、绑定方法和属性以及动态加载资源(如2x和3x图片)

3. 消息机制

消息机制的转发是runtime乃至整个OC开发的基石,正因为有它,所以OC的运行时才得以构建,简单来说就是某个类(或者实体)调用某个方法,就是给该类发一条名称为此方法名的消息,如果得以处理则执行,不处理就进行转发,直到该消息被执行或者抛出异常为止。
具体的传递过程可以参考这里

4.runtime

要说runtime是什么,用苹果官方的说法,叫做:The runtime system is a dynamic shared library with a public interface consisting of a set of functions and data structures in the header files located within the directory /usr/include/objc.
大致意思就很明显了 runtime是苹果用C/C++封装的一个动态库,用来提供一系列功能函数和结构体数据,这个库的路径也说了在/usr/include/objc路径下。
在这个路径下苹果用C的API封装了动态的创建类(方法)、改变方法实现的很多C函数。

5.isa

这个比较关键,isa就是一个指针,这个指针的类型是Class,其实就是objc_class这个结构体类型,这个结构体在runtime中定义了一个类也就是OC中的Class了,其中包含了如图的属性:

runtime000

而OC中生成的实例对象也是一种结构体指针,即:objc_object

runtime001

可以清楚的看到,里面只有一个isa指针指向生成它的类,下面图中的结构也可以看出。

6. 对象、类和元类

这个属于OC基础了,因为一切皆对象,所以类也是一种实体对象会加载到内存中,既然它是对象,那么它也有类就是所谓的元类。而类本身用C语言表示的话就是一个结构体,里面有各种属性,第一个就是isa指针。
我们先上图看下OC中这三者之间的关系:

runtime002

从图中我们可以得知:

(1)NSObject的类中定义了实例方法,例如:-(id)init;-(void)dealloc;
(2)NSObject的元类中定义了类方法,如:+(id)alloc;+(void)load;
(3)所有类都继承自NSObject,包括NSObject的元类,所以NSObject是所有类的根,所谓:NSObject中定义的实例方法可以被所有对象调用
(4)NSObject的元类的isa指向它自己

2016年学习计划

吐槽自己

当前H5/JS技术这么火热,所以最近两、三个月折腾了好多跟我开发工作没有多大关系的技术:

  1. UI: Html5、CSS3
  2. 交互: JavaScript
  3. 数据: ejs、jade模板引擎
  4. 服务器: Linux操作系统、Node.js、PHP
  5. 混合技能: Wordpres个人博客>Hexo学习搭建个人博客、markdown使用
  6. 开发工具: WireShark、Atom、Sublime、HBuilder、Mou、IDEA、Eclipse

学的东西很杂也不深入,虚飘飘的,感觉iOS的技术都生疏了。。。无非是怕落伍被淘汰,但是折腾了一圈之
后,还是觉得要先把OC和Swift的优势巩固,所以反省了下自己最后得出的结论是:iOS大法好啊,还是继
续深入研究OC和Swift

鉴于此,我决定还是回归本尊,既不能捡了芝麻丢西瓜,也不能不睁眼环视周围的世界,就制定一个自己的学
习计划,按部就班的一个一个去实施,感觉会扎实很多!

时间计划表

Time Content Target
3月份 iOS/H5 draw 精(博客总结)
4月份 iOS runtime/Node.js 精(博客总结)
4月份 消息机制 精(博客总结)
5月份 iOS autoLayout 精(博客总结)
6月份 Html5、JavaScript应用 熟(博客总结)

yilia 主题学习

hexo-theme-yilia

Yilia 是为 hexo 2.4+制作的主题。
崇尚简约优雅,以及极致的性能。 你可以点击 我的博客 查看效果。

如遇到问题或有需求,可以:

我都会看到并处理。

如果你想体验手机浏览效果,可以扫一下二维码:

litten-qrcode

—————————————————————

关于主题:

  1. 我喜欢简约。所以近期文章,搜索框都拿掉了
  2. 接地气一点。所以用上了jiathis分享,友言评论,以及baidu的cdn
  3. 追求移动端的体验
  4. 让大家把注意力放到内容上。这是本主题设计初衷
  5. 主题不支持IE6,7,8。以后也不会

一、近期更新

2015.6.14 - 模块化加载
2015.2.21 - 移动侧重构&布局bug修改
2014.11.7 - 增加“友情链接”“关于我”
2014.10.22 - 优化fancybox展示
2014.10.16 - 增加表格样式
2014.9.19 - 云标签挂件

二、使用

安装

1
$ git clone https://github.com/litten/hexo-theme-yilia.git themes/yilia

配置

修改hexo根目录下的 _config.ymltheme: yilia

更新

1
2
cd themes/yilia
git pull

三、外观

宽屏

宽屏

宽屏文字

宽屏文字

窄屏

窄屏

同步instagram

同步instagram

移动端

移动端
移动端

四、配置

主题配置文件在主目录下的_config.yml

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
# Header
menu:
主页: /
所有文章: /archives
# 随笔: /tags/随笔

# SubNav
subnav:
github: "#"
weibo: "#"
rss: "#"
zhihu: "#"
#douban: "#"
#mail: "#"
#facebook: "#"
#google: "#"
#twitter: "#"
#linkedin: "#"

rss: /atom.xml

# Content
excerpt_link: more
fancybox: true
mathjax: true

# Miscellaneous
google_analytics: ''
favicon: /favicon.png

# 你的头像url
avatar: ""
# 是否开启分享
share: true
# 是否开启多说评论,填写你在多说申请的项目名称 duoshuo: duoshuo-key
# 若使用disqus,请在博客config文件中填写disqus_shortname,并关闭多说评论
duoshuo: true
#是否开启云标签
tagcloud: true

# 是否开启友情链接
# 不开启——
# friends: false
# 开启——
friends:
奥巴马的博客: http://localhost:4000/
卡卡的美丽传说: http://localhost:4000/
本泽马的博客: http://localhost:4000/
吉格斯的博客: http://localhost:4000/
习大大大不同: http://localhost:4000/
托蒂的博客: http://localhost:4000/

#是否开启“关于我”。
#不开启——
#aboutme: false
#开启——
aboutme: 我是谁,我从哪里来,我到哪里去?我就是我,是颜色不一样的吃货…

##五、其他

同步你的instagram图片

markdownStudy

markdown

markdown icon

overView

Mou , the missing Markdown editor for web developers.

Syntax

Strong and Emphaisze

strong or strong (Cmd+b)

emphasize or emphasize ( Cmd + I )

Sometimes I want a lot of text to be bold.
Like, seriously, a LOT of text

Blockqutoes

Right andgle brackets > are used for block queote

An email expmple@example.com link.

Simple inline link , another inner link SM
with title Resize.

A reference style link. Input id, then anywhere in the doc,define the link
with corresponding id:
Titles (or called tool tips) in the links are optional.

插入图片

An inline image Smller icon, title is optional.
A Resize icon reference style image.

插入代码块

Inline code are surround by backtick key. To create a block code.
Indent each line by at least 1 tab, or 4 apaces.
var Mou = exactlyTheAppIwant;

有序列表

Ordered lists are created using “1.”+空格

  1. 第一项
  2. 第二项
  3. 第三项

无序列表

无序列表用 * + 空格 进行创建

  • 汽车
  • 飞机
  • 轮船
    也可以用 - + 空格 进行创建
  • 男人
  • 女人
强制换行

End a line with two or more spaces will create a hard linebreak,
called <br /> in HTML. ( Control + Return )
Above line ended with 2 spaces.

水平分割线

zhe


头信息

Setext模式

这是标题一

这是标题二

atx模式
“#”格式开头

Extra Syntax

Footnotes

Footnotes work mostly like reference-style links. A footnote is made of two things: a marker in the text that will become a superscript number;
a footnote definition that will be placed in a list of footnotes at the end of the document. A footnote looks like this:
That’s some text with a footnote.[^1]
[^1]: And that’s the footnote.

Strikethrough

Wrap with 2 tilde characters:
Strikethrough
删除线

Fenced Code Blocks

Start with a line containing 3 or more backticks, and ends with the first line with the same number of backticks:

1
2
3
Fenced code blocks are like Stardard Markdown’s regular code
blocks, except that they’re not indented and instead rely on
a start and end fence lines to delimit the code block.

表格
First Hea网络r Second Header Third Header 第四个
Content Cell Content Cell Content Cell 死cell
Content Cell Content Cell Content Cell cell

If you wish, you can add a leading and tailing pipe to each line of the table:

First Header Second Header Third Header
Content Cell Content Cell Content Cell
Content Cell Content Cell Content Cell

Specify alignment for each column by adding colons to separator lines:

First Header Second Header Third Header
Left Center Right
Left Center Right

And more?

Don’t forget to check Preferences, lots of useful options are there.

Follow @Mou on Twitter for the latest news.

For feedback, use the menu Help - Send Feedback

firstHexoStudy

一直想用一个好用顺手的博客系统记录自己的点点滴滴和求学之路,就是没有找到好的方式,几周的时间
之后,终于把脚落定在hexo这篇热土之上,在此真心感谢hexo的作者 Tommy Chen

1.blockquote - 块的使用

这是blockquote(1)

这是blockquote(2)

I love you forever!

异类Halsen

2.Code Block 代码块

(1)代码块 lang声明语言

1
[arr addObjec:person];

(2)显示代码title

array.map
1
array.map(callBack[,thisArg]);

(3)显示相关链接

daqiangdaqiang.html
1
2
daqiang([0,1,false,2,'',3])
=> [1,2,3]

3.Pull Quote

这个插件可以帮助 在文章中插入重要引述。

pullquote content

4.jsFiddle

null

5.gist

6.iFrame

7.image

textContent

9.Include Code

10.Vimeo 在文章中插入Vimeo视频。

11.Include Posts 包含其他文章的链接

12.Include Assets 包含文章资源。

13.Raw 一些内容不想被主题渲染,可以使用该插件呈现原始状态。

content content