一、源码依赖

本文基于:

android gradle plugin版本:

com.android.tools.build:gradle:2.3.0

gradle 版本:4.1

Gradle源码总共30个G,为简单起见,方便大家看源码,此处通过gradle依赖的形式来查看源码,依赖源码姿势:

创建一个新工程,app 项目目录中删除所有文件,仅留下gradle文件,依赖

apply plugin: 'java'
sourceCompatibility = 1.8
dependencies {compile gradleApi()compile 'com.android.tools.build:gradle:2.3.0'
}

将跟目录下的gradle文件,删除掉gradle依赖

buildscript {repositories {google()jcenter()}dependencies {
// compile 'com.android.tools.build:gradle:2.3.0'}
}

然后rebuild一下,就可以在External Libraries中查看到android gradle的源码已经依赖了

二、Android Gradle Plugin简介

我们知道Android gradle plugin是用来构建Android工程的gradle插件,在Android gradle 插件中,可以看到app工程和library工程所依赖的plugin是不一样的

// app 工程
apply plugin: 'com.android.application'
// library 工程
apply plugin: 'com.android.library'

而对应填写andorid块中所填写的配置也不同,这就是区分Application和Library的插件的extension块

分别为:

app工程 -> AppPlugin -> AppExtension
librar工程 -> LibraryPlugin -> LibraryExtension

对应的是AppPlugin和AppExtension,这两个插件构建的流程大抵是相同的,只是各自插件生成的任务不同,接下来我们着重分析Application插件是如何构建我们的Android应用的

三、AppPlugin的构建流程

我们先看下app工程中gradle的文件格式

apply plugin: 'com.android.application'
android {compileSdkVersion 25buildToolsVersion '26.0.2'defaultConfig {applicationId "com.zengshaoyi.gradledemo"minSdkVersion 15targetSdkVersion 25versionCode project.ext.versionCodeversionName project.ext.versionNametestInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'}}lintOptions {abortOnError false}
}

跟踪apply方法,其实是进入到

AppPlugin的apply的方法,我们可以看到内部实现是直接调用父类BasePlugin的apply方法

protected void apply(@NonNull Project project) {checkPluginVersion();this.project = project;ExecutionConfigurationUtil.setThreadPoolSize(project);checkPathForErrors();checkModulesForErrors();ProfilerInitializer.init(project);threadRecorder = ThreadRecorder.get();ProcessProfileWriter.getProject(project.getPath()).setAndroidPluginVersion(Version.ANDROID_GRADLE_PLUGIN_VERSION).setAndroidPlugin(getAnalyticsPluginType()).setPluginGeneration(GradleBuildProject.PluginGeneration.FIRST);threadRecorder.record(ExecutionType.BASE_PLUGIN_PROJECT_CONFIGURE,project.getPath(),null,this::configureProject);threadRecorder.record(ExecutionType.BASE_PLUGIN_PROJECT_BASE_EXTENSION_CREATION,project.getPath(),null,this::configureExtension);threadRecorder.record(ExecutionType.BASE_PLUGIN_PROJECT_TASKS_CREATION,project.getPath(),null,this::createTasks);// Apply additional pluginsfor (String plugin : AndroidGradleOptions.getAdditionalPlugins(project)) {project.apply(ImmutableMap.of("plugin", plugin));}}

threadRecoirder.recode()是记录最后一个参数的路径和执行的时间点,前面做了一些必要性的信息检测之前,其实主要做了以下几件事情:

// 配置项目,设置构建回调
this::configureProject
// 配置Extension
this::configureExtension
// 创建任务
this::createTasks

::是java 8引入的特性,详情可以查看java8特性 ,这里就是方法的调用

configureProject

直接来看源码

private void configureProject() {extraModelInfo = new ExtraModelInfo(project);checkGradleVersion();AndroidGradleOptions.validate(project);// Android SDK处理类sdkHandler = new SdkHandler(project, getLogger());// 设置项目评估阶段回调project.afterEvaluate(p -> {// TODO: Read flag from extension.if (!p.getGradle().getStartParameter().isOffline()&& AndroidGradleOptions.getUseSdkDownload(p)) {// 相关配置依赖的下载处理 SdkLibData sdkLibData =SdkLibData.download(getDownloader(), getSettingsController());dependencyManager.setSdkLibData(sdkLibData);sdkHandler.setSdkLibData(sdkLibData);}});// 创建AndroidBuilderandroidBuilder = new AndroidBuilder(project == project.getRootProject() ? project.getName() : project.getPath(),creator,new GradleProcessExecutor(project),new GradleJavaProcessExecutor(project),extraModelInfo,getLogger(),isVerbose());// dataBinding的相关处理dataBindingBuilder = new DataBindingBuilder();dataBindingBuilder.setPrintMachineReadableOutput(extraModelInfo.getErrorFormatMode() ==ExtraModelInfo.ErrorFormatMode.MACHINE_PARSABLE);// Apply the Java and Jacoco plugins.project.getPlugins().apply(JavaBasePlugin.class);project.getPlugins().apply(JacocoPlugin.class);// 给assemble任务添加描述project.getTasks().getByName("assemble").setDescription("Assembles all variants of all applications and secondary packages.");...

可以看到 configureProject 方法中在 project.afterEvaluate 设置了回调,当项目评估结束时,根据项目配置情况,设置 dependece 依赖;创建了 AndroidBuilder 对象,这个对象是用来合并manifest 和创建 dex 等作用,后面在创建任务的过程中会使用到,结下来继续看 configureProject 的源码

 // call back on execution. This is called after the whole build is done (not// after the current project is done).// This is will be called for each (android) projects though, so this should support// being called 2+ times.// 设置构建回调project.getGradle().addBuildListener(new BuildListener() {private final LibraryCache libraryCache = LibraryCache.getCache();@Overridepublic void buildStarted(Gradle gradle) {}@Overridepublic void settingsEvaluated(Settings settings) {}@Overridepublic void projectsLoaded(Gradle gradle) {}@Overridepublic void projectsEvaluated(Gradle gradle) {}@Overridepublic void buildFinished(BuildResult buildResult) {ExecutorSingleton.shutdown();sdkHandler.unload();threadRecorder.record(ExecutionType.BASE_PLUGIN_BUILD_FINISHED,project.getPath(),null,() -> {// 当任务执行完成时,清楚dex缓存PreDexCache.getCache().clear(FileUtils.join(project.getRootProject().getBuildDir(),FD_INTERMEDIATES,"dex-cache","cache.xml"),getLogger());JackConversionCache.getCache().clear(FileUtils.join(project.getRootProject().getBuildDir(),FD_INTERMEDIATES,"jack-cache","cache.xml"),getLogger());libraryCache.unload();Main.clearInternTables();});}});// 设置创建有向图任务回调project.getGradle().getTaskGraph().addTaskExecutionGraphListener(taskGraph -> {for (Task task : taskGraph.getAllTasks()) {// TransformTask是class编译成dex的重要任务if (task instanceof TransformTask) {Transform transform = ((TransformTask) task).getTransform();if (transform instanceof DexTransform) {PreDexCache.getCache().load(FileUtils.join(project.getRootProject().getBuildDir(),FD_INTERMEDIATES,"dex-cache","cache.xml"));break;} else if (transform instanceof JackPreDexTransform) {JackConversionCache.getCache().load(FileUtils.join(project.getRootProject().getBuildDir(),FD_INTERMEDIATES,"jack-cache","cache.xml"));break;}}}});

这里在添加了 BuildListener,在 buildFinished 的时候清楚了dex缓存,而在任务有向图创建的回调中,判断是否是 DexTransfrom,从而从缓存中加载dex。

总结一下 configureProject 做的事情,主要是进行版本有效性的判断,创建了 AndroidBuilder 对象,并设置了构建流程的回调来处理依赖和dex的加载和缓存清理。

configureExtension

这个阶段就是配置 extension 的阶段,就是创建我们 android 块中的可配置的对象

private void configureExtension() {final NamedDomainObjectContainer<BuildType> buildTypeContainer =project.container(BuildType.class,new BuildTypeFactory(instantiator, project, project.getLogger()));final NamedDomainObjectContainer<ProductFlavor> productFlavorContainer =project.container(ProductFlavor.class,new ProductFlavorFactory(instantiator, project, project.getLogger(), extraModelInfo));final NamedDomainObjectContainer<SigningConfig> signingConfigContainer =project.container(SigningConfig.class, new SigningConfigFactory(instantiator));extension =createExtension(project,instantiator,androidBuilder,sdkHandler,buildTypeContainer,productFlavorContainer,signingConfigContainer,extraModelInfo);...

首先创建了 BuildType、ProductFlavor、SigningConfig 三个类型的Container,接着传入到了createExtension方法中,点入查看是个抽象的方法,各自的实现在子类中,这里也就是我们的AppPlugin 中

@NonNull@Overrideprotected BaseExtension createExtension(@NonNull Project project,@NonNull Instantiator instantiator,@NonNull AndroidBuilder androidBuilder,@NonNull SdkHandler sdkHandler,@NonNull NamedDomainObjectContainer<BuildType> buildTypeContainer,@NonNull NamedDomainObjectContainer<ProductFlavor> productFlavorContainer,@NonNull NamedDomainObjectContainer<SigningConfig> signingConfigContainer,@NonNull ExtraModelInfo extraModelInfo) {return project.getExtensions().create("android",AppExtension.class,project,instantiator,androidBuilder,sdkHandler,buildTypeContainer,productFlavorContainer,signingConfigContainer,extraModelInfo);}

这里也就是可以看到我们android块配置是如何来的了,对应的Extension也确实是AppExtension,继续查看 configureExtension 的源码

 dependencyManager = new DependencyManager(project,extraModelInfo,sdkHandler);ndkHandler = new NdkHandler(project.getRootDir(),null, /* compileSkdVersion, this will be set in afterEvaluate */"gcc","" /*toolchainVersion*/);taskManager =createTaskManager(project,androidBuilder,dataBindingBuilder,extension,sdkHandler,ndkHandler,dependencyManager,registry,threadRecorder);variantFactory = createVariantFactory(instantiator, androidBuilder, extension);variantManager =new VariantManager(project,androidBuilder,extension,variantFactory,taskManager,instantiator,threadRecorder);// Register a builder for the custom tooling modelModelBuilder modelBuilder = new ModelBuilder(androidBuilder,variantManager,taskManager,extension,extraModelInfo,ndkHandler,new NativeLibraryFactoryImpl(ndkHandler),getProjectType(),AndroidProject.GENERATION_ORIGINAL);registry.register(modelBuilder);// Register a builder for the native tooling modelNativeModelBuilder nativeModelBuilder = new NativeModelBuilder(variantManager);registry.register(nativeModelBuilder);

这一部分主要是创建一些管理类,其中 createTaskManager、createVariantFactory 都是抽象方法,对应的实现类

createTaskManager
AppPlugin -> ApplicationTaskManager
LibraryPlugin -> LibraryTaskManager
createVariantFactory
AppPlugin -> ApplicationVariantFactory
LibraryPlugin -> LibraryVariantFactory

这里简单介绍一下 TaskManager 就是创建具体任务的管理类,app 工程和库 library 工程所需的构建任务是不同的,后面我们会介绍 app 工程创建的构建任务;VariantFactory 就是我们常说的构建变体的工厂类,主要是生成Variant(构建变体)的对象。我们回到 createExtension 的源码中

 // map the whenObjectAdded callbacks on the containers.signingConfigContainer.whenObjectAdded(variantManager::addSigningConfig);buildTypeContainer.whenObjectAdded(buildType -> {SigningConfig signingConfig =signingConfigContainer.findByName(BuilderConstants.DEBUG);buildType.init(signingConfig);variantManager.addBuildType(buildType);});productFlavorContainer.whenObjectAdded(variantManager::addProductFlavor);...// create default Objects, signingConfig first as its used by the BuildTypes.variantFactory.createDefaultComponents(buildTypeContainer, productFlavorContainer, signingConfigContainer);

这一部分做得事情,配置了 BuildTypeContainer、ProductFlavorContainer、SigningConfigContainer 这三个配置项的 whenObjectAdded 的回调,每个配置的添加都会加入到 variantManager 中;创建默认配置,下面是 ApplicationVariantFactory 的 createDefaultComponents 代码

 @Overridepublic void createDefaultComponents(@NonNull NamedDomainObjectContainer<BuildType> buildTypes,@NonNull NamedDomainObjectContainer<ProductFlavor> productFlavors,@NonNull NamedDomainObjectContainer<SigningConfig> signingConfigs) {// must create signing config first so that build type 'debug' can be initialized// with the debug signing config.signingConfigs.create(DEBUG);buildTypes.create(DEBUG);buildTypes.create(RELEASE);}

总结一下 configureExtension 方法的作用,主要是创建 Android 插件的扩展对象,对配置项 BuildType、ProductFlavor、SigningConfig 做了统一的创建和回调处理, 创建taskManager、variantFactory、variantManager。

createTasks

private void createTasks() {threadRecorder.record(ExecutionType.TASK_MANAGER_CREATE_TASKS,project.getPath(),null,() -> // 在项目评估之前创建任务 taskManager.createTasksBeforeEvaluate(new TaskContainerAdaptor(project.getTasks())));project.afterEvaluate(project ->threadRecorder.record(ExecutionType.BASE_PLUGIN_CREATE_ANDROID_TASKS,project.getPath(),null,// 在项目评估完成之后创建 androidTask() -> createAndroidTasks(false)));}

这里主要是分两块,一个是在 beforeEvaluate 创建任务;一个是在 afterEvaluate 创建任务。这里的区别是 AndroidTask 是依赖配置项的配置才能生成相应任务,所以是需要在 afterEvaluate 之后创建,如果对项目评估回调不理解的话,可以查阅Project文档。beforeEvaluate 创建的任务跟我们编译没有太大关系,我们重点查看一下 afterEvaluate 创建的任务 createAndroidTasks

 @VisibleForTestingfinal void createAndroidTasks(boolean force) {...threadRecorder.record(ExecutionType.VARIANT_MANAGER_CREATE_ANDROID_TASKS,project.getPath(),null,() -> {// 创建AndroidTasksvariantManager.createAndroidTasks();ApiObjectFactory apiObjectFactory =new ApiObjectFactory(androidBuilder, extension, variantFactory, instantiator);for (BaseVariantData variantData : variantManager.getVariantDataList()) {apiObjectFactory.create(variantData);}});...}

我们主要看下variantManager的createAndroidTasks的方法

 /*** Variant/Task creation entry point.** Not used by gradle-experimental.*/public void createAndroidTasks() {variantFactory.validateModel(this);variantFactory.preVariantWork(project);final TaskFactory tasks = new TaskContainerAdaptor(project.getTasks());if (variantDataList.isEmpty()) {recorder.record(ExecutionType.VARIANT_MANAGER_CREATE_VARIANTS,project.getPath(),null /*variantName*/,this::populateVariantDataList);}// Create top level test tasks.recorder.record(ExecutionType.VARIANT_MANAGER_CREATE_TESTS_TASKS,project.getPath(),null /*variantName*/,() -> taskManager.createTopLevelTestTasks(tasks, !productFlavors.isEmpty()));for (final BaseVariantData<? extends BaseVariantOutputData> variantData : variantDataList) {recorder.record(ExecutionType.VARIANT_MANAGER_CREATE_TASKS_FOR_VARIANT,project.getPath(),variantData.getName(),() -> createTasksForVariantData(tasks, variantData));}taskManager.createReportTasks(tasks, variantDataList);}

首先判断 variantDataList 是否是空,如果是空的就会进入到 populateVariantDataList 方法中

/*** Create all variants.*/public void populateVariantDataList() {if (productFlavors.isEmpty()) {createVariantDataForProductFlavors(Collections.emptyList());} else {List<String> flavorDimensionList = extension.getFlavorDimensionList();// Create iterable to get GradleProductFlavor from ProductFlavorData.Iterable<CoreProductFlavor> flavorDsl =Iterables.transform(productFlavors.values(),ProductFlavorData::getProductFlavor);// Get a list of all combinations of product flavors.List<ProductFlavorCombo<CoreProductFlavor>> flavorComboList =ProductFlavorCombo.createCombinations(flavorDimensionList,flavorDsl);for (ProductFlavorCombo<CoreProductFlavor> flavorCombo : flavorComboList) {//noinspection uncheckedcreateVariantDataForProductFlavors((List<ProductFlavor>) (List) flavorCombo.getFlavorList());}}}

从方法注释可以看到,这个方法主要的作用就是创建所有的 variants,试想一下该段代码会做哪些事情,是否是解析 buildType、productFlavor 配置?

创建构建变体(BuildVariant)

继续观察上面的代码,可以看到无论是否有配置productFlavor 子项,都会进入到 createVariantDataForProductFlavors 方法。如果有配置的话,通过获取配置的 flavorDimension 和 productFlavor 数组,调用 ProductFlavorCombo.createCombinations 组合出最后的产品风味数组 flavorComboList ,最后通过遍历调用 createVariantDataForProductFlavors 方法

 /*** Creates VariantData for a specified list of product flavor.** This will create VariantData for all build types of the given flavors.** @param productFlavorList the flavor(s) to build.*/private void createVariantDataForProductFlavors(@NonNull List<ProductFlavor> productFlavorList) {...for (BuildTypeData buildTypeData : buildTypes.values()) {boolean ignore = false;...if (!ignore) {BaseVariantData<?> variantData = createVariantData(buildTypeData.getBuildType(),productFlavorList);variantDataList.add(variantData);...}}...
}

看上述代码,通过 creatVariantData 方法,将 buildType 和 productFlavor 的作为参数传入,创建了 variantData,并且加入到了 variantDataList 集合中,这里我们就是将所有的构建变体集合到了 variantDataList 中。

接着我们返回继续看 createAndroidTasks 方法

 /*** Variant/Task creation entry point.** Not used by gradle-experimental.*/public void createAndroidTasks() {...for (final BaseVariantData<? extends BaseVariantOutputData> variantData : variantDataList) {recorder.record(ExecutionType.VARIANT_MANAGER_CREATE_TASKS_FOR_VARIANT,project.getPath(),variantData.getName(),() -> createTasksForVariantData(tasks, variantData));}...}

通过上面拿到的variantDataList,遍历该集合来创建任务

 /*** Create tasks for the specified variantData.*/public void createTasksForVariantData(final TaskFactory tasks,final BaseVariantData<? extends BaseVariantOutputData> variantData) {final BuildTypeData buildTypeData = buildTypes.get(variantData.getVariantConfiguration().getBuildType().getName());if (buildTypeData.getAssembleTask() == null) {// 创建assemble + buildType任务buildTypeData.setAssembleTask(taskManager.createAssembleTask(tasks, buildTypeData));}// Add dependency of assemble task on assemble build type task.tasks.named("assemble", new Action<Task>() {@Overridepublic void execute(Task task) {assert buildTypeData.getAssembleTask() != null;// 将 assemble 任务依赖于我们的 assemble + buildType 任务task.dependsOn(buildTypeData.getAssembleTask().getName());}});VariantType variantType = variantData.getType();// 根据 variantData 创建 assemble + flavor + buildType 任务createAssembleTaskForVariantData(tasks, variantData);if (variantType.isForTesting()) {...} else {// 根据 variantData 创建一系列任务taskManager.createTasksForVariantData(tasks, variantData);}}

首先会先根据 buildType 信息创建 assemble + buildType 的任务,可以看下taskManager. createAssembleTask里的代码

 @NonNullpublic AndroidTask<DefaultTask> createAssembleTask(@NonNull TaskFactory tasks,@NonNull VariantDimensionData dimensionData) {final String sourceSetName =StringHelper.capitalize(dimensionData.getSourceSet().getName());return androidTasks.create(tasks,// 设置任务名字为 assembleXXX"assemble" + sourceSetName,assembleTask -> {// 设置描述和任务组assembleTask.setDescription("Assembles all " + sourceSetName + " builds.");assembleTask.setGroup(BasePlugin.BUILD_GROUP);});}

创建完任务之后,将assemble任务依赖于我们的assembleXXX任务,随后调用 createAssembleTaskForVariantData 方法,此方法是创建 assemble + flavor + buildType 任务,流程多了 productFlavor 任务的创建,这里就不赘述了。后面会执 createTasksForVariantData,这个方法就是根据 variant 生成一系列 Android 构建所需任务(后面会详细介绍),回到 createAndroidTasks 方法中

threadRecorder.record(ExecutionType.VARIANT_MANAGER_CREATE_ANDROID_TASKS,project.getPath(),null,() -> {variantManager.createAndroidTasks();ApiObjectFactory apiObjectFactory =new ApiObjectFactory(androidBuilder, extension, variantFactory, instantiator);for (BaseVariantData variantData : variantManager.getVariantDataList()) {// 创建variantApi,添加到extensions中apiObjectFactory.create(variantData);}});

最后就遍历 variantDataList 通过 ApiObjectFactory 创建 variantApi,添加到 extensions 中。至此,我们就已经将配置的构建变种任务已经添加到我们的任务列表中,并形成了相关依赖。
    一篇文太长,还有一半下一章发出来。

最后

感谢你到这里,喜欢的话请帮忙点个赞让更多需要的人看到哦。更多Android进阶技术,面试资料整理分享,职业生涯规划,产品,思维,行业观察,谈天说地。可以加Android架构师群;701740775。

Android Gradle Plugin 源码解析(上)相关推荐

  1. Android Gradle Plugin 源码解析之 externalNativeBuild

    在Android Studio 2.2开始的Android Gradle Plugin版本中,Google集成了对cmake的完美支持,而原先的ndkBuild的方式支持也变得更加良好.这篇文章就来说 ...

  2. Android Gradle Plugin 源码阅读与编译

    前言 为了解一些Andorid的构建流程,有时候需要阅读Android Gradle Plugin的相关源码的.自己阅读Android Gradle Plugin源码主要经历了三个时期: 1.AOSP ...

  3. BAT高级架构师合力熬夜15天,肝出了这份PDF版《Android百大框架源码解析》,还不快快码住。。。

    前言 为什么要阅读源码? 现在中高级Android岗位面试中,对于各种框架的源码都会刨根问底,从而来判断应试者的业务能力边际所在.但是很多开发者习惯直接搬运,对各种框架的源码都没有过深入研究,在面试时 ...

  4. Android通知系统源码解析

    Android通知系统源码解析 1. 概述 2. 流程图 2.1. 发送通知流程图 3. 源码解析 3.1. 使用通知--APP进程 3.1.1. 创建通知: 3.1.2. 发送(更新)通知: 3.1 ...

  5. Kubernetes学习笔记之Calico CNI Plugin源码解析(二)

    女主宣言 今天小编继续为大家分享Kubernetes Calico CNI Plugin学习笔记,希望能对大家有所帮助. PS:丰富的一线技术.多元化的表现形式,尽在"360云计算" ...

  6. http://a.codekk.com/detail/Android/grumoon/Volley 源码解析

    http://a.codekk.com/detail/Android/grumoon/Volley 源码解析

  7. Android之EasyPermissions源码解析

    转载请标明出处:[顾林海的博客] 个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持! 前言 我们知道在Android中想要申请权限就需要在AndroidManifest ...

  8. Android之AsyncTask源码解析

    转载请标明出处:[顾林海的博客] 个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持! ##前言 AsyncTask是一种轻量级的异步任务类,内部封装了Thread和Ha ...

  9. Android之DiskLruCache源码解析

    转载请标明出处: http://blog.csdn.net/hai_qing_xu_kong/article/details/73863258 本文出自:[顾林海的博客] 个人开发的微信小程序,目前功 ...

最新文章

  1. 年赚百万烤肉店老板嘲讽程序员:你们拼死拼活也挣不到100万
  2. for android 软件,安卓特工 for Android
  3. php基础+jquery基础
  4. 利用HTTP Cache来优化网站
  5. 测试教程网.unittest教程.7. 各种断言方法
  6. 【2017年第2期】应用驱动的大数据融合平台建设
  7. 除了陈真处外的深圳论坛SZ4J
  8. Windows下nginx的安装及使用方法入门
  9. Linux内核的文档管理工具:Sphinx
  10. 固件是通用的吗_冷镦和冷挤压是一回事吗,两者有什么区别?
  11. 解决Nginx + PHP(FastCGI)遇到的502 Bad Gateway错误[原创]
  12. ftp一句話download
  13. Win10 x64 VS2015 MFC打开主对话框报错:“未在此计算机上注册activex控件{648A5600-2C6E-101B-82B6-000000000014}”
  14. wps共享文档无法连接服务器,WPS云文档链接分享后对方没有访问权限?解决办法在此...
  15. Udacity 传感器融合笔记 (一)lidar
  16. file-saver实现文件流下载
  17. QNX和linux的区别 -- qnx4.0 内核介绍 -- 微内核 -- qnx与vxworks区别
  18. 张勇卸任淘宝董事长,戴珊接任;苹果称不送充电器已节省55万吨矿石;Windows彻底告别SMB1传输协议|极客头条
  19. 修改 nginx 的默认端口
  20. linux系统读取plc状态,Linux系统下上位机通讯协议及PLC冗余系统组态-工业支持中心-西门子中国...

热门文章

  1. MacBook如何用Parallels Desktop安装windows7/8
  2. Centos中文输入法安装以及切换
  3. 如何删除mac通用二进制文件
  4. 如何免费(轻成本)在网上做推广宣传
  5. Servlet,过滤器,监听器,拦截器的区别
  6. symfony2 Process 组件的学习笔记
  7. 对卫星网络及内容的安全防护措施
  8. 远程安装oracle 10.2.1 for redhat 5.0 2.6.18-53.el5xen
  9. KVM libvirt的CPU热添加
  10. BZOJ 2440: [中山市选2011]完全平方数 [容斥原理 莫比乌斯函数]