日常应用开发遇到的小问题二三则

前言

这两天的工作又相对杂乱一些,处理一下A事情,又要搞一搞B事情,需要盯着C事情,还要尝试一下D事情,所以这篇总结没有什么主线,主要目的是记录一下最近解决的问题,先不展开讨论,当类似的问题积累一些再展开描述,我就先记录一下流水账了。

Redis问题

本来是一个常规清理数据,Redis回收内存碎片的操作,但是因redis-server版本问题被迫切换解决方案

启用碎片自动回收失败

1
2
3
127.0.0.1:6379> config set activedefrag yes
(error) ERR Active defragmentation cannot be enabled: it requires a Redis server compiled with
a modified Jemalloc like the one shipped by default with the Redis source distribution

开启主动碎片整理机制失败,提示错误大概意思是说,当前的 Redis 服务器未使用支持主动碎片整理(Active Defragmentation)的 Jemalloc 分配器,因为Redis 的主动碎片整理功能依赖于 Jemalloc 分配器,如果 Redis 是使用其他分配器(如 libc malloc)编译的,或者当前的 Jemalloc 缺乏必要的功能,就会导致该功能无法启用。

可以通过Redis命令 redis-cli INFO memory | grep allocator 来查询,如果显示 mem_allocator:jemalloc,则表示启用了 Jemalloc。如果显示 mem_allocator:libc,则说明当前未使用 Jemalloc,需要重新安装或编译 Redis。

我运行一看当然是 mem_allocator:libc 了,看来没办法进行碎片整理了,幸好是个slave节点,所以我干脆保存数据后重启一下吧。

启动Redis未脱离终端

首先关闭redis服务

1
shutdown SAVE

这个过程会进行同步存储,所以会给你一种卡死了的状态,因为我这里有90G数据,存储大概用了10多分钟,Redis服务成功关闭

然后按配置文件启动Redis

1
/usr/local/bin/redis-server ./redis.conf

启动完终端就停在这个了,难道我起了个前台程序?我记得之前都是这么启动的啊,总不能为了脱离终端我还要使用 nohup& 来配合吧,有点low啊,主要是日志是不是就放到nohup.out文件里了,找找有没有配置吧,打开redis.conf文件发现daemonize no,我的天啊,这个redis-server为什么这么与众不同,守护进程模式居然是关着的,虽然说把配置文件改成 daemonize yes 再启动就好了,但是我好奇的是之前是怎么启动的。

我决定不修改配置文件了,保持原来的样子,直接在启动时指定 --daemonize yes 好了,这样可以达到目的,在Linux系统中使用Redis时,命令行参数和配置文件参数(也称为Redis配置)具有不同的优先级。如果同一个配置选项在配置文件和命令行参数中被设置,那么命令行参数将覆盖配置文件中的设置。

Vercel问题

之前简单聊过将Nextjs框架编写的网站部署到Vecel有天然的适应性,因为Vercel背后的团队就是开发出Nextjs框架的那群人,部署到Vercel以后绑定一个域名就可以使用了,不需要自己安装运行环境,不需要配置DN缓存,不需要配置SSL证书,真是方便极了,但免费的账户是有资源限制的,特别是要团队开发的话需要购买付费版本。

未在Vecel团队的人提交无法触发自动部署

用过Vercel就会发现,当通过github导入项目之后,以后每次推送到github后都能自动触发Vercel的网站发布功能,但是未添加到Vercel团队的账号提交时无法触发自动部署,提示

Vercel - No GitHub account was found matching the commit author email address

一种办法就是把所有可能提交的账号都添加到Vercel团队,但是这是要花钱的,每添加一个成员每月$20,还有就是找已经在团队的成员再提交一次,触发自动部署就行了,这个成本也很低,也就是仓库记录不那么干净了而已

1
git commit --allow-empty -m"trigger redeployment"

更新package.json后部署Vercel时报错

因为我使用 npm install 初始化完项目后,启动时有两个警告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Module not found: Can't resolve 'bufferutil' in 'E:\WorkSpace\nodeweb\qxweb\node_modules\ws\lib'

Import trace for requested module:
./node_modules/ws/lib/buffer-util.js
./node_modules/ws/lib/websocket.js
./node_modules/ws/index.js
./node_modules/@supabase/realtime-js/dist/main/RealtimeClient.js
./node_modules/@supabase/realtime-js/dist/main/index.js
./node_modules/@supabase/supabase-js/dist/main/index.js
./store/SupabaseStore.tsx

