Jenkins的Pipeline脚本在美团餐饮SaaS中的实践

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

一、背景

在日常开发中,我们经常会有发布需求,而且还会遇到各种环境,比如:线上环境(Online),模拟环境(Staging),开发环境(Dev)等。最简单的就是手动构建、上传服务器,但这种方式太过于繁琐,使用持续集成可以完美地解决这个问题,推荐了解一下Jenkins。 Jenkins构建也有很多种方式,现在使用比较多的是自由风格的软件项目(Jenkins构建的一种方式,会结合SCM和构建系统来构建你的项目,甚至可以构建软件以外的系统)的方式。针对单个项目的简单构建,这种方式已经足够了,但是针对多个类似且又存在差异的项目,就难以满足要求,否则就需要大量的job来支持,这就存在,一个小的变动,就需要修改很多个job的情况,难以维护。我们团队之前就存在这样的问题。

目前,我们团队主要负责开发和维护多个Android项目,而且每个项目都需要构建,每个构建流程非常类似但又存在一定的差异。比如构建的流程大概如下:

  • 克隆代码;
  • 静态代码检查(可选);
  • 单元测试(可选);
  • 编译打包APK或者热补丁;
  • APK分析,获取版本号(VersionCode),包的Hash值(apkhash)等;
  • 加固;
  • 上传测试分发平台;
  • 存档(可选);
  • 触发自动化测试(可选);
  • 通知负责人构建结果等。

整个流程大体上是相同的,但是又存在一些差异。比如有的构建可以没有单元测试,有的构建不用触发自动化测试,而且构建结果通知的负责人也不同。如果使用自由风格软件项目的普通构建,每个项目都要建立一个job来处理流程(可能会调用其他job)。

这种处理方式原本也是可以的,但是必须考虑到,可能会有新的流程接入(比如二次签名),构建流程也可能存在Bug等多种问题。无论哪种情况,一旦修改主构建流程,每个项目的job都需要修改和测试,就必然会浪费大量的时间。针对这种情况,我们使用了Pipeline的构建方式来解决。

当然,如果有项目集成了React Native,还需要构建JsBundle。在Native修改以后,JsBundle不一定会有更新,如果是构建Native的时候一起构建JsBundle,就会造成很多资源浪费。并且直接把JsBundle这类大文件放在Native的Git仓库里,也不是特别合适。

本文是分享一种Pipeline的使用经验,来解决这类问题。

二、Pipeline的介绍

Pipeline也就是构建流水线,对于程序员来说,最好的解释是:使用代码来控制项目的构建、测试、部署等。使用它的好处有很多,包括但不限于:

  • 使用Pipeline可以非常灵活的控制整个构建过程;
  • 可以清楚的知道每个构建阶段使用的时间,方便构建的优化;
  • 构建出错,使用stageView可以快速定位出错的阶段;
  • 一个job可以搞定整个构建,方便管理和维护等。

Stage View

三、使用Pipeline构建

新建一个Pipeline项目,写入Pipeline的构建脚本,就像下面这样: 对于单个项目来说,使用这样的Pipeline来构建能够满足绝大部分需求,但是这样做也有很多缺陷,包括:

  • 多个项目的Pipeline打包脚本不能公用,导致一个项目写一份脚本,维护比较麻烦。一个变动,需要修改多个job的脚本;
  • 多个人维护构建job的时候,可能会覆盖彼此的代码;
  • 修改脚本失败以后,无法回滚到上个版本;
  • 无法进行构建脚本的版本管理,老版本发修复版本需要构建,可能和现在用的job版本已经不一样了,等等。

四、把Pipeline当代码写

既然存在缺陷,我们就要找更好的方式,其实Jenkins提供了一个更优雅的管理Pipeline脚本的方式,在配置项目Pipeline的时候,选择Pipeline script from SCM,就像下面这样: 这样,Jenkins在启动job的时候,首先会去仓库里面拉取脚本,然后再运行这个脚本。在脚本里面,我们规定的构建方式和流程,就会按部就班地执行。构建的脚本,可以实现多人维护,还可以Review,避免出错。 以上就算搭建好了一个基础,而针对多个项目时,还有一些事情要做,不可能完全一样,以下是构建的结构图:

如此以来,我们的构建数据来源分为三部分:job UI界面、仓库的通用Pipeline脚本、项目下的特殊配置,我们分别来看一下:

