干货: 在 Flutter 项目下安卓 flavor 打包配置实践 | 开发者说·DTalk

干货: 在 Flutter 项目下安卓 flavor 打包配置实践 | 开发者说·DTalk
www.zeeklog.com  - 干货: 在 Flutter 项目下安卓 flavor 打包配置实践 | 开发者说·DTalk

本文原作者: 狐友技术团队,原文发布于搜狐技术产品:

https://mp.weixin.qq.com/s/uOMxhc8xnFPmi7j_Z5JqVg

1.前言

Flutter 是 Google 这几年大力推广的跨平台 UI 框架,可以快速在 iOS 和 Android 上构建高质量的原生用户界面。在架构搭建阶段,我们依然需要原生技术的支持。比如说,我们在开发 Android 项目时,会通过在 gradle 文件中配置 Flavor 来实现不同渠道的属性配置,之后通过在编译过程中自动生成 BuildConfig 文件来读取不同 Flavor 下的各种属性。

在 Flutter 项目中,我们如何实现不同 Flavor 下读取相应属性并实现多渠道打包呢?以及 Flutter 的打包过程跟 Android 原生打包有什么不同呢?

本文将以 Flutter 1.22.4 版本为基础,通过 Flutter 项目中对于 Android 工程的构建流程详解,来进行 Flavor 配置说明以及 Apk 构建过程的详细分析。并针对 SDK 的 bug 提出了解决方案及原因梳理。

2.Flavor 配置及打包

2.1 Flavor 配置

在移动开发中,我们通常需要配置三个开发环境,分别为开发环境,测试环境和生产环境。不同的环境配置不同的 Host,及第三方 sdk 的 id,如分享,推送等功能。为了方便测试在同一个手机安装不同环境的 apk,还会为不同环境配置不同包名。配置方式如下: 首先在 gradle 文件中添加 productFlavors,同其他的 Android 工程的配置方式:

productFlavors {
    // 生产环境
    flavoronline {
        applicationId "com.sohu.flavor"
    }
    // 开发环境
    flavordev {
        applicationId "com.sohu.flavor.dev"
    }
    // 测试环境
    flavortest {
        applicationId "com.sohu.flavor.test"
    }
}

需要注意的是,所有的 Flavor 名称都需要是小写的,原因之后的章节会进行说明。

2.2 为不同 Flavor 配置入口文件

Flutter 项目并没有类似 BuildConfig 这样自动生成不同 Flavor 下的配置文件。如果读取 Android 层的 BuildConfig 文件需要通过 MethodChannel,异步获取。在现实场景中,冷启动的时候就需要根据 Flavor 上报一些数据,或进行业务处理。所以在不同的 Flavor下,Flutter 建议在 App 初始化时指定不同的入口文件来进行不同配置的实现,实现方式如下: 我们知道默认情况下,Flutter 工程的入口文件是为 lib/main.dart。为了实现多入口,我们需要写三个 Flavor 的入口文件来替代 main.dart。分别为:

1. main_android_dev.dart

import 'package:flutter/material.dart';
import 'package:supermarie/common/config/app_config.dart';
import 'package:supermarie/my_app.dart';


void main() {
  AppConfig.init(ConfigType.androidDev);
  runApp(MyAppDev());
}


class MyAppDev extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MyApp();
  }
}

2. main_android_test.dart

import 'package:flutter/material.dart';
import 'package:supermarie/common/config/app_config.dart';
import 'package:supermarie/my_app.dart';


void main() {
  AppConfig.init(ConfigType.androidTest);
  runApp(MyAppTest());
}


class MyAppTest extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MyApp();
  }
}

3. main_android_online.dart

import 'package:flutter/material.dart';
import 'package:supermarie/common/config/app_config.dart';
import 'package:supermarie/my_app.dart';


void main() {
  AppConfig.init(ConfigType.androidOnline);
  runApp(MyAppOnline());
}


class MyAppOnline extends StatelessWidget {