./node_modules/ws/lib/validation.js
Module not found: Can't resolve 'utf-8-validate' in 'E:\WorkSpace\nodeweb\qxweb\node_modules\ws\lib'

Import trace for requested module:
./node_modules/ws/lib/validation.js
./node_modules/ws/lib/websocket.js
./node_modules/ws/index.js
./node_modules/@supabase/realtime-js/dist/main/RealtimeClient.js
./node_modules/@supabase/realtime-js/dist/main/index.js
./node_modules/@supabase/supabase-js/dist/main/index.js
./store/SupabaseStore.tsx

所以我就手动安装了这两个库 npm isntall bufferutil utf-8-validate,这就导致我的 package.json 文件更新内容里增加了这两个库的引用版本,提交代码部署Vercel时报错

1
2
3
4
5
6
7
8
9
10
11
12
13
Running build in Washington, D.C., USA (East) – iad1
Cloning github.com/2338-AI/quantum-solutions-web (Branch: main, Commit: 96249d1)
Previous build cache not available
Cloning completed: 2.607s
Running "vercel build"
Vercel CLI 39.1.1
Detected `pnpm-lock.yaml` version 9 generated by pnpm@9.x
Installing dependencies...
 ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with "frozen-lockfile" because pnpm-lock.yaml is not up to date with <ROOT>/package.json
Note that in CI environments this setting is true by default. If you still need to run install in such cases, use "pnpm install --no-frozen-lockfile"
Failure reason:
specifiers in the lockfile ({"@douyinfe/semi-ui":"^2.44.0","@react-spring/web":"^9.7.5","@supabase/supabase-js":"^2.38.1","accept-language":"^3.0.18","classes-names":"^1.0.0","emailjs-com":"^3.2.0","i18next":"^23.5.1","i18next-browser-languagedetector":"^7.1.0","i18next-resources-to-backend":"^1.1.4","is-mobile":"^5.0.0","next":"13.5.4","react":"^18","react-dom":"^18","react-google-recaptcha":"^3.1.0","react-i18next":"^13.3.0","swiper":"^11.1.14","tailwind-merge":"^2.5.4","@types/node":"^20","@types/react":"^18","@types/react-dom":"^18","@types/react-google-recaptcha":"^2.1.7","@typescript-eslint/eslint-plugin":"^6.3.0","@typescript-eslint/parser":"^6.3.0","autoprefixer":"^10","eslint":"^8","eslint-config-airbnb":"^19.0.4","eslint-config-airbnb-typescript":"^17.1.0","eslint-config-next":"13.5.4","postcss":"^8","postcss-pxtorem":"^6.1.0","prettier":"^3.3.3","prettier-plugin-tailwindcss":"^0.6.8","tailwindcss":"^3","typescript":"^5"}) don't match specs in package.json ({"@types/node":"^20","@types/react":"^18","@types/react-dom":"^18","@types/react-google-recaptcha":"^2.1.7","@typescript-eslint/eslint-plugin":"^6.3.0","@typescript-eslint/parser":"^6.3.0","autoprefixer":"^10","eslint":"^8","eslint-config-airbnb":"^19.0.4","eslint-config-airbnb-typescript":"^17.1.0","eslint-config-next":"13.5.4","postcss":"^8","postcss-pxtorem":"^6.1.0","prettier":"^3.3.3","prettier-plugin-tailwindcss":"^0.6.8","tailwindcss":"^3","typescript":"^5","@douyinfe/semi-ui":"^2.44.0","@react-spring/web":"^9.7.5","@supabase/supabase-js":"^2.38.1","accept-language":"^3.0.18","bufferutil":"^4.0.8","classes-names":"^1.0.0","emailjs-com":"^3.2.0","i18next":"^23.5.1","i18next-browser-languagedetector":"^7.1.0","i18next-resources-to-backend":"^1.1.4","is-mobile":"^5.0.0","next":"13.5.4","react":"^18","react-dom":"^18","react-google-recaptcha":"^3.1.0","react-i18next":"^13.3.0","swiper":"^11.1.14","tailwind-merge":"^2.5.4","utf-8-validate":"^6.0.5"})
Error: Command "pnpm install" exited with 1

这个错误提示说明 Vercel 在部署过程中使用 pnpm 安装依赖时,遇到了 pnpm-lock.yamlpackage.json 文件中的依赖不匹配问题。具体来说,pnpm-lock.yaml 文件中的依赖版本与 package.json 中声明的版本范围不一致,导致 pnpm 无法继续安装。

