调用链路
LoadBuild.run -> NotifyingSettingsLoader.findAndLoadSettings -> CompositeBuildSettingsLoader.findAndLoadSettings -> DefaultSettingsLoader.findAndLoadSettings -> DefaultSettingsLoader.findSettingsAndLoadIfAppropriate -> NotifyingSettingsProcessor.process -> PropertiesLoadingSettingsProcessor.process -> ScriptEvaluatingSettingsProcessor.process -> ScriptEvaluatingSettingsProcessor.applySettingsScript -> BuildOperationScriptPlugin.apply
实现分析
在 ScriptEvaluatingSettingsProcessor 里,先创建了 SettingsInternal 实例,以及 ScriptSource 实例,代表 settings.gradle 文件在内存中的映射,之后就调用 BuildOperationScriptPlugin.apply 去执行 settings.gradle 文件了。
关于 BuildOperationScriptPlugin.apply,我们后面细说,因为在解析 build.gradle 文件的时候也会用到这个方法。
下面是对应的代码:
// ScriptEvaluatingSettingsProcessor
public SettingsInternal process(GradleInternal gradle,
SettingsLocation settingsLocation,
ClassLoaderScope buildRootClassLoaderScope,
StartParameter startParameter) {
Timer settingsProcessingClock = Timers.startTimer();
Map<String, String> properties = propertiesLoader.mergeProperties(Collections.<String, String>emptyMap());
SettingsInternal settings = settingsFactory.createSettings(gradle, settingsLocation.getSettingsDir(),
settingsLocation.getSettingsScriptSource(), properties, startParameter, buildRootClassLoaderScope);
applySettingsScript(settingsLocation, settings);
LOGGER.debug(“Timing: Processing settings took: {}”, settingsProcessingClock.getElapsed());
return settings;
}
private void applySettingsScript(SettingsLocation settingsLocation, final SettingsInternal settings) {
ScriptSource settingsScriptSource = settingsLocation.getSettingsScriptSource();
ClassLoaderScope settingsClassLoaderScope = settings.getClassLoaderScope();
ScriptHandler scriptHandler = scriptHandlerFactory.create(settingsScriptSource, settingsClassLoaderScope);
ScriptPlugin configurer = configurerFactory.create(settingsScriptSource, scriptHandler, settingsClassLoaderScope, settings.getRootClassLoaderScope(), true);
configurer.apply(settings);
}
2.2.7 创建 project 以及 subproject
调用链路
LoadBuild.run -> NotifyingSettingsLoader.findAndLoadSettings -> CompositeBuildSettingsLoader.findAndLoadSettings -> DefaultSettingsLoader.findAndLoadSettings -> DefaultSettingsLoader.findSettingsAndLoadIfAppropriate -> NotifyingSettingsProcessor.process -> ProjectPropertySettingBuildLoader.load -> InstantiatingBuildLoader.load
实现分析
在解析了 settings.gradle 文件以后,就可以知道项目里有哪些 project,就可以创建 project 实例了。
// InstantiatingBuildLoader
// 这里传入的参数对应的是:rootProjectDescriptor: SettingsInternal.getRootProject() defaultProject: SettingsInternal.getDefaultProject() buildRootClassLoaderScope:SettingsInternal.getRootClassLoaderScope()
public void load(ProjectDescriptor rootProjectDescriptor, ProjectDescriptor defaultProject, GradleInternal gradle, ClassLoaderScope buildRootClassLoaderScope) {
createProjects(rootProjectDescriptor, gradle, buildRootClassLoaderScope);
attachDefaultProject(defaultProject, gradle);
}
private void attachDefaultProject(ProjectDescriptor defaultProject, GradleInternal gradle) {
gradle.setDefaultProject(gradle.getRootProject().getProjectRegistry().getProject(defaultProject.getPath()));
}
private void createProjects(ProjectDescriptor rootProjectDescriptor, GradleInternal gradle, ClassLoaderScope buildRootClassLoaderScope) {
// 创建主项目实例
// ProjectInternal 继承自 Project,最终返回的 rootProject 是 DefaultProject 类型
ProjectInternal rootProject = projectFactory.createProject(rootProjectDescriptor, null, gradle, buildRootClassLoaderScope.createChild(“root-project”), buildRootClassLoaderScope);
gradle.setRootProject(rootProject);
addProjects(rootProject, rootProjectDescriptor, gradle, buildRootClassLoaderScope);
}
private void addProjects(ProjectInternal parent, ProjectDescriptor parentProjectDescriptor, GradleInternal gradle, ClassLoaderScope buildRootClassLoaderScope) {
// 创建子项目实例
for (ProjectDescriptor childProjectDescriptor : parentProjectDescriptor.getChildren()) {
ProjectInternal childProject = projectFactory.createProject(childProjectDescriptor, parent, gradle, parent.getClassLoaderScope().createChild(“project-” + childProjectDescriptor.getName()), buildRootClassLoaderScope);
addProjects(childProject, childProjectDescriptor, gradle, buildRootClassLoaderScope);
}
}
// ProjectFactory
public DefaultProject createProject(ProjectDescriptor projectDescriptor, ProjectInternal parent, GradleInternal gradle, ClassLoaderScope selfClassLoaderScope, ClassLoaderScope baseClassLoaderScope) {
// 获取 project 对应的 build.gradle
File buildFile = projectDescriptor.getBuildFile();
ScriptSource source = UriScriptSource.file(“build file”, buildFile);
// 创建 project 实例
DefaultProject project = instantiator.newInstance(DefaultProject.class,
projectDescriptor.getName(),
parent,
projectDescriptor.getProjectDir(),
source,
gradle,
gradle.getServiceRegistryFactory(),
selfClassLoaderScope,
baseClassLoaderScope
);
// 设置 project 的层级关系
if (parent != null) {
parent.addChildProject(project);
}
// 注册 project
projectRegistry.addProject(project);
return project;
}
这里根据 settings.gradle 的配置,创建项目实例。创建子项目的时候,如果父项目不为空,就将自己设置成父项目的子项目,这样就可以通过 project.getChildProjects 获取项目的子项目了。
我们在写 gradle 脚本的时候,经常会用到的 project 属性,就是在这个时候创建出来了。
到此为止,就解析了 settings.gradle 文件然后创建了项目实例。
三、configureBuild
3.1 整体实现图
3.2 具体分析
我们之前有说到,gradle 构建过程分为配置阶段和运行阶段,配置阶段主要是执行脚本的内容,运行阶段是执行 task 的内容,这里就是配置阶段的流程。要注意,之前说的配置和运行阶段,是从整体来看的两个阶段,从源码来理解,就是这篇文章介绍的几个阶段,要更细化一点。
配置阶段执行的内容比较简单,就是把 gradle 脚本编译成 class 文件,然后运行(gradle 是采用 groovy 语言编写的,groovy 是一门 jvm 语言,所以必须要编译成 class 才能运行)。
// DefaultGradleLauncher
private void configureBuild() {
if (stage == Stage.Load) {
buildOperationExecutor.run(new ConfigureBuild());
stage = Stage.Configure;
}
}
在配置项目的时候,如果指定了 configure-on-demand 参数,只会配置主项目以及执行 task 需要的项目,默认没有指定,会配置所有的项目,这里只看默认情况。
3.2.1 配置主项目及其子项目的主要链路
调用链路
ConfigureBuild.run -> DefaultBuildConfigurer.configure -> TaskPathProjectEvaluator.configureHierarchy -> TaskPathProjectEvaluator.configure -> DefaultProject.evaluate -> LifecycleProjectEvaluator.evaluate -> LifecycleProjectEvaluator.doConfigure -> ConfigureActionsProjectEvaluator.evaluate
实现分析
// TaskPathProjectEvaluator
public void configureHierarchy(ProjectInternal project) {
configure(project);
for (Project sub : project.getSubprojects()) {
configure((ProjectInternal) sub);
}
}
最终执行到了 LifecycleProjectEvaluator.doConfigure
3.2.2 回调 BuildListener.beforeEvaluate 接口
在这里回调 beforeEvaluate 接口,通知配置将要开始。我们也就知道了这个回调执行的阶段。
3.2.3 设置默认的 task 和 插件
调用链路
ConfigureBuild.run -> DefaultBuildConfigurer.configure -> TaskPathProjectEvaluator.configureHierarchy -> TaskPathProjectEvaluator.configure -> DefaultProject.evaluate -> LifecycleProjectEvaluator.evaluate -> LifecycleProjectEvaluator.doConfigure -> ConfigureActionsProjectEvaluator.evaluate -> PluginsProjectConfigureActions.execute
实现分析
在 PluginsProjectConfigureActions 里,会给 project 添加两个 task:init 和 wrapper,然后添加帮助插件:org.gradle.help-tasks。
3.2.4 编译脚本并执行
调用链路
ConfigureBuild.run -> DefaultBuildConfigurer.configure -> TaskPathProjectEvaluator.configureHierarchy -> TaskPathProjectEvaluator.configure -> DefaultProject.evaluate -> LifecycleProjectEvaluator.evaluate -> LifecycleProjectEvaluator.doConfigure -> ConfigureActionsProjectEvaluator.evaluate -> BuildScriptProcessor.execute -> BuildOperationScriptPlugin.apply
实现分析
这里调用的还是 BuildOperationScriptPlugin.apply 去编译和执行 build.gradle 脚本,和前面解析 settings.gradle 是一样的,这里我们先知道这个就是编译 build.gradle 为 class。
文件并且执行,然后先往后看流程,后面再详细说脚本是如何编译和执行的。
3.2.5 回调 BuildListener.afterEvaluate
3.2.6 回调 BuildListener.projectsEvaluated
四、constructTaskGraph
4.1 整体实现图
4.2 具体分析
这一步是构建 task 依赖图
// DefaultGradleLauncher
private void constructTaskGraph() {
if (stage == Stage.Configure) {
buildOperationExecutor.run(new CalculateTaskGraph());
stage = Stage.TaskGraph;
}
}
4.2.1 处理需要排除的 task
调用链路
CalculateTaskGraph.run -> DefaultBuildConfigurationActionExecuter.select -> ExcludedTaskFilteringBuildConfigurationAction.configure
实现分析
// ExcludedTaskFilteringBuildConfigurationAction
public void configure(BuildExecutionContext context) {
GradleInternal gradle = context.getGradle();
Set excludedTaskNames = gradle.getStartParameter().getExcludedTaskNames();
if (!excludedTaskNames.isEmpty()) {
final Set<Spec> filters = new HashSet<Spec>();
for (String taskName : excludedTaskNames) {
filters.add(taskSelector.getFilter(taskName));
}
gradle.getTaskGraph().useFilter(Specs.intersect(filters));
}
context.proceed();
}
这一步是用来处理需要排除的 task,也就是在命令行通过 -x or --exclude-task 指定的 task,这里主要是给 TaskGraph 设置了 filter,以便在后面计算依赖的时候排除相应的 task。
4.2.2 添加默认的 task
调用链路
CalculateTaskGraph.run -> DefaultBuildConfigurationActionExecuter.select -> DefaultTasksBuildExecutionAction.configure
实现分析
这里会检查命令行里是否有传入 Task 名称进来,如果指定了要执行的 task,那么什么都不做。
如果没有指定,就看 project 是否有默认的 task,默认的 task 可以通过 defaultTasks 在 build.gradle 里进行指定。
如果也默认 task 也没有,那么就把要指定的 task 设置成 help task,也就是输出 gradle 的帮助内容。
4.2.3 计算 task 依赖图
调用链路
CalculateTaskGraph.run -> DefaultBuildConfigurationActionExecuter.select -> TaskNameResolvingBuildConfigurationAction.configure
实现分析
- 根据命令行的 taskname 筛选 task。如果我们的 task 指定了 project,也就是类似这样的 :app:assembleDebug,那么就直接选中了 task,如果没有指定具体 project,那么会把所有 project 下符合 taskname 的 task 都筛选出来。
CalculateTaskGraph.run -> DefaultBuildConfigurationActionExecuter.select -> TaskNameResolvingBuildConfigurationAction.configure -> CommandLineTaskParser.parseTasks
- 把 task 添加到 taskGraph 中,这里会处理 task 的依赖关系,包括 dependson finalizedby mustrunafter shouldrunafter,然后把信息都保存在 org.gradle.execution.taskgraph.TaskInfo 里。
CalculateTaskGraph.run -> DefaultBuildConfigurationActionExecuter.select -> TaskNameResolvingBuildConfigurationAction.configure -> DefaultTaskGraphExecuter.addTasks
4.2.4 生成 task graph
调用链路
CalculateTaskGraph.run -> TaskGraphExecuter.populate -> DefaultTaskExecutionPlan.determineExecutionPlan
实现分析
根据上一步计算的 task 及其依赖,生成 task 图
五、runTasks
5.1 整体实现图
5.2 具体分析
task 图生成以后,就开始执行 task
5.2.1 处理 dry run
调用链路
DefaultBuildExecuter.execute -> DryRunBuildExecutionAction.execute
实现分析
如果在命令行里指定了 --dry-run,在这里就会拦截 task 的执行,直接输出 task 的名称以及执行的先后关系。
5.2.2 创建线程,执行 task
调用链路
DefaultBuildExecuter.execute -> SelectedTaskExecutionAction.execute -> DefaultTaskPlanExecutor.process
实现分析
创建 TaskExecutorWorker 去执行 task,默认是 8 个线程。
// DefaultTaskPlanExecutor
public void process(TaskExecutionPlan taskExecutionPlan, Action<? super TaskInternal> taskWorker) {
ManagedExecutor executor = executorFactory.create(“Task worker for '” + taskExecutionPlan.getDisplayName() + “'”);
try {
WorkerLease parentWorkerLease = workerLeaseService.getCurrentWorkerLease();
// 开线程
startAdditionalWorkers(taskExecutionPlan, taskWorker, executor, parentWorkerLease);
taskWorker(taskExecutionPlan, taskWorker, parentWorkerLease).run();
taskExecutionPlan.awaitCompletion();
} finally {
executor.stop();
}
}
5.2.3 task 执行前处理
调用链路
DefaultBuildExecuter.execute -> SelectedTaskExecutionAction.execute -> DefaultTaskPlanExecutor.process -> TaskExecutorWorker.run -> DefaultTaskExecutionPlan.executeWithTask -> DefaultTaskExecutionPlan.selectNextTask -> DefaultTaskExecutionPlan.processTask -> EventFiringTaskWorker.execute -> DefaultBuildOperationExecutor.run
实现分析
到这里就正式开始 task 的执行过程了。有几个步骤:
- 回调 TaskExecutionListener.beforeExecute
- 链式执行一些列对 Task 的处理,具体的处理如下:
CatchExceptionTaskExecuter.execute // 加了 try catch,防止执行过程中异常
ExecuteAtMostOnceTaskExecuter.execute // 判断 task 是否执行过
SkipOnlyIfTaskExecuter.execute // 判断 task 的 onlyif 条件是否满足执行
SkipTaskWithNoActionsExecuter.execute // 跳过没有 action 的 task,没有 action 说明 task 不需要执行
ResolveTaskArtifactStateTaskExecuter.execute // 设置 artifact 状态
SkipEmptySourceFilesTaskExecuter.execute // 跳过设置了 source file 但是 source file 为空的 task,source file 为空说明 task 没有需要处理的资源
ValidatingTaskExecuter.execute() // 确认 task 是否可以执行
ResolveTaskOutputCachingStateExecuter.execute // 处理 task output 缓存
SkipUpToDateTaskExecuter.execute // 跳过 update-to-date 的 task
ExecuteActionsTaskExecuter.execute // 真正执行 task
5.2.4 task 执行
调用链路
DefaultBuildExecuter.execute -> SelectedTaskExecutionAction.execute -> DefaultTaskPlanExecutor.process -> TaskExecutorWorker.run -> DefaultTaskExecutionPlan.executeWithTask -> DefaultTaskExecutionPlan.selectNextTask -> DefaultTaskExecutionPlan.processTask -> EventFiringTaskWorker.execute -> DefaultBuildOperationExecutor.run -> ExecuteActionsTaskExecuter.execute
实现分析
经过前面一系列处理,这里开始真正执行 task 了。
- 回调 TaskActionListener.beforeActions
- 回调 OutputsGenerationListener.beforeTaskOutputsGenerated
- 取出 task 中的 Actions 全部执行
// ExecuteActionsTaskExecuter
private GradleException executeActions(TaskInternal task, TaskStateInternal state, TaskExecutionContext context) {
final List actions = new ArrayList(task.getTaskActions());
int actionNumber = 1;
for (ContextAwareTaskAction action : actions) {
// …
executeAction("Execute task action " + actionNumber + “/” + actions.size() + " for " + task.getPath(), task, action, context);
// …
actionNumber++;
}
return null;
}
这里可以看到,Task 的本质,其实就是执行其中的 Actions。举个例子来说,我们一般自定义 Task 的时候,经常用下面的写法:
task {
doLast {
// task 具体任务
}
}
这里的 doLast 就相当于给 Task 添加了一个 Action。
看一下 AbstractTask 的 doLast 方法
// AbstractTask
public Task doLast(final Action<? super Task> action) {
// …
taskMutator.mutate(“Task.doLast(Action)”, new Runnable() {
public void run() {
getTaskActions().add(wrap(action));
}
});
return this;
}
private ContextAwareTaskAction wrap(final Action<? super Task> action) {
if (action instanceof ContextAwareTaskAction) {
return (ContextAwareTaskAction) action;
}
return new TaskActionWrapper(action);
}
可以看到,我们传入的闭包,最终是包装成 TaskActionWrapper 添加到 task 的 actions 中的。
- 回调 TaskActionListener.afterActions
- 回调 TaskExecutionListener.afterExecute
六、finishBuild
6.1 整体实现图
6.2 具体分析
private void finishBuild(BuildResult result) {
if (stage == Stage.Finished) {
return;
}
buildListener.buildFinished(result);
if (!isNestedBuild()) {
gradle.getServices().get(IncludedBuildControllers.class).stopTaskExecution();
}
stage = Stage.Finished;
}
这里逻辑不多,回调了 BuildListener.buildFinished 接口
通过上面几个步骤,我们基本上看到了 gradle 的执行流程,简单来说,步骤如下:
- 解析 settings.gradle 并执行,生成 Project 实例
- 解析 build.gradle 并执行
- 生成 task 依赖图
- 执行 task
七、gradle 脚本如何编译和执行
在前面介绍 loadSettings 和 configureBuild 阶段的时候,我们提到了 BuildOperationScriptPlugin.apply 这个方法,只是简单带过,是用来编译 gradle 脚本并执行的,这里来具体分析一下。
7.1 编译脚本
调用链路
BuildOperationScriptPlugin.apply -> DefaultScriptPluginFactory.ScriptPluginImpl.apply -> DefaultScriptCompilerFactory.ScriptCompilerImpl.compile -> BuildScopeInMemoryCachingScriptClassCompiler.compile -> CrossBuildInMemoryCachingScriptClassCache.getOrCompile -> FileCacheBackedScriptClassCompiler.compile
实现分析
这里编译过程分为两部分,首先编译脚本的 buildscript {} 部分,忽略其他部分,然后再编译脚本的其他部分并执行。所以 buildscript {} 里的内容会先于其他内容执行。
-
会先检查缓存,如果有缓存的话,直接使用,没有缓存再进行编译
-
最终会调用到 CompileToCrossBuildCacheAction.execute -> DefaultScriptCompilationHandler.compileToDir -> DefaultScriptCompilationHandler.compileScript 去执行真正的编译操作
脚本缓存路径: /Users/zy/.gradle/caches/4.1/scripts-remapped/build_a3v29m9cbrge95ug6eejz9wuw/31f5shvfkfunwn5ullupyy7xt/cp_proj4dada6424967ba8dfea75e81c8880f7f/classes
目录下的 class 如下:
- 具体编译方法是通过 RemappingScriptSource.getResource().getText() 获取到脚本内容,然后通过 GroovyClassLoader.parseClass 编译的。
我们以 app/build.gradle 为例,看一下最终生成的脚本是什么样子的。
build.gradle 脚本内容
apply plugin: ‘com.android.application’
apply plugin: ‘myplugin’
android {
compileSdkVersion 26
defaultConfig {
applicationId “com.zy.easygradle”
minSdkVersion 19
targetSdkVersion 26
versionCode 1
versionName “1.0”
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
}
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
flavorDimensions “size”, “color”
productFlavors {
big {
dimension “size”
}
small {
dimension “size”
}
blue {
dimension “color”
}
red {
dimension “color”
}
}
}
dependencies {
// implementation gradleApi()
implementation fileTree(dir: ‘libs’, include: [‘*.jar’])
implementation ‘com.android.support:appcompat-v7:26.1.0’
implementation ‘com.android.support.constraint:constraint-layout:1.1.3’
implementation project(‘:module1’)
}
gradle.addBuildListener(new BuildListener() {
@Override
void buildStarted(Gradle gradle) {
// println(‘构建开始’)
}
@Override
void settingsEvaluated(Settings settings) {
// println(‘settings 文件解析完成’)
}
@Override
void projectsLoaded(Gradle gradle) {
// println(‘项目加载完成’)
}
@Override
void projectsEvaluated(Gradle gradle) {
// println(‘项目解析完成’)
}
@Override
void buildFinished(BuildResult result) {
// println(‘构建完成’)
}
})
gradle.addProjectEvaluationListener(new ProjectEvaluationListener() {
@Override
void beforeEvaluate(Project project) {
// println(“${project.name} 项目配置之前调用”)
}
@Override
void afterEvaluate(Project project, ProjectState state) {
// println(“${project.name} 项目配置之后调用”)
}
})
gradle.taskGraph.whenReady {
// println(“task 图构建完成”)
}
gradle.taskGraph.beforeTask {
// println(“task 执行完成”)
}
gradle.taskGraph.afterTask {
// println(“task 执行完成”)
}
task task1 {
doLast {
println(‘task2’)
}
}
task task2 {
doLast {
println(‘task2’)
}
}
task1.finalizedBy(task2)
编译后 class 内容
package defpackage;
import groovy.lang.MetaClass;
import java.lang.ref.SoftReference;
import org.codehaus.groovy.reflection.ClassInfo;
import org.codehaus.groovy.runtime.GStringImpl;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.callsite.CallSite;
import org.codehaus.groovy.runtime.callsite.CallSiteArray;
import org.codehaus.groovy.runtime.typehandling.ShortTypeHandling;
import org.gradle.api.internal.project.ProjectScript;
import org.gradle.internal.scripts.ScriptOrigin;
/* compiled from: /Users/zy/workspace/note/blog/android-training/gradle/EasyGradle/app/build.gradle /
public class build_ak168fqfikdepd6py4yef8tgs extends ProjectScript implements ScriptOrigin {
private static / synthetic / SoftReference $callSiteArray = null;
private static / synthetic / ClassInfo KaTeX parse error: Expected group after '_' at position 73: …tic */ boolean _̲_stMC = false;
private static final / synthetic / String __originalClassName = “BuildScript”;
private static final / synthetic */ String __signature = “988274f32891a2a3d3b8d16074617c05”;
private static /* synthetic */ CallSiteArray KaTeX parse error: Expected '}', got 'EOF' at end of input: …epd6py4yef8tgs.createCallSiteArray_1(strArr);
return new CallSiteArray(build_ak168fqfikdepd6py4yef8tgs.class, strArr);
}
private static /* synthetic */ void $createCallSiteArray_1(String[] strArr) {
strArr[0] = “apply”;
strArr[1] = “apply”;
strArr[2] = “android”;
strArr[3] = “dependencies”;
strArr[4] = “addBuildListener”;
strArr[5] = “gradle”;
strArr[6] = “addProjectEvaluationListener”;
strArr[7] = “gradle”;
strArr[8] = “whenReady”;
strArr[9] = “taskGraph”;
strArr[10] = “gradle”;
strArr[11] = “beforeTask”;
strArr[12] = “taskGraph”;
strArr[13] = “gradle”;
strArr[14] = “afterTask”;
strArr[15] = “taskGraph”;
strArr[16] = “gradle”;
strArr[17] = “task”;
strArr[18] = “task”;
strArr[19] = “finalizedBy”;
strArr[20] = “task1”;
strArr[21] = “task2”;
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数同学面临毕业设计项目选题时,很多人都会感到无从下手,尤其是对于计算机专业的学生来说,选择一个合适的题目尤为重要。因为毕业设计不仅是我们在大学四年学习的一个总结,更是展示自己能力的重要机会。
因此收集整理了一份《2024年计算机毕业设计项目大全》,初衷也很简单,就是希望能够帮助提高效率,同时减轻大家的负担。
既有Java、Web、PHP、也有C、小程序、Python等项目供你选择,真正体系化!
由于项目比较多,这里只是将部分目录截图出来,每个节点里面都包含素材文档、项目源码、讲解视频
如果你觉得这些内容对你有帮助,可以添加VX:vip1024c (备注项目大全获取)
ask";
strArr[18] = “task”;
strArr[19] = “finalizedBy”;
strArr[20] = “task1”;
strArr[21] = “task2”;
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数同学面临毕业设计项目选题时,很多人都会感到无从下手,尤其是对于计算机专业的学生来说,选择一个合适的题目尤为重要。因为毕业设计不仅是我们在大学四年学习的一个总结,更是展示自己能力的重要机会。
因此收集整理了一份《2024年计算机毕业设计项目大全》,初衷也很简单,就是希望能够帮助提高效率,同时减轻大家的负担。
[外链图片转存中…(img-UfepdkJW-1712542049616)]
[外链图片转存中…(img-Wg2Wd6IR-1712542049617)]
[外链图片转存中…(img-YFGLw9Y9-1712542049617)]
既有Java、Web、PHP、也有C、小程序、Python等项目供你选择,真正体系化!
由于项目比较多,这里只是将部分目录截图出来,每个节点里面都包含素材文档、项目源码、讲解视频
如果你觉得这些内容对你有帮助,可以添加VX:vip1024c (备注项目大全获取)
[外链图片转存中…(img-4D0oYf4k-1712542049617)]