  @override
  Widget build(BuildContext context) {
    return MyApp();
  }
}

其中 AppConfig.init() 方法为根据不同 Flavor 进行的初始化操作。如: 配置 Host,各种 id,key 等。接着再执行 'runApp()' 方法。而 MyApp() 是走完配置后统一的程序入口,即 App 的首页展示。其结构如图所示:

www.zeeklog.com  - 干货: 在 Flutter 项目下安卓 flavor 打包配置实践 | 开发者说·DTalk

2.3 不同 Flavor 及不同入口的打包

Flavor 和入口文件配置完成后,我们可以尝试打包了,打包方式如下: flutter build apk --[debug/release] --flavor [flavor] -t [entrance] 参数说明: [debug/release]: 指定 debug 或 release,[flavor]: 指定 Flavor,[entrance]: 指定入口文件名称。所以三个Flavor,总共六个包的打包令分别为:

//开发环境:
flutter build apk --debug --flavor flavordev -t lib/main_android_dev.dart
flutter build apk --release --flavor flavordev -t lib/main_android_dev.dart
//测试环境:
flutter build apk --debug --flavor flavortest -t lib/main_android_test.dart
flutter build apk --release --flavor flavortest -t lib/main_android_test.dart
//生产环境:
flutter build apk --debug --flavor flavoronline -t lib/main_android_online.dart
flutter build apk --release --flavor flavoronline -t lib/main_android_online.dart

如果一切正常的情况下,我们就会在项目根目录 /build/app/outputs/app/[flavor]/[debug/release] 下看到生成的 apk 文件了。好,Flutter 项目下 Android 工程的 Flavor 设置及打包已经完成了。下一章将通过源码解析来梳理 Flutter 项目 Android 包的打包流程。

3. Flutter Android 构建过程详解

Flutter 项目的构建入口文件位于: fluttersdk/packages/flutter_tools/bin 下的 flutter_tools.dart 文件,这是所有平台的构建入口,代码如下:

import 'package:flutter_tools/executable.dart' as executable;


void main(List<String> args) {
  executable.main(args);
}

main() 函数里只有一行代码,执行了 executable.dart 的 main() 方法,我们继续看它的实现:

//skip 
//...


  await runner.run(args, () => <FlutterCommand>[
//skip 
//...
    BuildCommand(verboseHelp: verboseHelp),
//skip 
//...
  ], verbose: verbose,
     muteCommandLogging: muteCommandLogging,
     verboseHelp: verboseHelp,
     overrides: <Type, Generator>{
//skip 
//...
     });
}

这段代码比较长,会初始化很多 command 指令,然后再依次执行。但由于我们分析的是 apk 的构建流程,所以我们只看 BuildCommand() 的执行过程。

  BuildCommand({ bool verboseHelp = false }) {
    addSubcommand(BuildAarCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildApkCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildAppBundleCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildAotCommand());
    addSubcommand(BuildIOSCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildIOSFrameworkCommand(
      buildSystem: globals.buildSystem,
      verboseHelp: verboseHelp,
    ));
    addSubcommand(BuildBundleCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildWebCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildMacosCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildLinuxCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildWindowsCommand(verboseHelp: verboseHelp));
    addSubcommand(BuildFuchsiaCommand(verboseHelp: verboseHelp));
  }

BuildCommand() 中添加了很多子 command 指令,用于不同平台或生成文件的构建。由于我们分析的是 apk 的打包,接下来继续看 BuildApkCommand() 的实现,位于 fluttersdk/packages/flutter_tools/lib/src/commands/build_apk.dart:

class BuildApkCommand extends BuildSubCommand {
  BuildApkCommand({bool verboseHelp = false}) {
    addTreeShakeIconsFlag();
    usesTargetOption();
//skip
//...
  }
//skip
//...


  @override
  Future<FlutterCommandResult> runCommand() async {
//skip
//...
    await androidBuilder.buildApk(
      project: FlutterProject.current(),
      target: targetFile,
      androidBuildInfo: androidBuildInfo,
    );
    return FlutterCommandResult.success();
  }
}

