先看下面这个例子,如果能回答出这个strings.xml最终显示的结果,本文也不用看了。

<resources>

<string name="test1">test "string"</string>
<string name="test2">test \"string\"</string>
<string name="test3">test &quot;string&quot;</string>

<string name="test4">test 'string'</string>
<string name="test5">test \'string\'</string>
<string name="test6">test &apos;string&apos;</string>


</resources>

上面例子显示结果如下:

test1 : test string
test2 : test "string"
test3 : test string

test4 : test 'string' //IDE lint 报错
test5 : test 'string'
test6 : test 'string' // 新GAP编译报错

测试环境:

  • gradle android plugin version: 3.1.0
  • gradle version 4.4

(注意,最新的GAP>=3.3.2后,用&apos;转义'不会在IDE lint中报错,但是会在mergeResourcestask执行时报错)

TLDR

在android的strings.xml中的value转义规则如下

  1. " 转义只能用\", 不能用&quot;,不转义的话不会报错,最终显示结果"会被吞掉

  2. ' 转义既可以用&apos;, 也能用\', 不转义的话 AS lint 会报错

  3. > 不用转义

  4. < 必须转义为&lt;,不转义的话 AS lint 会报错

  5. & 必须转义为&amp;,不转义的话 AS lint 会报错

  6. @? 可以不用转义

在xml的定义中,下面特殊字符因为有特殊的用处,因此当其出现在node value中时需要转义,转义规则如下。

& - &amp;
< - &lt;
> - &gt;
" - &quot;
' - &apos;

比如在groovy的 groovy.xml.XmlUtil中对xml的text的转义如下

public static String escapeXml(String orig) {
return StringGroovyMethods.collectReplacements(orig, new Closure<String>((Object)null) {
public String doCall(Character arg) {
switch(arg) {
case '"':
return "&quot;";
case '&':
return "&amp;";
case '\'':
return "&apos;";
case '<':
return "&lt;";
case '>':
return "&gt;";
default:
return null;
}
}
});
}

可以看到这里按照xml的规定处理了所有的转义。

android的string资源也是定义在xml中的,在官方文档中也有说明这些特殊符号如何转义。

可以见到这里对转义的处理有些不一样。这是因为android中的string既可以直接用在xml attribute中,也可以注册在strings.xml文件中。所以上面列举了双引号和单引号的几种不同处理方式。

再来看下android util 包下处理escapeText的函数

com.android.utils.XmlUtils.java

/**
* Converts the given attribute value to an XML-text-safe value, meaning that
* less than and ampersand characters are escaped.
*
* @param textValue the text value to be escaped
* @return the escaped value
*/
@NonNull
public static String toXmlTextValue(@NonNull String textValue) {
for (int i = 0, n = textValue.length(); i < n; i++) {
char c = textValue.charAt(i);
if (c == '<' || c == '&') {
StringBuilder sb = new StringBuilder(2 * textValue.length());
appendXmlTextValue(sb, textValue);
return sb.toString();
}
}

return textValue;
}

/**
* Appends text to the given {@link StringBuilder} and escapes it as required for a DOM text
* node.
*
* @param sb the string builder
* @param start the starting offset in the text string
* @param end the ending offset in the text string
* @param textValue the text value to be appended and escaped
*/
public static void appendXmlTextValue(
@NonNull StringBuilder sb, @NonNull String textValue, int start, int end) {
for (int i = start, n = Math.min(textValue.length(), end); i < n; i++) {
char c = textValue.charAt(i);
if (c == '<') {
sb.append(LT_ENTITY);
} else if (c == '&') {
sb.append(AMP_ENTITY);
} else {
sb.append(c);
}
}
}

可以看到这里并不是按照规范对全部特殊符号做了转义,这里只处理了'<' '&'。剩下的>"' 保留不动。这是因为其他的几种特殊字符,都是通过\来实现转义的。

测试后发现的规律总结如下:

  1. " 转义只能用\", 不能用&quot;,不转义的话不会报错,最终显示结果"会被吞掉

  2. ' 转义 既可以用&apos; 不能用&quot;会在编译期报错, 只能用\', 不转义的话 AS lint 会报错

  3. > 不用转义

  4. < 必须转义为&lt;,不转义的话 AS lint 会报错

  5. & 必须转义为&amp;,不转义的话 AS lint 会报错

  6. @? 可以不用转义

使用场景:

在通过插件或脚本处理strings.xml文件时,如果需要处理string value,一定不能按照xml的语言规范去转义,否则用于转义双引号的\"Android写法会被标准实现转义处理为&quot;,同理还有\'会被处理为&apos;,从而导致编译失败或者显示在Android上时,字符被吞掉的情况。

所以最保险的做法在写android Groovy插件处理xml时,请认准com.android.utils.XmlUtils.java