前言

从毕业到现在,做了整整3年Android开发,兜兜转转又回到了南京.哎…
今天主要记录下,来到新公司实现一个打包工具的过程,目前我们Android端的任务除了修改已经存在的bug和开发新的功能外,就是对于不同的买家输出对应的系统,说白了就是基于一套模板代码,要打成不同的APK,然后通过替换一些图片来达到样子长得不一样的目的,就这么简单,目前呢没有实现通过网络下载图片来实现,还是通过替换本地文件来进行输出的.所以一个打包工具就是必须的了.

正题

那么问题来了这个打包工具应该要具备什么功能呢?

  1. 要会查找UI文件,包括从本地查找和从SVN查找
  2. 从模板代码自动创建一个对应的Android工程
  3. 自动替换工程里面的图片文件
  4. 自动修改app.gradle文件(修改appid,baseurl等等)
  5. 自动打包成APK文件
  6. 自动加固应用
  7. 上传打包记录到数据库(包括应用名称,id等等)
  8. 自动安装到手机并且打开应用

OK!大体上的功能就是这些了,接下来具体说下利用C#语言怎么实现这些功能,首先上几个最终的效果图

上图是每次打包都需要修改的配置

上图是配置一些路径信息

上图是配置SVN账号信息

上图是配置下加固的信息,当前使用的加固是360加固

上图是配置数据库信息,用来保存打包记录

上图就是打包的过程截图

首先我们需要一个配置文件来保存,我们一些不怎么变动的打包信息,就可以保存在里面,这里我建了一个config.ini文件用来保存配置信息.

[app]project_directory_name =app_name =base_url =app_id =version_code =version_name =[check]is_install_jiagu_apk = false[path]ui_root_path =D:\UIout_root_path =D:\OutServicemodel_path =D:\BtyProjects\MuYeHua[svn]svn_username =xxxxsvn_password =xxxxsvn_path =svn://xx.xx.xx.xx/[jiagu]jiagu_username =xxxxxjiagu_password =xxxxxxjiagu_path =D:\xxx\jiagujiagu_out_path =D:\OutApks[db]db_host =xx.xx.xx.xxdb_port =xxdb_user =xxxdb_pwd =xxxxxdb_name =xxxdb_table =xxxx

接下来我们针对上面提出的功能点一个个分析实现的过程:
1.查找本地和SVN上的UI文件
对于查找本地文件很简单UI的文件夹都是用项目名称来命名的只需要利用文件操作检查本地UI目录下是不是有指定的文件夹就行,

DirectoryInfo directoryInfo = new DirectoryInfo(config.ui_root_path);                DirectoryInfo[] uis = directoryInfo.GetDirectories();                foreach (DirectoryInfo info in uis)                {                    if (info.Name.Contains(config.app_name))                    {                        CheckAppColor(info.Name);                        apendResultString("本地存在UI文件:" + info.Name + "    颜色值:" + app_color);                        changeLogo();                        return;                    }                }

那么对于SVN上的目录遍历我们就需要利用SVN工具来进行了,这里我们需要知道一点的是C#怎么调用其他应用程序并且截取标准输出

//同步模式调用其他程序,截取输出        public List exec(string exePath, string parameters)        {            List list = new List();            ProcessStartInfo psi = new ProcessStartInfo();            psi.RedirectStandardOutput = true;            psi.CreateNoWindow = true;            psi.UseShellExecute = false;            psi.FileName = exePath;            psi.Arguments = parameters;            Process process = Process.Start(psi);            StreamReader outputStreamReader = process.StandardOutput;            string line = outputStreamReader.ReadLine();//每次读取一行            while (!outputStreamReader.EndOfStream)            {                apendResultString(line);                list.Add(line);                line = outputStreamReader.ReadLine();            }            process.WaitForExit();//等待程序执行完退出进程            process.Close();//关闭进程            outputStreamReader.Close();//关闭流            return list;        }

有了这个基础我们就好办了,先遍历SVN指定目录下的文件夹,判断是否有我们项目的UI文件

 List list = exec("svn", ls_svn_dir);                    if (list.Count() > 0)                    {                        foreach (string path in list)                        {                            if (path.Contains(config.app_name))                            {                                ui_addr_on_svn = config.svn_path + svn_ui_dir + "/" + path;                                CheckAppColor(ui_addr_on_svn);                                apendResultString("SVN存在UI文件:" + ui_addr_on_svn + "    颜色值:" + app_color);                                break;                            }                        }                    }