我们看到 BuildApkCommand() 的构造方法先进行了一些初始化配置。回到之前 executable.main() 方法,如果我们继续跟踪 runner.run() 方法,执行 command 的实现,实际上就是执行各个 command 以及其子 command 的代码,在执行到 BuildApkCommand() 时,调用了其 runCommand() 方法,如上述代码所示。这个方法的核心实现就是 androidBuilder.buildApk(),我们接着看 androidBuilder.buildApk() 的实现:

  @override
  Future<void> buildApk({
    @required FlutterProject project,
    @required AndroidBuildInfo androidBuildInfo,
    @required String target,
  }) async {
    try {
      await buildGradleApp(
        project: project,
        androidBuildInfo: androidBuildInfo,
        target: target,
        isBuildingBundle: false,
        localGradleErrors: gradleErrors,
      );
    } finally {
      globals.androidSdk?.reinitialize();
    }
  }

其中核心代码 buildGradleApp() 方法的位置在 fluttersdk/packages/flutter_tools/lib/src/android/gradle.dart 下。从文件的命名中即可得知,这个文件跟 Android 的 gradle 文件是密切相关的。我们截取 buildGradleApp() 方法核心部分的实现:

Future<void> buildGradleApp({
  @required FlutterProject project,
  @required AndroidBuildInfo androidBuildInfo,
  @required String target,
  @required bool isBuildingBundle,
  @required List<GradleHandledError> localGradleErrors,
  bool shouldBuildPluginAsAar = false,
  int retries = 1,
}) async {
//skip
//...
  // The default Gradle script reads the version name and number
  // from the local.properties file.
  updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo);
//skip
//...


  final BuildInfo buildInfo = androidBuildInfo.buildInfo;
  final String assembleTask = isBuildingBundle
    ? getBundleTaskFor(buildInfo)
    : getAssembleTaskFor(buildInfo);


  final Status status = globals.logger.startProgress(
    "Running Gradle task '$assembleTask'...",
    timeout: timeoutConfiguration.slowOperation,
    multilineOutput: true,
  );


  final List<String> command = <String>[
    gradleUtils.getExecutable(project),
  ];
//skip
//...
  command.add(assembleTask);
//skip
//...
  final Stopwatch sw = Stopwatch()..start();
  int exitCode = 1;
  try {
    exitCode = await processUtils.stream(
      command,
      workingDirectory: project.android.hostAppGradleRoot.path,
      allowReentrantFlutter: true,
      environment: gradleEnvironment,
      mapFunction: consumeLog,
    );
  } on ProcessException catch (exception) {
    consumeLog(exception.toString());
    // Rethrow the exception if the error isn't handled by any of the
    // `localGradleErrors`.
    if (detectedGradleError == null) {
      rethrow;
    }
  } finally {
    status.stop();
  }
//skip
//...


  // Gradle produced an APK.
  final Iterable<String> apkFilesPaths = project.isModule
    ? findApkFilesModule(project, androidBuildInfo)
    : listApkPaths(androidBuildInfo);
  final Directory apkDirectory = getApkDirectory(project);
  final File apkFile = apkDirectory.childFile(apkFilesPaths.first);
  if (!apkFile.existsSync()) {
    _exitWithExpectedFileNotFound(
      project: project,
      fileExtension: '.apk',
    );
  }


//skip
//...

从代码中我们可得知,在 buildGradleApp() 方法中会读取并修改 local.properties 的属性,local.properties 是根据 Flutter 的 pubspec.yaml 文件等配置生成的本地文件。然后根据 buildInfo 获取 assembleTask,设置当前的编译状态 status,初始化 command 数组,并将 assembleTask 加入其中。接着执行 command 列表,其中一个 command 即调用 gradle 执行 assembleTask。执行完成后,生成 apk 文件,进行构建检查,然后构建结束。既然执行到 gradle 文件了,我们再来看看 Flutter 项目下的 gradle 文件和普通 Android 工程的 gradle 文件有什么不同:

def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader('UTF-8') { reader ->
        localProperties.load(reader)
    }
}


def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}


def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
    flutterVersionCode = '1'
}


def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
    flutterVersionName = '1.0'
}


apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
//skip
//...

这是截取项目中的 gradle 文件,不同之处都在头部。首先是读取 localProperties 的属性,主要判断 flutter sdk 是否存在,以及获取 app 的 versionCode 和 versionName。然后在执行项目其他 gradle 配置之前,先执行 sdk 中的 flutter.gradle 文件。位置在: fluttersdk/packages/flutter_tools/gradle/flutter.gradle。其结构如下:

www.zeeklog.com  - 干货: 在 Flutter 项目下安卓 flavor 打包配置实践 | 开发者说·DTalk

flutter.gradle 这个文件代码很长,但从这张图上,我们可以很清晰的看出结构。主要任务就是执行了 FlutterPlugin,在 FlutterPlugin 中定义了 FlutterTask 这个任务,而 FlutterTask 必然承载了 Flutter 的编译过程。FlutterTask 的核心代码如下:

    @TaskAction
    void build() {
        buildBundle()
    }

buildBundle() 方法就是根据不同的配置,执行了各种 flutter 的编译指令,比如之前示例代码中,我们通过 -t 指定 flutter 层的程序入口。由于这部分已经出离本文的讨论范围,先不做展开了。大家只要知道这部分执行完成后最终生成了 Flutter 层的编译产物即可。经过了原生层和 Flutter 层的构建,整个 Flutter 项目的 Android apk 构建流程就梳理完毕了。构建思路已经非常清晰,但是在打包过程中,我们发现了新的问题,使我们对构建流程的一些细节进行了梳理,下一章将对打包过程中的 bug 和原因进行说明。

4. Flutter SDK 打包 bug 说明

我们回到 Flavor 设置及打包这个章节的开始的 Flavor 配置示例:

productFlavors {
    // 生产环境
    flavoronline {
        applicationId "com.sohu.flavor"
    }
    // 开发环境
    flavordev {
        applicationId "com.sohu.flavor.dev"
    }
    // 测试环境
    flavortest {
        applicationId "com.sohu.flavor.test"
    }
}

在最一开始的配置示例中,我们说明了在 gradle 文件中配置的 Flavor 名称必须是小写的。但实际上一开始我们是用的驼峰命名,即: flavorOnline, flavorDev, flavorTest。这是因为我们开发过程是在 macOS 系统下进行的,打包是在 Ubuntu 系统下通过 jenkins 打包完成的。在开发过程中,我们打各个环境下的包都完全没有问题,但是在 Ubuntu 系统下,会报如下错误:

Gradle build failed to produce an .apk file. It's likely that this file was generated under XXX(目录), but the tool couldn't find it.

通常情况下,这个问题的出现是因为在多渠道打包时,打包命令没有指定相应的入口文件或渠道名称,但实际上我们的打包命令是没有问题的,而且我们在 XXX 目录下找到了 apk 文件,这些 apk 文件在安装运行过程中也都没有任何问题。那么这个问题是怎么出现的呢?先说结论,经过验证,这个是 flutter sdk(1.22.4) 的一个 bug。再说明原因之前,说明一下针对此问题的三种可行解决方案。

  1. 不作任何处理,每次安装 apk 包都通过 adb install 命令去安装。jenkins 上虽然每次都会提示打包失败,但实际上打包文件都在,也都会显示在页面中。
  2. Flavor 名称全小写,也就是我们最终的实现方案。
  3. 修改 flutter sdk 中的 gradle.dart 文件第 488 行为:
final File apkFile = apkDirectory.childFile(apkFilesPaths.first.toLowerCase());

