String和Color的硬编码多达3千处,咋办?

2023-10-08 01:50

本文主要是介绍String和Color的硬编码多达3千处,咋办?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

bauerbao 博客地址:

https://www.jianshu.com/u/fe199bb130bf

最近接手了一个老项目,代码经过N手,里面要啥有啥,好多库都是多份的,就算同一个库也存在着不同的版本,连源码都依赖了API 27和28两个版本。其中最难受的就是color和国际化字段string,全部以硬编码的形式存在于项目中。

大概检测了下,string和color的硬编码多达3K处,如果哪天要求支持多语言,接手的人肯定哭死。所以本文主要讲color和string的硬编码如何优化。

最容易想到的就是一个个cut/copy去改。有兴趣的话,大家可以尝试下。
幸好AS有个快捷方法可以处理。

1. 点击目标位置行首的小灯泡

2. 点击"Extract string resource"

3.填写对应的信息即可完成快捷插入的操作

ennnn...确实很快捷,快的话,一条记录10s,3k多条记录也就2万秒......

那有没有更快捷的方法么?有,用插件或脚本,但是有局限性。

首先Android工程主要代码一般存在于java和res文件夹中。java中的string和color资源如果要被引用的话,就要涉及到context.getString(id)、context.getColor(id)、ContextCompat.getColor(context, id)这几个方法,都和context有关。