看到这时我才意识到我的项目里有个 pnpm-lock.yaml,而我使用的 npm 安装,本地有个 package-lock.json 文件,这样一看是我工具用错了呀,我说这个项目怎么没提交 package-lock.json 文件呢

清理模块重新安装吧

1
2
rm -rf .node_modules package-lock.json
pnpm install

输出如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
PS E:\WorkSpace\nodeweb\qxweb> pnpm install

╭──────────────────────────────────────────────────────────────────╮
│ │
│ Update available! 9.5.0 → 9.14.2. │
│ Changelog: https://github.com/pnpm/pnpm/releases/tag/v9.14.2 │
│ Run "pnpm add -g pnpm" to update. │
│ │
│ Follow @pnpmjs for updates: https://x.com/pnpmjs │
│ │
╰──────────────────────────────────────────────────────────────────╯

Lockfile is up to date, resolution step is skipped
Packages: +423
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 WARN  GET https://registry.npmmirror.com/next/-/next-13.5.4.tgz error (ECONNRESET). Will retry in 10 seconds. 2 retries left.
Downloading @next/swc-win32-x64-msvc@13.5.4: 36.94 MB/36.94 MB, done
Downloading next@13.5.4: 16.80 MB/16.80 MB, done
Downloading typescript@5.2.2: 7.23 MB/7.23 MB, done
Progress: resolved 423, reused 184, downloaded 239, added 423, done
node_modules/.pnpm/es5-ext@0.10.62/node_modules/es5-ext: Running postinstall script, done in 43.1s
node_modules/.pnpm/utf-8-validate@5.0.10/node_modules/utf-8-validate: Running install script, done in 52.3s
node_modules/.pnpm/bufferutil@4.0.8/node_modules/bufferutil: Running install script, done in 52.3s

dependencies:
+ @douyinfe/semi-ui 2.44.0
+ @react-spring/web 9.7.5
+ @supabase/supabase-js 2.38.1
+ accept-language 3.0.18
+ classes-names 1.0.0
+ emailjs-com 3.2.0
+ i18next 23.5.1
+ i18next-browser-languagedetector 7.1.0
+ i18next-resources-to-backend 1.1.4
+ is-mobile 5.0.0
+ next 13.5.4
+ react 18.2.0
+ react-dom 18.2.0
+ react-google-recaptcha 3.1.0
+ react-i18next 13.3.0
+ swiper 11.1.14
+ tailwind-merge 2.5.4

devDependencies:
+ @types/node 20.8.6
+ @types/react 18.2.28
+ @types/react-dom 18.2.13
+ @types/react-google-recaptcha 2.1.7
+ postcss-pxtorem 6.1.0
+ prettier 3.3.3
+ prettier-plugin-tailwindcss 0.6.8
+ tailwindcss 3.3.3
+ typescript 5.2.2

Done in 21m 2.3s

这次再启动项目 pnpm run dev 就不报缺失模块的警告了

简单说下 npmpnpm 的关系,npm 是 Node.js 的默认包管理工具,用于安装、管理和共享 JavaScript 包。安装 Node.js 时会自带 npm,不需要额外安装。pnpm 是一种兼容 npm 和 yarn 的包管理工具。它与 npm 类似,也用来管理 JavaScript 包,但通过优化磁盘使用和依赖解析性能解决了 npm 的一些问题。

再简单说下 package.jsonpackage-lock.jsonpnpm-lock.yaml 的关系

  • package.json 描述项目的基本信息(如项目名称、版本号),声明项目的依赖项、脚本命令和配置,是项目中最重要的依赖描述文件,是开发者直接编辑的文件,但不包含依赖的具体版本信息
  • package-lock.json 是 npm 包管理工具生成的锁定文件,记录依赖的具体版本号和结构,确保不同环境中安装的依赖版本一致。不需要手动编辑,由 npm 自动生成和管理。
  • pnpm-lock.yaml 是 pnpm 包管理工具生成的锁定文件,用于精确记录依赖的版本号和安装结构。确保团队或 CI/CD 环境在安装依赖时,所有人使用的依赖版本完全一致。不需要手动编辑,由 pnpm 自动生成和管理。

可以看到package-lock.jsonpnpm-lock.yaml是互斥的,平时 npmpnpm 选择一个就行,别混着用。

Android问题