接下来我们分析一下问题产生的原因: 比如我们打一个 debug flavorDev 的包,打包完成后在 XXX 目录下为: app-flavordev-debug.apk。我们回到 gradle.dart 文件,看看打包完成后的执行内容,主要是做一些检查工作:

  // Gradle produced an APK.
  final Iterable<String> apkFilesPaths = project.isModule
    ? findApkFilesModule(project, androidBuildInfo)
    : listApkPaths(androidBuildInfo);
  final Directory apkDirectory = getApkDirectory(project);
  final File apkFile = apkDirectory.childFile(apkFilesPaths.first);
  if (!apkFile.existsSync()) {
    _exitWithExpectedFileNotFound(
      project: project,
      fileExtension: '.apk',
    );
  }
void _exitWithExpectedFileNotFound({
  @required FlutterProject project,
  @required String fileExtension,
}) {
  assert(project != null);
  assert(fileExtension != null);
 
  final String androidGradlePluginVersion =
  getGradleVersionForAndroidPlugin(project.android.hostAppGradleRoot);
  BuildEvent('gradle-expected-file-not-found',
    settings:
    'androidGradlePluginVersion: $androidGradlePluginVersion, '
      'fileExtension: $fileExtension',
    flutterUsage: globals.flutterUsage,
  ).send();
  throwToolExit(
    'Gradle build failed to produce an $fileExtension file. '
    "It's likely that this file was generated under ${project.android.buildDirectory.path}, "
    "but the tool couldn't find it."
  );
}

其中 apkFilesPaths 是需要检查的 apk 路径,通过 apkFilesPaths 拿到 apkFile 文件名称,接着对 apkFile 文件进行检查,如果不存在则报如上所述异常。我们看看 apkFilesPaths 是如何生成的,由于 project.isModule 为 false,所以直接看 listApkPaths() 的实现:

Iterable<String> listApkPaths(
  AndroidBuildInfo androidBuildInfo,
) {
  final String buildType = camelCase(androidBuildInfo.buildInfo.modeName);
  final List<String> apkPartialName = <String>[
    if (androidBuildInfo.buildInfo.flavor?.isNotEmpty ?? false)
      androidBuildInfo.buildInfo.flavor,
    '$buildType.apk',
  ];
  if (androidBuildInfo.splitPerAbi) {
    return <String>[
      for (AndroidArch androidArch in androidBuildInfo.targetArchs)
        <String>[
          'app',
          getNameForAndroidArch(androidArch),
          ...apkPartialName
        ].join('-')
    ];
  }
  return <String>[
    <String>[
      'app',
      ...apkPartialName,
    ].join('-')
  ];
}

这段代码说明检查的 apk 的命名方式为: 'app-' + androidBuildInfo.buildInfo.flavor + '$buildType.apk'。所以 apkFilesPaths.first 的结果如下:

app-flavorDev-debug.apk

回过头来看 XXX 目录下的 apk 文件: app-flavordev-debug.apk,发现差了一个大小写。所以猜测是大小写问题导致的。我们修改 gradle.dart 的以下代码:

final File apkFile = apkDirectory.childFile(apkFilesPaths.first);

为:

final File apkFile = apkDirectory.childFile(apkFilesPaths.first.toLowerCase());

即检查的 apkFile 的文件名称强制转成小写,发现 build 成功不会再报错。那为什么在 macOS 就不会报错呢?大概是因为不同 os 在针对检查文件一致性的方式不同。Ubuntu 系统下会针对文件名称大小写进行严格检查。

注意: 为了让 Flutter sdk 修改的内容生效,修改代码后需删除 fluttersdk/flutter/bin/cache 路径下的 flutter_tools.snapshot 和 flutter_tools.stamp 重新编译 sdk。

另外,我们知道在 gradle 文件下可以指定输出的文件名称,这个是没有问题的。但是 flutter sdk 中的 flutter.gradle(fluttersdk/packages/flutter_tools/gradle/flutter.gradle) 依旧会在 XXX 目录下输出相应的小写文件,此处可看 flutter.gradle 的相应实现:

