前言:前段时间,公司要开发一个移动开发平台,有一个功能是在网页端创建Android项目,填入项目名和包名,要能够在后台生成一个Android Studio目录的工程,然后提供给用户。接到这个需求的时候我是一脸懵逼的,开发者要这玩意有啥用啊,我用Android Studio创建工程不是更方便?然而领导说了要做,那就做呗。于是有了以下思路:

在后台放一个标准的Android Studio工程,通过前台传递过来的项目名和包名去修改项目中用到包名的文件,替换成用户输入的包名就ok了。哇,多么简单,想好方案后汇报给领导。

领导说:“你这个确实可以这个功能,但是有没有逼格更高一点的方法?
我:



android studio 编译命令 android studio 命令行 编译_java


好吧,领导发话了还能咋办?原来的方法被搁下了,额,猜想出以下两种高逼格的方式:

  1. 通过控制Android Studio开发工具去生成项目,当然不是手动操作,是通过某种方式,比如命令的方式去操作开发工具,进而生成项目。
  2. 通过一行命令的方式,命令中传入包名、项目名,动态创建工程中所需要的各种文件,进而生成完整的工程目录。

好吧,最后的结果是:第一种pass,Android Studio好像没有给我们提供这样一个操作它的工具;至于第二种:好像可行,但是想想要自己创建那么多文件和文件夹就头皮发麻,一度想要放弃-。-于是绝望的去百度搜索:如何用命令行生成Android项目,一看,哇,有好多博客呀,都是讲如何用命令去生成Android工程:



android studio 编译命令 android studio 命令行 编译_java_02


然后点进去一看,运行了命令:

android create project -n HelloWorld -t android-25 -p HelloWorld -k top.overcode.helloworld -a HelloWorld

其中,-n指定要创建的项目的名称,-t指定项目针对的Android的平台,-p指定该项目的保存路径,-k指定该项目的包名,-a选项指定Activity的名称。 然后生成了工程,目录结构如下:



android studio 编译命令 android studio 命令行 编译_android studio 编译命令_03


这尼玛,是Eclipse结构的工程啊。。。。。。。。



android studio 编译命令 android studio 命令行 编译_java_04


好吧,搜索良久无果,决定看看Android Studio到底如何生成工程的。于是在As安装目录下闲逛了良久,无意中发现以下目录:

D:\Android\as\plugins\android\lib\templates

templates,模板,会不会有模板工程之类的东西呢?点进去的目录如下:



android studio 编译命令 android studio 命令行 编译_Android_05


gradle-projects?Android Studio不就是gradle项目么?继续深入

android studio 编译命令 android studio 命令行 编译_Android_06


NewAndroidProject,新的Android项目,似乎有点像了,继续

D:\Android\as\plugins\android\lib\templates\gradle-projects\NewAndroidProject\root

发现许多个.ftl结尾的文件,

android studio 编译命令 android studio 命令行 编译_java_07

点开build.gradle.ftl文件,发现长这样



android studio 编译命令 android studio 命令行 编译_java_08


咦,这不就是我们的根目录的build.gradle文件么?和项目中的一对比:



buildscript {
    
    repositories {
        google()
        jcenter()

    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

发现classpath 'com.android.tools.build:gradle:3.2.1’中的版本号似乎是从外界传递进来的。于是去搜索了.ftl文件的用途,说是模板文件,于是猜测这些文件是项目文件的模板文件,Android Studio在创建项目的时候,把我们输入的如包名、项目名等内容通过某种方式传递到模板文件中来。于是在搜索ftl的同时,有提到了FreeMarker,这又是啥呢?



android studio 编译命令 android studio 命令行 编译_android studio 编译命令_09


嗯,大体意思就是用来改变模板中的数据的,是一个java类库,可以操作.ftl文件,向其中传入参数,.ftl文件可以获取到传递过来的参数,这样只需要模板文件,然后通过FreeMarker操作模板文件就可以生成Android项目中所需要的文件了,到此研究似乎有了一些进展。那Android Studio是不是也是通过这种方式呢?于是在Android Studio的安装目录下疯狂搜索:



android studio 编译命令 android studio 命令行 编译_android studio 编译命令_10


至此可以确定,Android Studio是通过这种方式来生成工程中的项目文件的。即模板文件+FreeMarker模板引擎。

于是看下标准的Android项目的目录:



android studio 编译命令 android studio 命令行 编译_包名_11


.gradle目录和gradle目录和目录结构层次比较深,好像不好去创建这么多的目录和文件,于是想到了gradle似乎有一个命令:

gradle init

前提是要配置gradle的环境变量哦,这里就不说怎么配置了,自行百度~执行完发现:



android studio 编译命令 android studio 命令行 编译_java_12


哇咔咔,根目录下的文件夹几乎都生成完了,只剩一个app目录了,这个没法通过命令去生成了。app目录下:



android studio 编译命令 android studio 命令行 编译_android studio 编译命令_13


这么一看,我们只需要:

  1. 创建一个libs目录和src目录。
  2. 用模板生成文本文件。

然后src中也一样,有文件夹就创建文件夹,需要创建文件就用模板文件生成。依次递进文件夹,直到该目录下只剩文件为止。看似很难,其实很简单啦。举个例子,比如src目录下有如下路径:

androidTest/java/com/suning/demo

为啥是这个路径,因为我们的包名是com.suning.demo。前面androidTest/java是固定的。两者拼接就是最终的文件目录,直到ExampleInstrumentedTest.java类



android studio 编译命令 android studio 命令行 编译_Android_14


这个文件需要替换的就这么两处。所以对于androidTest/java这个目录下,我们只需要:

  1. 根据包名递进创建目录com/suning/demo。
  2. 模板文件替换ExampleInstrumentedTest.java类中的包名。

然后AndroidManifest.xml、gradle文件中中需要替换的包名同理。文件和目录的创建都搞定了,还有资源文件,怎么办呢?这个有图片啥的,不可能手动创建,去一个标准的工程下复制整个res目录进来就行。好了,所需要的东西我们都有了,那么创建文件夹、生成文件、移动资源目录到特定的文件夹,我们要如何去弄呢?当然是脚本啦,在此之前我们要看下如何通过Freemarker去动态替换.ftl中的内容,需要Freemarker.jar,然后看如下代码:

/*
	 * 生成AndroidManifest文件
	 * */
	
	public static void createAndroidManifest() throws Exception {
		Template template = getTemplate("AndroidManifest.xml.ftl");
		FileWriter fileWriter = new FileWriter(new File(PROJECT_ROOT+"/app/src/main/AndroidManifest.xml"));
		Map<Object, Object> map = new HashMap<>();
		map.put("packageName", PACKGE_NAME);
		template.process(map, fileWriter);
		fileWriter.close();
	}

上面代码是用来生成AndroidManifest.xml文件的:

  1. 首先获取模板文件构造成Template对象。
  2. 一个输入流指向要生成的AndroidManifest.xml文件。
  3. 把需要传递的参数通过map集合存放。
  4. 以map和输入流为参数,输出最终生成的文件。
  5. 关闭流。

再看下AndroidManifest.xml.ftl是如何获取传递进来的参数的:



android studio 编译命令 android studio 命令行 编译_包名_15


packageName就是我们map集合中的一个键,${packageName}即可取到这个键所对应的值。模板文件的创建都同理。至于在命令行中如何获取我们输入的项目名和包名,并传递给java类去操作模板文件,这又涉及到shell 命令的语法了。懂得忽略,不懂的私下百度,这里因为篇幅的原因不去详细介绍了。然后来总结下生成一个工程我们需要怎么做:

  1. 根据命令行传递的项目名新建工程的根目录。通过脚本执行gradle init命令生成根目录下gradle相关的东西。
  2. 编写好操作各个模板文件的java类(负责去操作所有需要通过模板生成的文件,需要Freemarker.jar的支持)。
  3. 一份标准的资源文件目录备用。
  4. 各个模板文件。

看下我的目录:



android studio 编译命令 android studio 命令行 编译_android studio 编译命令_16


其中marker目录有以下模板文件:


android studio 编译命令 android studio 命令行 编译_包名_17


这是一个工程中需要生成的所有文件,都需要通过模板去生成对应的文件。

build脚本的所有代码如下:



# @author Huxin 2019/2/2

echo "create project start..."

#check project name 
#if empty
if [ -z "$1" ];then
echo "please input the project name,such as './bulid Test com.example '"
echo "create project fail !"
exit 0
fi

#if exist
if [ -d "$1" ]; then
echo "The project \"$1\" had exists, you must use another name"
echo "create project fail !"
exit 0
fi

#check project package name
if [ -z "$2" ];then
package="com.example.apps";
else
#package=$(echo $2 | tr '[A-Z]' '[a-z]' )
package=$2
fi


#get lowercase project name
var=$(echo $1 | tr '[A-Z]' '[a-z]' )

package_dir=$(echo $package | sed -e 's/\./\//g' )

#make project dir
mkdir $1

#into project root dir
cd $1

#init gradle
gradle init

if [ $? -eq 0 ]; then
#back tools root dir
    cd ..
	fi


#change code path
rm -rf $1/app/src/main/java/
mkdir -p $1/app/src/main/java/$package_dir

mkdir -p $1/app/src/test/java/$package_dir

mkdir -p $1/app/src/androidTest/java/$package_dir

cp -r res/  $1/app/src/main/res

sed -i "s/appName/$1/g" $1/app/src/main/res/values/strings.xml

#into project root dir 
cd $1
echo "" >settings.gradle 
 echo "include ':app'" >settings.gradle 
cd  app

mkdir libs

#back project root dir
cd ..
cd ..

file=$4
 
# generate class
class=$(echo $file | awk -F '.' '{print $1}')

 
echo "start compile......."
 
# compile java class
javac -encoding UTF-8 -cp freemarker.jar Generate.java
 
if [ $? -eq 0 ]; then
    echo "compile success,ready run..."
 
    # run
	java -cp ".;freemarker.jar" Generate "$1" "$2"
    if [ $? -eq 0 ]; then
        echo "run complete!"
    else
        echo "run error!"
    fi
else
    echo "compile error!"
fi

 rm -f Generate.class


echo "All has been done !"

大体步骤如下:

  1. 通过传递的项目名建立项目根目录。
  2. 执行gradle init命令,生成根目录下gradle相关的目录和文件夹。
  3. 创建对应的文件夹,并拷贝资源文件。
  4. 通过java类去输出所有需要生成的工程文件,如build.gradle、AndroidManifest.xml、MainActivity.java等等。。。。
  5. 删除编译后的操作类的字节码文件。

好吧,还有个Generate.java文件,用来生成所有需要生成的文件,代码如下:

import java.io.File;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import freemarker.template.Configuration;
import freemarker.template.Template;

public class Generate {
	//当前java文件所在的路径,即这个工具所在的根目录。
	public final static String ROOT_PATH = Generate.class.getResource("").getPath();
	//模板文件所在的路径。
	public final static String FTL_PATH = ROOT_PATH+"/marker";
	
	//项目根目录。
	public static String PROJECT_ROOT = "";
	
	//包名
	public static String PACKGE_NAME = "";
	
	
	public static void main(String[] args) throws Exception {
		//传递进来三个参数,第一个是项目名,第二个是包名。
		PROJECT_ROOT = args[0];
		PACKGE_NAME = args[1];
		
		createBuildGradle();

		createAppBuildGradle(PROJECT_ROOT+"/app");
		createAndroidManifest();
		createIgnoreAndProguardFiles();
		createTestFiles();
		createMainActivity();
	}
	
	//生成module的build.gradle文件
	private static void createAppBuildGradle(String appPath) throws Exception {
		Template template = getTemplate("build.gradle1.ftl");
		File file = new File(appPath+"/build.gradle");
		if(!file.exists()) {
			file.createNewFile();
		}
		FileWriter fileWriter = new FileWriter(file);
		Map<Object, Object> map = new HashMap<>();

		map.put("packageName", PACKGE_NAME);
		template.process(map, fileWriter);
		fileWriter.close();
	}

	/*
	 * 生成项目的build.gradle
	 * */
	public static void createBuildGradle() throws Exception{
		// 在模板文件目录中找到名称为name的文件
		Template template = getTemplate("build.gradle.ftl");
		FileWriter fileWriter = new FileWriter(new File(PROJECT_ROOT+"/build.gradle"));
		
		Map<Object, Object> map = new HashMap<>();
		map.put("gradlePluginVersion", "3.2.1");
		template.process(map, fileWriter);
		fileWriter.close();
	}
	
	/*
	 * 生成AndroidManifest文件
	 * */
	
	public static void createAndroidManifest() throws Exception {
		Template template = getTemplate("AndroidManifest.xml.ftl");
		FileWriter fileWriter = new FileWriter(new File(PROJECT_ROOT+"/app/src/main/AndroidManifest.xml"));
		Map<Object, Object> map = new HashMap<>();
		map.put("packageName", PACKGE_NAME);
		template.process(map, fileWriter);
		fileWriter.close();
	}
	
	
	/*创建混淆文件和忽略文件*/
	
	public static void createIgnoreAndProguardFiles() throws Exception{
		//project下的忽略文件
		Template template = getTemplate(".gitignore.ftl");
		File projectIgnoreFile = new File(PROJECT_ROOT+"/.gitignore");
		if(!projectIgnoreFile.exists()) {
			projectIgnoreFile.createNewFile();
		}
		FileWriter fileWriter = new FileWriter(projectIgnoreFile);
		Map<Object, Object> map = new HashMap<>();
		template.process(map, fileWriter);
		
		
		//module下的忽略文件
		Template template1 = getTemplate(".gitignore1.ftl");
		File moduleIgnoreFile = new File(PROJECT_ROOT+"/app/.gitignore");
		if(!moduleIgnoreFile.exists()) {
			moduleIgnoreFile.createNewFile();
		}
		FileWriter fileWriter1 = new FileWriter(moduleIgnoreFile);
		Map<Object, Object> map1 = new HashMap<>();
		template1.process(map1, fileWriter1);
		fileWriter1.close();
		
		//创建混淆文件
		Template template2 = getTemplate("proguard-rules.pro.ftl");
		File proguardFile = new File(PROJECT_ROOT+"/app/proguard-rules.pro");
		if(!proguardFile.exists()) {
			proguardFile.createNewFile();
		}
		FileWriter fileWriter2 = new FileWriter(proguardFile);
		Map<Object, Object> map2 = new HashMap<>();
		template2.process(map2, fileWriter2);
		
		//关流。
		fileWriter.close();
		fileWriter1.close();
		fileWriter2.close();
		
	}
	
	/*
	 * 创建Test文件
	 * */
	public static void createTestFiles() throws Exception{
		
		//创建ExampleUnitTest类
		Template template = getTemplate("ExampleUnitTest.java.ftl");
		String [] packages = PACKGE_NAME.split("\\.");
		String testFilePath = PROJECT_ROOT+"/app/src/test/java";
		for(int i=0;i<packages.length;i++) {
			testFilePath = testFilePath+"/"+packages[i];
		}
		File testFile = new File(testFilePath+"/ExampleUnitTest.java");
		if(!testFile.exists()) {
			testFile.createNewFile();
		}
		FileWriter fileWriter = new FileWriter(testFile);
		Map<Object, Object> map = new HashMap<>();
		map.put("packageName", PACKGE_NAME);
		template.process(map, fileWriter);
		
		
		//创建ExampleInstrumentedTest类
		Template template1 = getTemplate("ExampleInstrumentedTest.java.ftl");
		String androidTestFilePath = PROJECT_ROOT+"/app/src/androidTest/java";
		for(int i=0;i<packages.length;i++) {
			androidTestFilePath = androidTestFilePath+"/"+packages[i];
		}
		File androidTestFile = new File(androidTestFilePath+"/ExampleInstrumentedTest.java");
		if(!androidTestFile.exists()) {
			androidTestFile.createNewFile();
		}
		FileWriter fileWriter1 = new FileWriter(androidTestFile);
		Map<Object, Object> map1 = new HashMap<>();
		map1.put("packageName", PACKGE_NAME);
		template1.process(map1, fileWriter1);
		fileWriter.close();
		fileWriter1.close();
	}
	
    //创建MainActivity类。
	public static void createMainActivity() throws Exception{
		Template template = getTemplate("MainActivity.java.ftl");
		String [] packages = PACKGE_NAME.split("\\.");
		String mainActivityPath = PROJECT_ROOT+"/app/src/main/java";
		for(int i=0;i<packages.length;i++) {
			mainActivityPath = mainActivityPath+"/"+packages[i];
		}
		File mainActivityFile = new File(mainActivityPath+"/MainActivity.java");
		if(!mainActivityFile.exists()) {
			mainActivityFile.createNewFile();
		}
		FileWriter fileWriter = new FileWriter(mainActivityFile);
		Map<Object, Object> map = new HashMap<>();
		map.put("packageName", PACKGE_NAME);
		template.process(map, fileWriter);
		fileWriter.close();
	}
	
	
	public static Template getTemplate(String templateName) throws Exception{
		Configuration configuration = new Configuration();
		configuration.setDirectoryForTemplateLoading(new File(FTL_PATH));
		Template template = configuration.getTemplate(templateName);
		return template;
	}

}

这个类是用来通过模板文件来生成工程中所有的文件的,是生成文件的核心类,当然需要Freemarker jar包的支持。到此我们通过命令生成Android Studio目录结构的Android工程就已经完成了。最终整个工具的目录如下:



android studio 编译命令 android studio 命令行 编译_包名_18


说到底就是一个脚本,一个java类就可以了,主要是构建这样一个工具的思路。这个脚本可以在windows下执行,也可以在linux下执行。先看windows下,首先你需要一个shell 环境,但是windows下默认是没有shell环境的,你可以下载Git for Windows,下载地址:Git for Windows,下载完成,一路默认安装,安装完成就可以使用了,在桌面或任何文件目录中,点击右键菜单中会有Git Bash Here选项:



android studio 编译命令 android studio 命令行 编译_android studio 编译命令_19


但是要使用这个工具,我们需要在tools文件夹下右击,选择Git Bash Here,如下:



android studio 编译命令 android studio 命令行 编译_Android_20


然后执行如下命令:

./build Demo com.suning.demo

可以看到如下效果:



android studio 编译命令 android studio 命令行 编译_java_21


表示工程生成成功了。其中Demo是项目名,com.suning.demo是项目的包名。有两点需要注意:

1. tools文件夹所在的目录不要包含中文,可能会出现乱码。
2. 执行命令的时候一定要切换到tools目录下,无论是windows还是linux。

看下生成的工程:



android studio 编译命令 android studio 命令行 编译_包名_22


这个项目是可以直接用Android Studio来运行的。当然你可以修改这个工具,改变某个文件想要改变的地方都可以,感兴趣的自己可以改一改,没啥难度。哦,对了,要在linux下运行,build文件要修改一处地方:



android studio 编译命令 android studio 命令行 编译_java_23


把其中的分号改成冒号就行:

java -cp ".:freemarker.jar" Generate "$1" "$2"

不然在linux下运行会报错。所有的代码都上传到Gihub上了,代码传送门

总结:其实这个工具也挺鸡肋的,创建工程还是Android Studio来的方便,正常都不会用到,公司所开发的移动开发平台,要求网页端可以创建Android工程,并可以动态选择项目的依赖库(如Okhtp、Glide、Retrofit、RxJava等),然后生成工程的时候,远程调用命令,把所选依赖库的配置代码写入到gradle文件中,省去用户自行配置的时间,要说倒也算是优点功能,不过有没有人用还是个未知数,毕竟作为开发者,我还是更相信开发工具生成的项目,完-。-