Apple推送选择APNs就好了,而Android推送一直就是老大难,因为FCM对谷歌框架的依赖,国内基本是不可用的,所以在之前的蛮荒年代,真是八仙过海各显其能,相互拉起、定时检测等等搞得手机卡顿的不行,后来各家厂商基本都实现了自己的通道,比如华为、小米、OPPO、Vivo等等,所以找个聚合的SDK也是能办到的,但是国外还是主要依赖FCM。

但随着Android版本的提升,推送的要求越来越严,一旦应用被强制杀死,FCM消息虽然能达到手机,但是不允许拉起应用,导致消息无法触及到用户,这也是目前国外环境要实现推送所面临的难点。

要想能收到FCM的推送消息,就得保证应用不被杀死,可能得方向有提高优先级、定时检测重启、杀死后立即重启、采用其他的推送通道等等,但是效果都不理想,之前好用的方式升级版本或者切换一个手机厂商就不太好使了,没有什么通用的解决办法。

后来发现,真对国内手机只要给应用开启自启动权限,那么即使被强杀后也能收到FCM通知,或许这是一个可以努力的方向,但这个权限不是通用的逻辑,每个手机厂商都有自己的白名单,大厂APP出生就在白名单里,而我们自己开发的APP需要经过复杂的引导操作才能加入其中,虽然很难,但终归是第一条可行的路。

主动请求通知权限

在 Android 13 (API 33) 及以上版本,应用需要 主动请求通知权限,以便能够发送通知。对于之前的版本,应用只需要在 AndroidManifest.xml 中声明 POST_NOTIFICATIONS 权限即可。然而,从 Android 13 开始,仅在声明权限的基础上,还需要在运行时申请此权限。

步骤1:修改 AndroidManifest.xml

首先,在 AndroidManifest.xml 文件中声明权限:

1
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

步骤2:在应用中请求通知权限

从 Android 13 开始,你需要在运行时请求通知权限。你可以使用 NotificationManagerCompat 来检查和请求权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import android.os.Build;
import android.widget.Toast;
import androidx.core.app.NotificationManagerCompat;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// 检查 Android 13 及以上版本是否需要请求通知权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(this);
// 检查通知权限是否已被授权
if (!notificationManagerCompat.areNotificationsEnabled()) {
// 如果没有授权,可以弹出提示或者引导用户
requestNotificationPermission();
} else {
// 如果已授权,可以继续执行相关逻辑
Toast.makeText(this, "通知权限已授权", Toast.LENGTH_SHORT).show();
}
}
}

private void requestNotificationPermission() {
// 你可以在这里引导用户到设置页面手动授权
Toast.makeText(this, "请在设置中授权通知权限", Toast.LENGTH_LONG).show();
}
}

步骤3:引导用户授权

如果用户没有授权通知权限,你通常需要提供一个方法引导他们到设置页面,在该页面中手动授权。因为从 Android 13 开始,权限只能通过系统设置进行手动授权,而不是通过应用内弹窗。

1
2
3
4
5
private void goToSettings() {
Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
startActivity(intent);
}

网络状态变化的监听不能使用静态注册

Android 7.0 (API 24) 开始,网络状态变化的监听不能使用静态注册 (<receiver> 标签在 AndroidManifest.xml 中注册),必须通过动态注册来实现。这是出于优化电池和性能的考虑,Android 不再允许应用通过静态注册的方式来监听系统广播(如网络变化、屏幕开关等),尤其是对敏感的系统事件。

静态注册 (AndroidManifest.xml)

静态注册会在 AndroidManifest.xml 中声明广播接收器,并且会在整个应用运行期间自动接收系统广播。对于网络状态变化(如 CONNECTIVITY_ACTION)的静态注册从 Android 7.0 开始被限制。

AndroidManifest.xml 中的静态注册示例(在 Android 7.0 之前是有效的):

1
2
3
4
5
<receiver android:name=".NetworkChangeReceiver">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
</intent-filter>
</receiver>

这种方式在 Android 7.0 及以后会被限制,无法接收到网络状态变化的广播。

动态注册 (代码中注册)

动态注册意味着在应用运行时,通过 Context.registerReceiver() 来注册接收器,这样可以选择性地在需要时注册,并在不需要时取消注册。这样做的好处是,可以更灵活地控制接收器的生命周期,避免不必要的电池消耗。

动态注册的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class MainActivity extends AppCompatActivity {
private NetworkReceiver networkReceiver;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

// 初始化网络广播接收器
networkReceiver = new NetworkReceiver();
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);

// 动态注册接收器
registerReceiver(networkReceiver, filter);
}

@Override
protected void onDestroy() {
super.onDestroy();
// 注销接收器
unregisterReceiver(networkReceiver);
}
}