variant.outputs.all { output ->
    // `assemble` became `assembleProvider` in AGP 3.3.0.
    def assembleTask = variant.hasProperty("assembleProvider")
        ? variant.assembleProvider.get()
        : variant.assemble
    assembleTask.doLast {
//skip
//...
        if (variant.flavorName != null && !variant.flavorName.isEmpty()) {
            filename += "-${variant.flavorName.toLowerCase()}"
        }
        filename += "-${buildModeFor(variant.buildType)}"
        project.copy {
            from new File("$outputDirectoryStr/${output.outputFileName}")
            into new File("${project.buildDir}/outputs/flutter-apk");
            rename {
                return "${filename}.apk"
            }
        }
    }
}

所以为了保证打包成功,在不修改 sdk 源码的情况下,我们需将 Flavor 设置成小写即可绕过这个 bug。最后说明一下,目前在 Flutter sdk 的 master 分支上,此 bug 已修复。在 gradle.dart 文件中,会检查小写文件,listApkPaths() 的实现如下:

Iterable<String> listApkPaths(
  AndroidBuildInfo androidBuildInfo,
) {
  final String buildType = camelCase(androidBuildInfo.buildInfo.modeName);
  final List<String> apkPartialName = <String>[
    if (androidBuildInfo.buildInfo.flavor?.isNotEmpty ?? false)
      androidBuildInfo.buildInfo.lowerCasedFlavor,
    '$buildType.apk',
  ];
//skip
//...
}

可以看到 apkPartialName 返回的第 1 位是: androidBuildInfo.buildInfo.lowerCasedFlavor,在取 Flavor 的时候,直接取小写属性。所以大家也可以耐心等待,新的稳定版发布后,这个 bug 应该就 fix 了。

5. 总结

经过实践,最终我们的 Flutter 项目 Android 端,使用以上方式进行了 Flavor 配置与属性读取。并且本文通过源码解析梳理了整个构建流程,希望能够帮助大家更好的理解 Flutter 是如何进行 apk 构建的。另外在实践过程中,发现了 sdk 在打包过程中的一个 bug,且给出了解决方案可以在不修改源码的情况下绕过此问题。

6. 参考文献

  • https://www.jianshu.com/p/b9e7c00075e1from=timeline&isappinstalled=0

长按右侧二维码

查看更多开发者精彩分享

www.zeeklog.com  - 干货: 在 Flutter 项目下安卓 flavor 打包配置实践 | 开发者说·DTalk

"开发者说·DTalk" 面向

www.zeeklog.com  - 干货: 在 Flutter 项目下安卓 flavor 打包配置实践 | 开发者说·DTalk

中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。

www.zeeklog.com  - 干货: 在 Flutter 项目下安卓 flavor 打包配置实践 | 开发者说·DTalk

点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk"


www.zeeklog.com  - 干货: 在 Flutter 项目下安卓 flavor 打包配置实践 | 开发者说·DTalk
www.zeeklog.com  - 干货: 在 Flutter 项目下安卓 flavor 打包配置实践 | 开发者说·DTalk
www.zeeklog.com  - 干货: 在 Flutter 项目下安卓 flavor 打包配置实践 | 开发者说·DTalk

Read more

60个“特征工程”计算函数(Python代码)

60个“特征工程”计算函数(Python代码)

转自:coggle数据科学 近期一些朋友询问我关于如何做特征工程的问题,有没有什么适合初学者的有效操作。 特征工程的问题往往需要具体问题具体分析,当然也有一些暴力的策略,可以在竞赛初赛前期可以带来较大提升,而很多竞赛往往依赖这些信息就可以拿到非常好的效果,剩余的则需要结合业务逻辑以及很多其他的技巧,此处我们将平时用得最多的聚合操作罗列在下方。 最近刚好看到一篇文章汇总了非常多的聚合函数,就摘录在下方,供许多初入竞赛的朋友参考。 聚合特征汇总 pandas自带的聚合函数 * 其它重要聚合函数 其它重要聚合函数&分类分别如下。 def median(x):     return np.median(x) def variation_coefficient(x):     mean = np.mean(x)     if mean != 0:         return np.std(x) / mean     else:         return np.nan def variance(x):     return