job UI界面(参数化构建)

在配置job的时候,选择参数化构建过程,传入项目仓库地址、分支、构建通知人等等。还可以增加更多的参数 ,这些参数的特点是,可能需要经常修改,比如灵活选择构建的代码分支。

项目配置

在项目工程里面,放入针对这个项目的配置,一般是一个项目固定,不经常修改的参数,比如项目名字,如下图:

注入构建信息

QA提一个Bug,我们需要确定,这是哪次的构建,或者要知道commitId,从而方便进行定位。因此在构建时,可以把构建信息注入到APK之中。

  1. 把属性注入到gradle.properties
# 应用的后端环境
APP_ENV=Beta
# CI 打包的编号,方便确定测试的版本,不通过 CI 打包,默认是 0
CI_BUILD_NUMBER=0
# CI 打包的时间,方便确定测试的版本,不通过 CI 打包,默认是 0
CI_BUILD_TIMESTAMP=0
  1. 在build.gradle里设置buildConfigField
#使用的是gradle.properties里面注入的值
buildConfigField "String", "APP_ENV", "\"${APP_ENV}\""
buildConfigField "String", "CI_BUILD_NUMBER", "\"${CI_BUILD_NUMBER}\""
buildConfigField "String", "CI_BUILD_TIMESTAMP", "\"${CI_BUILD_TIMESTAMP}\""
buildConfigField "String", "GIT_COMMIT_ID", "\"${getCommitId()}\""//获取当前Git commitId
String getCommitId() {try {def commitId = 'git rev-parse HEAD'.execute().text.trim()return commitId;} catch (Exception e) {e.printStackTrace();}
}
  1. 显示构建信息 在App里,找个合适的位置,比如开发者选项里面,把刚才的信息显示出来。QA提Bug时,要求他们把这个信息一起带上
mCIIdtv.setText(String.format("CI 构建号:%s", BuildConfig.CI_BUILD_NUMBER));
mCITimetv.setText(String.format("CI 构建时间:%s", BuildConfig.CI_BUILD_TIMESTAMP));
mCommitIdtv.setText(String.format("Git CommitId:%s", BuildConfig.GIT_COMMIT_ID));

仓库的通用Pipeline脚本

通用脚本是抽象出来的构建过程,遇到和项目有关的都需要定义成变量,再从变量里进行读取,不要在通用脚本里写死。