如果SVN存在UI文件的情况下我们利用svn 的export命令进行下载(export和checkout的区别?)

//如果SVN存在UI就要开始下载了                    apendResultString("######开始从SVN下载UI文件######");                    string local_ui_path = config.ui_root_path + "\\" + config.app_name + app_color;                    string projectPath = "export " + ui_addr_on_svn + " " + local_ui_path + " --username " + config.svn_username + " --password " + config.svn_password;                    apendResultString(local_ui_path);                    apendResultString(projectPath);                    exec("svn", projectPath);                    apendResultString("######UI文件下载完成!!!######");

UI文件的处理基本就是这样,然后接下来就是工程的创建,我们只需要从模板目录copy一份然后重新命名(命名规则工程名称+日期),这其中过滤掉.svn目和build目录就行了

final_project_name = config.project_directory_name + "_" + config.version_code + "_" + DateTime.Now.ToString("yyyy-MM-dd-hh-mm-ss");            apendResultString("######开始创建Android工程" + final_project_name + "######");            string desPath = config.out_root_path + "\\" + final_project_name;            CopyDirectory(config.model_path, desPath);

工程创建成功之后我们就可以进行图片的替换了,

 foreach (string drawable in drawables_list)                {                    DirectoryInfo directory = new DirectoryInfo(config.ui_root_path + "\\" + config.app_name + app_color + "\\android\\" + drawable);                    foreach (FileInfo f in directory.GetFiles())                    {                        if (f.Name.EndsWith(".png") || f.Name.EndsWith(".jpg") || f.Name.EndsWith(".jpeg"))                        {                            apendResultString("覆盖:" + f.Name);                            f.CopyTo(config.out_root_path + "\\" + final_project_name + "\\" + res + "\\" + drawable + "\\" + f.Name, true);                        }                    }                }

图片替换的过程中我们注意只替换图片文件就行了,接下来修改一下gradle文件一个新的Android项目就算完成了

string config_path = config.out_root_path + "\\" + final_project_name + "\\config\\config.gradle";                string colors_path = config.out_root_path + "\\" + final_project_name + "\\" + res + "\\values\\colors.xml";                string vcs_path = config.out_root_path + "\\" + final_project_name + "\\.idea\\vcs.xml";                string[] congig_lines = File.ReadAllLines(config_path);                for (int i = 0; i < congig_lines.Length; i++)                {                    if (congig_lines[i].Contains("application_id"))                    {                        congig_lines[i] = "     application_id  : \"" + config.app_id + "\",";                        continue;                    }                    if (congig_lines[i].Contains("key_storefile"))                    {                        congig_lines[i] = "     key_storefile  : \"" + config.model_path + "/myh.jks\",";                        congig_lines[i] = congig_lines[i].Replace('\\', '/');                        continue;                    }                    if (congig_lines[i].Contains("outFile_name"))                    {                        congig_lines[i] = "     outFile_name  : \"" + config.project_directory_name + "\",";                        continue;                    }                    if (congig_lines[i].Contains("url"))                    {                        congig_lines[i] = "     url  : \"" + config.app_url + "\",";                        continue;                    }                    if (congig_lines[i].Contains("appname"))                    {                        congig_lines[i] = "     appname  : \"" + config.app_name + "\"";                        continue;                    }                    if (congig_lines[i].Contains("version_code"))                    {                        congig_lines[i] = "     version_code  : " + config.version_code + ",";                        continue;                    }                    if (congig_lines[i].Contains("version_name"))                    {                        congig_lines[i] = "     version_name  : \"" + config.version_name + "\",";                        continue;                    }                }                File.WriteAllLines(config_path, congig_lines);                string[] colors_lines = File.ReadAllLines(colors_path);                for (int i = 0; i < colors_lines.Length; i++)                {                    if (colors_lines[i].Contains("colorPrimary") && !colors_lines[i].Contains("Dark"))                    {                        colors_lines[i] = "    #" + app_color + "";                        continue;                    }                    if (colors_lines[i].Contains("colorPrimaryDark"))                    {                        colors_lines[i] = "    #" + app_color + "";                        continue;                    }                    if (colors_lines[i].Contains("colorAccent"))                    {                        colors_lines[i] = "    #" + app_color + "";                        continue;                    }                    if (colors_lines[i].Contains("color_theme"))                    {                        colors_lines[i] = "    #" + app_color + "";                        continue;                    }                }                File.WriteAllLines(colors_path, colors_lines);