By Ne0inhk
90w,确实可以封神了!

90w,确实可以封神了!

要说24年一定最热的技术,还得是AIGC! 前段时间阿里旗下的开源项目,登上GitHub热榜! AI大热,如今ChatGPT的优异表现,必然会出现各种细分场景应用的工具软件,和大量岗位项目! 山雨欲来风满楼,强人工智能的出现,所有科技公司已经开始巨量扩招此领域的人才。算法的岗位,近三个月已经增长68%!这件事在HR届也是相当震撼的。 目前各行各业都不景气的市场,人工智能岗位却一直保持常青!甚至同属AI边缘岗都比其他岗薪资高40%! 与此同时,AI算法岗上岸也不简单,竞争激烈,好公司核心岗位不用说,谁都想去。 所以事实就是,想要上岸,门槛也逐渐变高,项目经历、实习经历都很重要,越早明白这个道理就越能提前建立起自己的优势。 但我在b站逛知识区的时候,经常看到有些同学,因为一些客观原因导致无法参加实习,这种情况下,如果你想提升背景,增加项目经历的话,可以试试这个《CV/NLP 算法工程师培养计划》。 目前已经有上千位同学通过该计划拿到offer了,最新一期学员就业薪资最高能拿到78K!年薪94w! 优势就是有BAT大厂讲师带领,手把手带做AI真实企业项目(包含CV、NLP等

By Ne0inhk
再见nohup!试试这个神器,Python Supervisor!

再见nohup!试试这个神器,Python Supervisor!

👇我的小册 45章教程:() ,原价299,限时特价2杯咖啡,满100人涨10元。 作者丨Ais137 https://juejin.cn/post/7354406980784373798 1. 概述 Supervisor 是一个 C/S 架构的进程监控与管理工具,本文主要介绍其基本用法和部分高级特性,用于解决部署持久化进程的稳定性问题。 2. 问题场景 在实际的工作中,往往会有部署持久化进程的需求,比如接口服务进程,又或者是消费者进程等。这类进程通常是作为后台进程持久化运行的。 一般的部署方法是通过 nohup cmd & 命令来部署。但是这种方式有个弊端是在某些情况下无法保证目标进程的稳定性运行,有的时候 nohup 运行的后台任务会因为未知原因中断,从而导致服务或者消费中断,进而影响项目的正常运行。 为了解决上述问题,通过引入 Supervisor 来部署持久化进程,提高系统运行的稳定性。 3. Supervisor 简介 Supervisor is a client/

By Ne0inhk
第一本给程序员看的AI Agent图书上市了!

第一本给程序员看的AI Agent图书上市了!

AI Agent火爆到什么程度? OpenAI创始人奥特曼预测,未来各行各业,每一个人都可以拥有一个AI Agent;比尔·盖茨在2023年层预言:AI Agent将彻底改变人机交互方式,并颠覆整个软件行业;吴恩达教授在AI Ascent 2024演讲中高赞:AI Agent是一个令人兴奋的趋势,所有从事AI开发的人都应该关注。而国内的各科技巨头也纷纷布局AI Agent平台,如:钉钉的AI PaaS、百度智能云千帆大模型平台等等。 Agent 是未来最重要的智能化工具。对于程序员来说,是时候将目光转向大模型的应用开发了,率先抢占AI的下一个风口AI Agent。 小异带来一本新书《大模型应用开发 动手做 AI Agent》,这本书由《GPT图解》的作者黄佳老师创作,从0到1手把手教你做AI Agent。现在下单享受5折特惠! ▼点击下方,即可5折起购书 有这样一本秘籍在手,程序员们这下放心了吧,让我们先来揭开 Agent 的神秘面纱。 AI Agent 面面观

By Ne0inhk