node {try{stage('检出代码'){//从git仓库中检出代码git branch: "${BRANCH}",credentialsId: 'xxxxx-xxxx-xxxx-xxxx-xxxxxxx', url: "${REPO_URL}"loadProjectConfig();}stage('编译'){//这里是构建,你可以调用job入参或者项目配置的参数,比如:echo "项目名字 ${APP_CHINESE_NAME}"//可以判断if (Boolean.valueOf("${IS_USE_CODE_CHECK}")) {echo "需要静态代码检查"} else {echo "不需要静态代码检查"}}stage('存档'){//这个演示的Android的项目,实际使用中,请根据自己的产物确定def apk = getShEchoResult ("find ./lineup/build/outputs/apk -name '*.apk'")def artifactsDir="artifacts"//存放产物的文件夹sh "mkdir ${artifactsDir}"sh "mv ${apk} ${artifactsDir}"archiveArtifacts "${artifactsDir}/*"}stage('通知负责人'){emailext body: "构建项目:${BUILD_URL}\r\n构建完成", subject: '构建结果通知【成功】', to: "${EMAIL}"}} catch (e) {emailext body: "构建项目:${BUILD_URL}\r\n构建失败,\r\n错误消息:${e.toString()}", subject: '构建结果通知【失败】', to: "${EMAIL}"} finally{// 清空工作空间cleanWs notFailBuild: true}}// 获取 shell 命令输出内容
def getShEchoResult(cmd) {def getShEchoResultCmd = "ECHO_RESULT=`${cmd}`\necho \${ECHO_RESULT}"return sh (script: getShEchoResultCmd,returnStdout: true).trim()
}//加载项目里面的配置文件
def loadProjectConfig(){def jenkinsConfigFile="./jenkins.groovy"if (fileExists("${jenkinsConfigFile}")) {load "${jenkinsConfigFile}"echo "找到打包参数文件${jenkinsConfigFile},加载成功"} else {echo "${jenkinsConfigFile}不存在,请在项目${jenkinsConfigFile}里面配置打包参数"sh "exit 1"}
}

轻轻的点两下Build with Parameters -> 开始构建,然后等几分钟的时间,就能够收到邮件。

五、其他构建结构

以上,仅仅是针对我们当前遇到问题的一种不错的解决方案,可能并不完全适用于所有场景,但是可以根据上面的结构进行调整,比如:

  • 根据stage拆分出不同的Pipeline脚本,这样方便CI的维护,一个或者几个人维护构建中的一个stage;
  • 把构建过程中的stage做成普通的自由风格的软件项目的job,把它们作为基础服务,在Pipeline中调用这些基础服务等。

六、当遇上React Native

当项目引入了React Native以后,因为技术栈的原因,React Native的页面是由前端团队开发,但容器和原生组件是Android团队维护,构建流程也发生了一些变化。

方案对比

方案说明缺点优点
手动拷贝等JsBundle构建好了,再手动把构建完成的产物,拷贝到Native工程里面1. 每次手动操作,比较麻烦,效率低,容易出错<br />2. 涉及到跨端合作,每次要去前端团队主动拿JsBundle<br />3. Git不适合管理大文件和二进制文件简单粗暴
使用submodule保存构建好的JsBundle直接把JsBundle放在Native仓库的一个submodule里面,由前端团队主动更新,每次更新Native的时候,直接就拿到了最新的JsBundle1. 简单无开发成本<br />2. 不方便单独控制JsBundle的版本<br />3. Git不适合管理大文件和二进制文件前端团队可以主动更新JsBundle
使用submodule管理JsBundle的源码直接把JsBundle的源码放在Native仓库的一个submodule里面,由前端团队开发更新,每次构建Native的时候,先构构建JsBundle1. 不方便单独控制JsBundle的版本<br />2. 即使JsBundle无更新,也需要构建,构建速度慢,浪费资源方便灵活
分开构建,产物存档JsBundle和Native分开构建,构建完了的JsBundle分版本存档,Native构建的时候,直接去下载构建好了的JsBundle版本1. 通过配置管理JsBundle,解放Git<br />2. 方便Jenkins构建的时候,动态配置需要的JsBundle版本1. 需要花费时间建立流程<br />2. 需要开发Gradle的JsBundle下载插件

前端团队开发页面,构建后生成JsBundle,Android团队拿到前端构建的JsBundle,一起打包生成最终的产物。 在我们开发过程中,JsBundle修改以后,不一定需要修改Native,Native构建的时候,也不一定每次都需要重新构建JsBundle。并且这两个部分由两个团队负责,各自独立发版,构建的时候也应该独立构建,不应该融合到一起。

综合对比,我们选择了使用分开构建的方式来实现。

分开构建

因为需要分开发布版本,所以JsBundle的构建和Native的构建要分开,使用两个不同的job来完成,这样也方便两个团队自行操作,避免相互影响。 JsBundle的构建,也可以参考上文提到的Pipeline的构建方式来做,这里不再赘述。 在独立构建以后,怎么才能组合到一起呢?我们是这样思考的:JsBundle构建以后,分版本的储存在一个地方,供Native在构建时下载需要版本的JsBundle,大致的流程如下:

这个流程有两个核心,一个是构建的JsBundle归档存储,一个是在Native构建时去下载。

JsBundle归档存储

方案缺点优点
直接存档在Jenkins上面1. JsBundle不能汇总浏览<br>2. Jenkins很多人可能要下载,命名带有版本号,时间,分支等,命名不统一,不方便构建下载地址<br>3. 下载Jenkins上面的产物需要登陆授权,比较麻烦1. 实现简单,一句代码就搞定,成本低
自己构建一个存储服务1. 工程大,开发成本高<br>2. 维护起来麻烦可扩展,灵活性高
MSS<br>(美团存储服务)1. 储存空间大<br>2. 可靠性高,配合CDN下载速度快<br>3. 维护成本低, 价格便宜

这里我们选择了MSS。 上传文件到MSS,可以使用s3cmd,但毕竟不是每个Slave上面都有安装,通用性不强。为了保证稳定可靠,这里基于MSS的SDK写个小工具即可,比较简单,几行代码就可以搞定。

private static String TenantId = "mss_TenantId==";
private static AmazonS3 s3Client;public static void main(String[] args) throws IOException {if (args == null || args.length != 3) {System.out.println("请依次输入:inputFile、bucketName、objectName");return;}s3Client = AmazonS3ClientProvider.CreateAmazonS3Conn();uploadObject(args[0], args[1], args[2]);
}public static void uploadObject(String inputFile, String bucketName, String objectName) {try {File file = new File(inputFile);if (!file.exists()) {System.out.println("文件不存在:" + file.getPath());return;}s3Client.putObject(new PutObjectRequest(bucketName, objectName, file));System.out.printf("上传%s到MSS成功: %s/v1/%s/%s/%se", inputFile, AmazonS3ClientProvider.url, TenantId, bucketName, objectName);} catch (AmazonServiceException ase) {System.out.println("Caught an AmazonServiceException, which " +"means your request made it " +"to Amazon S3, but was rejected with an error response" +" for some reason.");System.out.println("Error Message:    " + ase.getMessage());System.out.println("HTTP Status Code: " + ase.getStatusCode());System.out.println("AWS Error Code:   " + ase.getErrorCode());System.out.println("Error Type:       " + ase.getErrorType());System.out.println("Request ID:       " + ase.getRequestId());} catch (AmazonClientException ace) {System.out.println("Caught an AmazonClientException, which " +"means the client encountered " +"an internal error while trying to " +"communicate with S3, " +"such as not being able to access the network.");System.out.println("Error Message: " + ace.getMessage());}
}

我们直接在Pipeline里构建完成后,调用这个工具就可以了。 当然,JsBundle也分类型,在调试的时候可能随时需要更新,这些JsBundle不需要永久保存,一段时间后就可以删除了。在删除时,可以参考MSS生命周期管理。所以,我们在构建JsBundle的job里,添加一个参数来区分。

//根据TYPE,上传到不同的bucket里面
def bucket = "rn-bundle-prod"
if ("${TYPE}" == "dev") {bucket = "rn-bundle-dev" //有生命周期管理,一段时间后自动删除
}
echo "开始JsBundle上传到MSS"
//jar地址需要替换成你自己的
sh "curl -s -S -L  http://s3plus.sankuai.com/v1/mss_xxxxx==/rn-bundle-prod/rn.bundle.upload-0.0.1.jar -o upload.jar"
sh "java -jar upload.jar ${archiveZip} ${bucket} ${PROJECT}/${targetZip}"
echo "上传JsBundle到MSS:${archiveZip}"

Native构建时JsBundle的下载

为了实现构建时能够自动下载,我们写了一个Gradle的插件。 首先要在build.gradle里面配置插件依赖:

classpath 'com.zjiecode:rn-bundle-gradle-plugin:0.0.1'

在需要的Module应用插件:

apply plugin: 'mt-rn-bundle-download'

在build.gradle里面配置JsBundle的信息:

RNDownloadConfig {//远程文件目录,因为有多种类型,所以这里可以填多个。paths = ['http://msstest-corp.sankuai.com/v1/mss_xxxx==/rn-bundle-dev/xxx/','http://msstest-corp.sankuai.com/v1/mss_xxxx==/rn-bundle-prod/xxx/']version  = "1"//版本号,这里使用的是打包JsBundle的BUILD_NUMBERfileName = 'xxxx.android.bundle-%s.zip' //远程文件的文件名,%s会用上面的version来填充outFile  = 'xxxx/src/main/assets/JsBundle/xxxx.android.bundle.zip' // 下载后的存储路径,相对于项目根目录
}

插件会在package的task前面,插入一个下载的task,task读取上面的配置信息,在打包阶段检查是否已经存在这个版本的JsBundle。如果不存在,就会去归档的JsBundle里,下载我们需要的JsBundle。 当然,这里的version可以使用上文介绍的注入构建信息的方式,通过job参数的方式进行注入。这样在Jenkins构建Native时,就可以动态地填写需要JsBundle的版本了。 这个Gradle插件,我们已经放到到了github仓库,你可以基于此修改,当然,也欢迎PR。 https://github.com/zjiecode/rn-bundle-gradle-plugin

六、总结

我们把一个构建分成了好几个部分,带来的好处如下:

  • 核心构建过程,只需要维护一份,减轻维护工作;
  • 方便多个人维护构建CI,避免Pipeline代码被覆盖;
  • 方便构建job的版本管理,比如要修复某个已经发布的版本,可以很方便切换到发布版本时候用的Pipeline脚本版本;
  • 每个项目,配置也比较灵活,如果项目配置不够灵活,可以尝试定义更多的变量;
  • 构建过程可视化,方便针对性优化和错误定位等。

当然,Pipeline也存在一些弊端,比如:

  • 语法不够友好,但好在Jenkins提供了一个比较强大的帮助工具(Pipeline Syntax);
  • 代码测试繁琐,没有本地运行环境,每次测试都需要提交运行一个job,等等。

当项目集成了React Native时,配合Pipeline,我们可以把JsBundle的构建产物上传到MSS归档。在构建Native的时候 ,可以动态地下载。

七、作者

张杰,美团点评高级Android工程师,2017年加入餐饮平台成都研发中心,主要负责餐饮平台B端应用开发。 王浩,美团点评高级Android工程师,2017年加入餐饮平台成都研发中心,主要负责餐饮平台B端应用开发。

八、招聘广告

本文作者来自美团成都研发中心(是的,我们在成都建研发中心啦)。我们在成都有众多后端、前端和测试的岗位正在招人,欢迎大家投递简历:songyanwei@meituan.com。

转载于:https://my.oschina.net/meituantech/blog/1922037

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/277530.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【springboot+easypoi】一行代码搞定excel导入导出

原文&#xff1a;https://www.jianshu.com/p/5d67fb720ece 开发中经常会遇到excel的处理&#xff0c;导入导出解析等等&#xff0c;java中比较流行的用poi&#xff0c;但是每次都要写大段工具类来搞定这事儿&#xff0c;此处推荐一个别人造好的轮子【easypoi】&#xff0c;下面…

用java编写一个计算器_用java程序编写一个计算器

展开全部给你一个参考&#xff0c;希望不62616964757a686964616fe58685e5aeb931333330343261要被百度吞了当晚餐import java.awt.BorderLayout;import java.awt.GridLayout;import java.awt.event.MouseEvent;import java.awt.event.MouseListener;import java.text.DecimalFor…

jquery中的ajax方法(备忘)

参考&#xff1a;https://www.cnblogs.com/tylerdonet/p/3520862.html w3school:http://www.w3school.com.cn/jquery/ajax_ajax.asp 1.url: 要求为String类型的参数&#xff0c;&#xff08;默认为当前页地址&#xff09;发送请求的地址。 2.type: 要求为String类型的参数&…

第4次作业

转载于:https://www.cnblogs.com/wzh2920330283/p/11027254.html

多核学习方法介绍

通过上篇文章的学习&#xff0c;我们知道&#xff0c;相比于单个核函数&#xff0c;多核模型可以具有更高的灵活性。经过多个核函数映射后的高维空间是由多个特征空间组合而成的组合空间&#xff0c;而显然组合空间可以组合各个子空间不同的特征映射能力&#xff0c;能够将异构…

linux go环境安装和基本项目结构

最近项目中要用到Go语言&#xff0c;所以简单总结一下安装和配置&#xff0c;Go这个语言本身就限定了很多规范&#xff0c;比如项目设置&#xff0c;编程风格等&#xff0c;开发中就不需要再因为各种规范问题纠结了&#xff0c;直接用官方规定的能避免很多坑&#xff0c;下面直…

运输层

运输层-TCP 简介 通俗点来说&#xff0c;运输层是连接底层和用户层的&#xff0c;运输层向它上面的应用层提供通信服务&#xff0c;它属于通信部分的最高层&#xff0c;同时也是用户功能的最低层。 运输层重要功能是复用和分用。复用&#xff1a;发送方不同应用进程可以使用同一…

phpStudy

很多朋友在学习php的过程中会看到phpstudy这个东西&#xff0c;那么phpstudy是做什么的呢&#xff1f;有什么用&#xff1f;接下来的这篇文章将个大家来详细的介绍一下phpstudy的内容。 首先在百度百科上对于phpstudy的定义是一个PHP调试环境的程序集成包。 该程序包集成最新的…

厉害了!Intel第九代酷睿参数曝光

2019独角兽企业重金招聘Python工程师标准>>> 导读上周有消息称&#xff0c;Intel第九代酷睿处理器最快于8月1日发布&#xff0c;共有三款主打产品&#xff0c;分别是i9-9900K、i7-9700K和i5-9600K。其中&#xff0c;i9-9900K设计为8核16线程&#xff0c;基础主频3.6…

Web应用性能分析工具—HAR文件

Web应用性能分析工具—HAR文件 来源 https://raynorli.com/2018/06/11/web-performance-analysis-har-file/ 客户经常有的一个问题就是&#xff0c;我的网页服务通过你的设备之后&#xff0c;访问变慢了&#xff0c;这类直观感受的故障很不好量化&#xff0c;而且基于Web应用的…

【mybatis】mybatis多表联查,存在一对多关系的,实体中使用List作为字段接收查询结果的写法...

实体如下&#xff1a; IntegralGoods  积分商品 IntegralGoodsImg  积分商品图片 ShelfLog    积分商品自动上架记录 IntegralGoods &#xff1a;IntegralGoodsImg&#xff1a;ShelfLog   1&#xff1a;n&#xff1a;1 1&#xff1a;1的多表联查或者m:n的多表联查 很简…

mac java tomcat_mac idea 配置tomcat

mac idea 配置tomcat一、下载安装tomcat二、有一个 javaWeb项目创建一个javaWeb项目 ,参考第一条&#xff0c;只是在第二步的时候选中java Web就行三、完善web项目在WEB-INF 下新建两个文件夹&#xff0c;lib(存放jar包)和classes(存放编译后的文件)打开项目结构设置配置classe…

30342程序格式

1.汇编语言程序格式 2.表达式操作符 转载于:https://www.cnblogs.com/ZanderZhao/p/11055237.html

SprinBoot易学难精

Spring Boot易学难精 易学 组件自动装配&#xff1a;规约大于配置&#xff0c;专注核心业务外部化配置&#xff1a;一次构建、按需调配&#xff0c;到处运行嵌入式容器&#xff1a;内纸容器、无序部署、独立运行Spring Boot Stater&#xff1a;简化依赖、按需装配、自我包含Pro…

一道没人搞得定的趣味Shell编程游戏题!,看看你会不会?

1.1猜数字编程游戏首先让系统随机生成一个数字&#xff0c;给这个数字定一个范围&#xff08;1-60&#xff09;&#xff0c;让用户输入猜的数字&#xff0c;对输入进行判断&#xff0c;如果不符合要求&#xff0c;就给予高或低的提示。其他要求&#xff1a;1、全部猜对后则给出…

java中拷贝文件的代码_拷贝文件夹中的所有文件到另外一个文件夹

[java]代码库/**** 拷贝文件夹中的所有文件到另外一个文件夹** param srcDirector* 源文件夹** param desDirector* 目标文件夹**/public static void copyFileWithDirector(String srcDirector,String desDirector) throws IOException {(new File(desDirector)).mkdirs();Fil…

infoseccrypto_java下载_關於php接ICBC的支付接口的解決方案

一&#xff1a;背景&#xff1a; 目前項目使用的是php語言開發&#xff0c;需要接入中國工商銀行的ICBC的線上支付接口。二&#xff1a;遇到的問題&#xff1a;支付時需要對數據簽名&#xff0c;但是銀行那邊不提供php版本的程序&#xff0c;只有java版本的&#xff0c;以下是對…

java windows 取所有任务_Win下,通过Jstack截取Java进程中的堆栈信息

在Java软件的使用过程中&#xff0c;有时会莫名的出现奇怪的问题。而这些问题常常无法使用日志信息定位&#xff0c;这时我们就需要通过查看进程内部线程的堆栈调用关系来分析问题出在哪里。举个例子&#xff0c;当我们在做某个操作时&#xff0c;莫名的会弹出多个警告框&#…

redis基础一_常用指令

# Redis configuration file example. # # Note that in order to read the configuration file, Redis must be # started with the file path as first argument: #./redis-server /path/to/redis.conf docker启动redis: docker run -d -p 6379:6379 -v /home/anmin/Desktop/…

滴滴Booster移动APP质量优化框架 学习之旅 三

推荐阅读&#xff1a; 滴滴Booster移动App质量优化框架-学习之旅 一 Android 模块Api化演练 不一样视角的Glide剖析(一) 滴滴Booster移动App质量优化框架-学习之旅 二对重复资源优化和无用资源优化进行了讨论。这里对不可编译无用assets资源优化进行讨论。 先看微信Matrix-ApkC…