接下来就是最重要的一个步骤了,编译我们的工程生成APK文件,这里我是利用了控制台去执行的gradlew clean assembleLocalRelease然后截取的控制台的输出

string gradle_dir = config.out_root_path + "\\" + final_project_name + "\\";            string cd_project_pan = gradle_dir.Substring(0, 2);            string cd_project_directory = "cd " + gradle_dir;            string buidl_project = "gradlew clean assembleLocalRelease";            buidl_project = buidl_project.Trim().TrimEnd('&') + "&exit";//说明:不管命令是否成功均执行exit命令,否则当调用ReadToEnd()方法时,会处于假死状态            using (Process p = new Process())            {                p.StartInfo.FileName = "cmd.exe";                p.StartInfo.UseShellExecute = false;        //是否使用操作系统shell启动                p.StartInfo.RedirectStandardInput = true;   //接受来自调用程序的输入信息                p.StartInfo.RedirectStandardOutput = true;  //由调用程序获取输出信息                p.StartInfo.CreateNoWindow = true;          //不显示程序窗口                p.Start();//启动程序                          //编译命令                p.StandardInput.WriteLine(cd_project_pan);                p.StandardInput.AutoFlush = true;                p.StandardInput.WriteLine(cd_project_directory);                p.StandardInput.AutoFlush = true;                p.StandardInput.WriteLine(buidl_project);                p.StandardInput.AutoFlush = true;                StreamReader reader = p.StandardOutput;//截取输出流                string line = reader.ReadLine();//每次读取一行                apendResultString(line);                while (!reader.EndOfStream)                {                    line = reader.ReadLine();                    apendResultString(line);                }                p.WaitForExit();//等待程序执行完退出进程                p.Close();            }

编译成APK后我们记录下apk的路径然后同样的方式我们调用jiagu命令,这里需要注意的是360加固一般我们用的都是可视化的窗口工具,但是人性化的是他还提供了jar 的调用方式,这里我们依然使用控制台进行操作,注意导入签名信息就行了

//加固命令            string cd_jiagu_pan = config.jiagu_path.Substring(0, 2);            string cd_jiagu_directory = "cd " + config.jiagu_path;            string login_jiagu = "java -jar jiagu.jar -login " + config.jiagu_username + " " + config.jiagu_password;            string apk_release_directory = config.out_root_path + "\\" + final_project_name + "\\app\\build\\outputs\\apk\\local\\release";            FileInfo[] apks = new DirectoryInfo(apk_release_directory).GetFiles();            foreach (FileInfo apkInfo in apks)            {                if (apkInfo.Name.Contains(config.project_directory_name))                {                    final_apk_path = apkInfo.FullName;                    break;                }            }            string jiagu_project = "java -jar jiagu.jar -jiagu " + final_apk_path + " " + config.jiagu_out_path + "  -autosign";            jiagu_project = jiagu_project.Trim().TrimEnd('&') + "&exit";//说明:不管命令是否成功均执行exit命令,否则当调用ReadToEnd()方法时,会处于假死状态            using (Process p = new Process())            {                p.StartInfo.FileName = "cmd.exe";                p.StartInfo.UseShellExecute = false;        //是否使用操作系统shell启动                p.StartInfo.RedirectStandardInput = true;   //接受来自调用程序的输入信息                p.StartInfo.RedirectStandardOutput = true;  //由调用程序获取输出信息                p.StartInfo.CreateNoWindow = true;          //不显示程序窗口                p.Start();                //启动程序                //编译命令                p.StandardInput.WriteLine(cd_jiagu_pan);                p.StandardInput.AutoFlush = true;                p.StandardInput.WriteLine(cd_jiagu_directory);                p.StandardInput.AutoFlush = true;                p.StandardInput.WriteLine(jiagu_project);                p.StandardInput.AutoFlush = true;                //获取cmd窗口的输出信息                StreamReader reader = p.StandardOutput;//截取输出流                string line = reader.ReadLine();//每次读取一行                apendResultString(line);                while (!reader.EndOfStream)                {                    line = reader.ReadLine();                    apendResultString(line);                }                p.WaitForExit();//等待程序执行完退出进程                reader.Close();                p.Close();            }