public class NetworkReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 获取网络连接状态
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();

if (activeNetwork != null && activeNetwork.isConnected()) {
Log.d("NetworkReceiver", "网络已连接");
} else {
Log.d("NetworkReceiver", "网络未连接");
}
}
}

在这个示例中,使用 registerReceiver() 动态注册了一个监听网络状态变化的广播接收器 NetworkReceiver,并在 onCreate() 中进行注册,在 onDestroy() 中进行注销,以避免内存泄漏。

各种Service介绍和对比

在 Android 中,Service 是一种后台组件,用于在应用中执行长时间运行的任务。根据服务的用途和生命周期,Android 提供了不同类型的服务。以下是 Android 中常见的 Service 类型和 BroadcastReceiver 的介绍及对比:

普通 Service (Normal Service) 没有前台 UI 组件。通常用于执行后台任务,不与用户交互,可以在应用的任何地方启动,并在后台运行,直到它完成工作或被显式停止,生命周期为 onCreate()onStartCommand()onDestroy(),适用执行短时间的后台任务,如数据同步、文件下载等场景。

前台服务 (Foreground Service) 在运行时必须显示一个持续的通知(通知栏),因此它会在用户和系统资源管理中被认为是一个“重要”的服务,不容易被系统杀死。生命周期为 onCreate()onStartCommand()onDestroy() (服务必须调用 startForeground() 来显示通知),适用执行长期任务,如播放音乐、导航、实时更新等场景。

1
2
3
4
5
6
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("My Foreground Service")
.setContentText("Service is running in the foreground")
.setSmallIcon(R.drawable.ic_notification)
.build();
startForeground(1, notification);

JobService 是一种特殊类型的服务,它用于执行计划任务(即 JobScheduler),在特定条件下(如网络连接、充电等)启动。JobService 适合用于执行延迟任务或条件任务,如周期性数据同步、定时上传等,与 JobScheduler 配合使用,可以在设备的空闲时间或在设备满足某些条件时执行任务。生命周期为 onCreate()onStartJob()onStopJob(),适用执行定期任务,延迟任务,或者需要满足某些条件(如网络连接、充电等)的任务场景

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyJobService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
// 执行任务
return true; // 返回 true 表示任务正在进行中
}

@Override
public boolean onStopJob(JobParameters params) {
// 停止任务
return true; // 返回 true 表示任务被取消
}
}

BroadcastReceiver 是 Android 中用于接收广播的组件。它通过监听系统广播或自定义广播来响应事件(如网络状态变化、电池电量变化等)。BroadcastReceiver 不会启动一个独立的线程,它只会在接收到广播时执行相应的代码。onReceive() 方法在接收到广播时调用。适用监听和响应系统广播,如网络连接变化、设备开关机、短信接收、系统更新等场景

1
2
3
4
5
6
7
8
public class NetworkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
// 处理网络变化
}
}
}
服务类型 生命周期 适用场景 优缺点
普通 Service onCreate()onStartCommand()onDestroy() 短时间的后台任务 简单易用,但容易被系统杀死,适用于短时间的任务
前台 ForegroundService onCreate()onStartCommand()onDestroy() 长时间运行的任务,如播放音乐、导航 不容易被杀死,但需要显示通知,可能影响用户体验
JobService onCreate()onStartJob()onStopJob() 执行定期任务、延迟任务,或者需要满足某些条件的任务 适用于需要在特定条件下执行的任务,延迟执行,但不能精确控制执行时机
BroadcastReceiver onReceive() 响应广播事件,如网络状态变化、电池状态变化等 适合处理广播事件,不能执行长时间操作,执行时间受限制

总结

  • Redis如果未使用Jemalloc无法开启主动的碎片回收,通过 redis-cli INFO memory | grep allocator 可查询内存分配器
  • 启动Redis的方法 /usr/local/bin/redis-server ./redis.conf --daemonize yes
  • 管理node模块时可以选择 nmp 或者 pnmp,不要混用,总的来说后者更优秀一点
  • 管理Nextjs项目时最好吧 package.jsonpackage-lock.json(pnpm-lock.yaml)都上传,便于安装出相同的运行环境
  • 触发Vercel重新部署的命令 git commit --allow-empty -m"trigger redeployment"
  • Android的服务有很多种,普通服务Service、前台服务ForegroundService,定时服务JobService等等

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

在自己的世界里独善其身,在别人的世界里顺其自然~

2024-11-29 15:50:46

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客