如果要插件/脚本去执行替换的话,context对象难以保证和代码中的申明一致。所以局限性就是只能处理xml中的string/color。目前市面上有LayoutFormat(https://github.com/shang1101/LayoutFormat_androidstudioplugin):支持string、dimen、格式化。StringKiller(https://github.com/zhouzhuo810/StringKiller):只支持string,还没发布到市场,同时一大堆bug。

效果

先看下最终效果,再决定要不要继续看下去。也可以直接看源码SrcHardCodeOptimizeUtil(https://github.com/bauer-bao/SrcHardCodeOptimizeUtil)。插件请搜索"XML Hard-code Optimize"

 准备工作

1. 下载IntelliJ IDEA https://www.jetbrains.com/idea/download/#p=mac,选择Community免费版即可。

2. Create New Project,选择IntelliJ Platform Plugin,然后Next,然后填入基本信息,然后Finish即可

3. 新工程页面如下,红框是注意点

4.修改plugin.xml文件中的<id><name><version><vendor><description><change-notes>标签内容,可以自由发挥,文体不限。

5. 目前src目录下还是空的,新建package:com.xxx.xxx。

6. 需要确定插件该显示在哪里,要显示几个菜单。这里,我们分两个按钮,一个处理string,一个处理color。然后在工程文件中,右击弹出菜单即可。所以此处我们需要新建2个action。


在步骤5新建的package下,新建两个Action。

说明:

Action ID:id,保证唯一即可
Class Name:类名
Name:菜单中显示的名称
Description:描述
Groups:所属的组,ProjectViewPopupMenu就是右击弹出的菜单栏
Anchor:最前/最后/上一个/下一个。其中上一个/下一个是以Actions为准
Keyboard Shortcuts:快捷键,可以在AS 快捷键中统一设置

7. 不做任何处理的话,两个action的菜单按钮是平铺在父级菜单下的,需要归到同一组中,代码如下:

<actions><group id="xml-hard-code-optimize" text="Optimize Hard Code" popup="true"description="Optimize the hard codes to strings.xml or colors.xml"><add-to-group group-id="ProjectViewPopupMenu" anchor="last"/><action id="string-optimize" class="com.srchardcodeutil.action.StringOptimizeAction" text="String Optimize"description="Optimize the hard codes to strings.xml"/><action id="color-optimize" class="com.srchardcodeutil.action.ColorOptimizeAction" text="Color Optimize"description="Optimize the hard codes to colors.xml"/></group>
</actions>

popup必须为true,否则没有效果。

8. 点击运行,可以看下效果

关键点

在代码编写之前,先缕一下思路,确定下有多少难点待解决。


1.哪几个层级可以执行插件?
2.怎么去查找硬编码?
3.找到之后如何替换?

附:string优化和color优化大同小异,此文均以string为例

一般目标xml文件都在res/layoutXXX/或者res/drawableXXX/下面。所以兼容res | drawableXXX/layoutXXX | XXX.xml这三层,当然这是最方便快捷的了。(关键点1方案)

进入之前新建的Action类中发现,默认继承AnAction类,并且重写了actionPerformed的方法。这个方法也是插件执行的入口。首先需要获取当前执行的是哪一层的文件,如果不是目标层级文件,就提示Error。其中VirtualFile是IntelliJ Platform’s 虚拟文件系统的文件,功能比File更强大。具体代码如下:

    VirtualFile file = e.getData(PlatformDataKeys.VIRTUAL_FILE);if (file == null) {//文件不存在Util.showError("File is not exist");return;}VirtualFile parentFile = file.getParent();if (file.isDirectory()) {//是文件夹if (!file.getName().startsWith("layout") && !file.getName().equals("res")) {//不支持Util.showError("Operation is not support");return;}} else {//是文件if (!parentFile.getName().startsWith("layout")) {//不支持Util.showError("Operation is not support");return;}}

获取操作的文件之后,就需要遍历文件/文件夹,并且对内容进行扫描。此处使用DocumentBuilderFactory将文件流转成Node,之后对这个node进行扫描,找到特定字符串。

那么如何去找到node中的硬编码呢?因为string的文案,千变万化,毫无规律可言,所以没法从string的规律去查找,只能从指定的属性去查找。Android中string比较多的属性,就是android:text和android:hint属性。所以,也就只处理这两个属性。但是color和string稍有不同,后文会讲解。(关键点2方案)


关键代码如下:

private List<Entity> generateStrings(Node node, List<Entity> strings, String fileName, StringBuilder oldContent) {if (node.getNodeType() == Node.ELEMENT_NODE) {//处理text属性String targetItem = "android:text";oldContent = scanNode(node, fileName, oldContent, strings, targetItem);//处理hint属性targetItem = "android:hint";oldContent = scanNode(node, fileName, oldContent, strings, targetItem);}//继续遍历子节点NodeList children = node.getChildNodes();for (int j = 0; j < children.getLength(); j++) {generateStrings(children.item(j), strings, fileName, oldContent);}return strings;
}private StringBuilder scanNode(Node node, String fileName, StringBuilder oldContent, List<Entity> strings, String targetItem) {Node stringNode = node.getAttributes().getNamedItem(targetItem);if (stringNode != null) {String value = stringNode.getNodeValue();if (value.length() > 0 &&!value.contains("@string") &&!(value.startsWith("@{") && value.endsWith("}"))) {//为空,或者已经有@string 或者是 databinding的样式,就不需要处理,反之需要处理List<Entity> queryList = Lists.newArrayList();queryList.addAll(entityList);queryList.addAll(strings);String targetId = null;//检查当前的value是否已经存在,entityList是已经遍历过文件的列表,strings是当前遍历的文件的列表for (Entity entity : queryList) {if (entity.getValue().equals(value)) {//已经存在的valuetargetId = entity.getId();break;}}if (targetId == null || targetId.length() == 0) {//不存在targetId = fileName + "_text_" + (index++);strings.add(new Entity(targetId, value));}int index = 0;while (index < oldContent.length() && index >= 0) {//关键方法index = Util.getRightIndex(oldContent, value, targetItem, 0);if (index != -1) {//说明找到对应的值 +2 是因为替换的是 "value",而不是valueoldContent = oldContent.replace(index, index + value.length() + 2, "\"@string/" + targetId + "\"");//继续查找下一个值index += value.length();}}}}return oldContent;
}

其中有个关键方法getRightIndex,总共考虑了3个方案。

方案1:


通过遍历node很容易就找到待替换的值。但是如果直接将值设置到node,再重写node的tostring方法,可以得到修改之后的内容,这样源文件的原有代码也就会被改变(比如会格式化原有代码)。因为考虑尽量不改动原有代码,只替换目标值,所以放弃了此方案。


方案2:


使用正则表达式。因为待替换的值毫无规律,可能存在和正则关键字一样的字符,导致匹配出错。其次,正则不易维护,不易读。在尝试了N久之后,最终放弃了。

采用方案3(关键点3方案):


a.使用String.indexOf,找到待替换值的index
b.查找index之前几位,是否包含android:text=或者android:hint=的字符串。如果包含,则说明找到了目标index,反之,则继续查找下一个index,直到找到为止。


注意:以android:text="aaa"为例。因为xml中属性标准写法如上,但是由于开发人员的不规范,=号两边可能会有N个空格,需要过滤掉空格,但是:号两边一定不会有空格,否则系统直接标红。

所以实现逻辑:对字符串从index开始,不断往前去循环,依次比对是否和android:text=一致,需要过滤空格。如果全部匹配,则符合要求。代码如下:

public static int getRightIndex(StringBuilder content, String value, String targetItem, int fromIndex) {int index = content.indexOf("\"" + value + "\"", fromIndex);if (index == -1 || isRightIndex(content, targetItem, index - 1)) {//没有找到 或者正确的index,直接返回return index;} else {//继续查找下一个合适的indexreturn getRightIndex(content, value, targetItem, index + value.length());}
}private static boolean isRightIndex(StringBuilder content, String targetItem, int index) {//正常的属性类似android:text= 所以需要加上=String targetItemStr = targetItem + "=";int targetIndex = targetItemStr.length() - 1;boolean isRight = true;for (int i = index; i > 0; i--) {char cur = content.charAt(i);if (cur != ' ') {//不为空格if (cur == targetItemStr.charAt(targetIndex)) {//继续匹配上一个值targetIndex--;if (targetIndex < 0) {//全部匹配完break;}} else {//不一致,则当前index不是正确的indexisRight = false;break;}}}return isRight;
}

至此,待替换的位置,已经找到,替换就容易多了。代码如下:

index = Util.getRightIndex(oldContent, value, targetItem, 0);
if (index != -1) {//说明找到对应的值 +2 是因为替换的是 "value",而不是valueoldContent = oldContent.replace(index, index + value.length() + 2, "\"@string/" + targetId + "\"");
}

遍历完node之后,将会把结果记录到List<Entity>中,并且将替换之后的内容保存到文件。在strings.xml中是以<string name="xxx">xxx</string>的形式存在,所以一个是name,一个是value。Entity就是name和value的实体类。

遍历完整个文件夹之后,会将List<Entity>保存到strings.xml文件,代码如下:

public static void saveToFile(VirtualFile parentFile, StringBuilder sb, boolean isString) {if (sb == null || sb.length() == 0) {return;}//获取父文件夹,找到string.xml,并且将字段添加到文件if (parentFile.getName().equalsIgnoreCase("res")) {//如果是res资源,则开始处理VirtualFile[] resDirChildren = parentFile.getChildren();for (VirtualFile child : resDirChildren) {if (child.getName().equals("values") && child.isDirectory()) {//找到values目录,因为从layout中拿到的string,肯定只有一种语言,因此只在values中生成一份string.xml,不处理其他values文件夹VirtualFile[] valuesChildren = child.getChildren();//处理values文件夹boolean exist = false;for (VirtualFile value : valuesChildren) {if (value.getName().equals(isString ? "strings.xml" : "colors.xml")) {//找到strings.xml或者colors.xmlexist = true;try {//将文件转成stringString content = new String(value.contentsToByteArray(), StandardCharsets.UTF_8);//替换成最新的stringString result = content.replace("</resources>", sb.toString() + "\n</resources>");//将内容全部写入文件中saveContentToFile(value.getPath(), result);} catch (IOException e1) {e1.printStackTrace();}break;}}if (!exist) {//目标文件不存在,新建文件File file1 = new File(child.getPath(), isString ? "strings.xml" : "colors.xml");//生成内容String content = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n   " + sb.toString() + "\n</resources>";//保存到文件saveContentToFile(file1.getPath(), content);}break;}}}
}

这时候string优化代码已经结束。color优化代码大同小异,值得注意的地方有如下两点:

1.因为color规律明显,一般值存在#xxx | #xxxxxx | #xxxxxxxx 三种形态,因此在scanNode方法中,可以直接使用正则去匹配,并且兼容全部属性。正则:^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$


2.svg.xml文件一般由UI提供,因此没必要去处理里面的color值

有三种方式去使用自己的插件。

1.运行调试(原则上不算)


2.Build->Prepare Plugin Module "xxx" For Deployment,之后就会在工程下自动生成对应的xxx.jar,然后打开AS,进入插件搜索页面,选择Install Plugin from Disk...,然后选择xxx.jar,重启AS即可。


3.上传到插件市场,再下载,再重启AS即可。

如果一不小心重启之后,AS一打开就爆红。当时忘了截图,盗用网上的图片。

只要删除安装的插件即可,反正只要删除插件就行,哪怕重装AS都行。
如果是Mac环境,/Users/xxx/Library/Application/Support/AndroidStudio3.4/SrcHardCodeOptimizeUtil.jar,删除即可。

如果出现错误 error:java: 无效的源发行版: 11,在设置中将Language level改小即可,目前主流是java8。

  如何发布  

进入https://plugins.jetbrains.com/,注册,上传即可。

注意:人工审核,在2个工作日给回复。很有可能发邮件给你确认个人信息,需要回复邮件。我将plugin.xml中的description、change log、vender等内容复制黏贴,结果就成功上架了

插件介绍

说了大半天,SrcHardCodeOptimizeUtil到底怎么用?有什么注意的地方?


1.插件市场下载请搜索"XML Hard-code Optimize"


2.支持对res | drawableXXX/layoutXXX | XXX.xml 执行插件


3.如果执行string选项的话,只有android:text和android:hint两个属性可以被识别


4.如果执行color选项的话,类似于#xxx/#xxxxxx/#xxxxxxxx的值都可以被识别


5.默认只判断values文件夹中的strings.xml/colors.xml,如果不存在,会主动创建


6.已过滤databinding的样式


7.colors优化操作已对svg的vector | group | path标签过滤


8.colors.xml中的name以color_xxx来命名,strings.xml中的name以 layout's name_text_index 来命名,index在单个文件中会自增长

9.没有添加默认快捷键,如需要,可在AS中自行添加


10.记得使用前备份代码

结语

虽然没法对整个工程的硬编码进行处理,但是已经很快捷的处理了至少一半的任务量。

                        喜欢 就关注吧,欢迎投稿!

这篇关于String和Color的硬编码多达3千处,咋办?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/161875

相关文章

C++ | Leetcode C++题解之第393题UTF-8编码验证

题目: 题解: class Solution {public:static const int MASK1 = 1 << 7;static const int MASK2 = (1 << 7) + (1 << 6);bool isValid(int num) {return (num & MASK2) == MASK1;}int getBytes(int num) {if ((num &

C语言 | Leetcode C语言题解之第393题UTF-8编码验证

题目: 题解: static const int MASK1 = 1 << 7;static const int MASK2 = (1 << 7) + (1 << 6);bool isValid(int num) {return (num & MASK2) == MASK1;}int getBytes(int num) {if ((num & MASK1) == 0) {return

form表单提交编码的问题

浏览器在form提交后,会生成一个HTTP的头部信息"content-type",标准规定其形式为Content-type: application/x-www-form-urlencoded; charset=UTF-8        那么我们如果需要修改编码,不使用默认的,那么可以如下这样操作修改编码,来满足需求: hmtl代码:   <meta http-equiv="Conte

三色标记(Tri-color marking)

维基百科部分 原文 https://en.wikipedia.org/wiki/Tracing_garbage_collection#TRI-COLOR Because of these performance problems, most modern tracing garbage collectors implement some variant of the tri-color ma

string字符会调用new分配堆内存吗

gcc的string默认大小是32个字节,字符串小于等于15直接保存在栈上,超过之后才会使用new分配。

4-4.Andorid Camera 之简化编码模板(获取摄像头 ID、选择最优预览尺寸)

一、Camera 简化思路 在 Camera 的开发中,其实我们通常只关注打开相机、图像预览和关闭相机,其他的步骤我们不应该花费太多的精力 为此,应该提供一个工具类,它有处理相机的一些基本工具方法,包括获取摄像头 ID、选择最优预览尺寸以及打印相机参数信息 二、Camera 工具类 CameraIdResult.java public class CameraIdResult {

Python字符编码及应用

字符集概念 字符集就是一套文字符号及其编码的描述。从第一个计算机字符集ASCII开始,为了处理不同的文字,发明过几百种字符集,例如ASCII、USC、GBK、BIG5等,这些不同的字符集从收录到编码都各不相同。在编程中出现比较严重的问题是字符乱码。 几个概念 位:计算机的最小单位二进制中的一位,用二进制的0,1表示。 字节:八位组成一个字节。(位与字节有对应关系) 字符:我们肉眼可见的文字与符号。

在Eclipse环境下修改Tomcat编码的问题

问题: 由于BMS需要设置UTF-8编码,要不就会出现中文乱码问题; 一、项目保持UTF-8格式; 二、由于可能会多次移除项目、加载项目,不想每次都要修改tmp0\conf 原因: 如果在eclipse中配置了tomcat后,其实,tomcat所用的所有tomcat配置文件,都不是catalina_home/config下面的xml文件,而是在eclipse所创建的Serve

hdu2072(string的应用)

单词数 Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 25447    Accepted Submission(s): 5957 Problem Description lily的好朋友xiaoou333最近很空,他

在Unity环境中使用UTF-8编码

为什么要讨论这个问题         为了避免乱码和更好的跨平台         我刚开始开发时是使用VS开发,Unity自身默认使用UTF-8 without BOM格式,但是在Unity中创建一个脚本,使用VS打开,VS自身默认使用GB2312(它应该是对应了你电脑的window版本默认选取了国标编码,或者是因为一些其他的原因)读取脚本,默认是看不到在VS中的编码格式,下面我介绍一种简单快