等待加固完成了,我这里会吧打包的信息上传到数据库进行保存

// 数据库配置            string connStr = "server=" + config.db_host + ";port=" + config.db_port + ";database=" + config.db_name + ";user=" + config.db_user + ";password=" + config.db_pwd + ";";            MySqlConnection connection = null;            bool exitsInDb = false;            try            {                connection = new MySqlConnection(connStr);                connection.Open();                string selectByAppNameStr = "select * from " + config.db_table + " where app_name = '" + config.app_name + "'";                MySqlCommand com = new MySqlCommand(selectByAppNameStr, connection);                MySqlDataReader reader = com.ExecuteReader();                while (reader.Read())                {                    string app_name_ = reader.GetString("app_name");                    if (app_name_.Equals(config.app_name))                    {                        exitsInDb = true;                    }                }                reader.Close();                string updateStr;                string date_string = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");                if (exitsInDb)                {                    updateStr = "update " + config.db_table + " set " +                     "app_name = '" + config.app_name + "'," +                     "app_file_name = '" + config.project_directory_name + "'," +                     "app_url='" + config.app_url + "'," +                     "app_version_code='" + config.version_code + "'," +                     "app_version_name='" + config.version_name + "'," +                     "update_date = '" + date_string + "'," +                     "app_color = '" + app_color + "'," +                     "user_name = '" + config.svn_username + "'" +                     " where " +                     "app_name = '" + config.app_name + "' " +                     "or " +                     "app_id = '" + config.app_id + "'";                }                else                {                    updateStr = "insert into " + config.db_table + " (app_name,app_file_name,app_url,app_version_code ,app_version_name ,app_id,create_date,update_date,app_color,user_name)" +                              " values ('" + config.app_name + "','" + config.project_directory_name                              + "','" + config.app_url + "','" + config.version_code + "','" + config.version_name                              + "','" + config.app_id + "','" + date_string + "','" + date_string + "','" + app_color + "','" + config.svn_username + "')";                }                MySqlCommand updateCom = new MySqlCommand(updateStr, connection);                updateCom.ExecuteNonQuery();            }

OK,到这里基本上是所有的事情都做完了,但是这里为了方便测试,我会连接上手机进行安装和启动应用,这所有的操作都是依靠adb命令来完成的

 string install_com = "install -r " + final_apk_path;            apendResultString("######开始安装应用,请确保USB连接######");            exec("adb", install_com);            apendResultString("######开始启动应用######");            string start_com = "shell am start -n \"" + config.app_id + "/com.qfwl.lelexin.modules.other.view.activity.SplashActivity\" -a android.intent.action.MAIN -c android.intent.category.LAUNCHER";            exec("adb", start_com);            apendResultString("######应用已经启动######");

一个像模像样的打包工具就这么完成了!当然有人说了,如果是mac系统呢?这里为了mac也能顺利的打包成功,我们也同样谢了python的打包脚本.

更多相关文章

  1. Android(安卓)关于引用jackson的jar包混淆报错或打包后运行报错
  2. 大厂面试,居然还问这些问题!
  3. Android(安卓)sdcard媒体文件更新(程序控制刷新MediaStore数据库)
  4. Android(安卓)Odex
  5. App实战:移动端Mock Api的几种方式
  6. 慢学Android(安卓)Jetpack
  7. Android(安卓)Binder 应用层调用过程分析
  8. Android(安卓)studio 引入简单的高德地图(一)
  9. Apk脱壳圣战之---脱掉“爱加密”的壳

随机推荐

  1. 控制流程系列教材 (一)- Java的If 条件语句
  2. 控制流程系列教材 (二)- java的switch语句
  3. 控制流程系列教材 (三)- java的while语句
  4. 如何设置 Centos7 为固定ip地址(详细教程
  5. 史上最详细的Intellij IDEA开发教程
  6. Springboot 如何做前后端分离?
  7. 如何在 阿里云 申请 ssl 免费证书
  8. 史上最全maven教程
  9. springboot整合 elasticsearch 做 增删改
  10. J2EE Listener 监听器教程