Android 音乐通知栏

  • 前言
  • 正文
    • ① 通知栏按钮点击监听
    • ② 通知栏点击监听
    • ③ 通知栏业务处理
    • ④ 运行效果图
  • 结语

前言

  这篇文章的标题有些言简意赅了,也突出了这篇文章的核心,那就是通知栏的操作,你可以看到市面上的音乐类APP都会有这个操作,通过音乐通知栏可以播放暂停、上一曲、下一曲、收藏、显示歌词等等。当然我这个Demo目前不考虑这么多,先实现播放暂停、上一曲、下一曲这些基本功能再说,你说对吧。

正文

  在第四篇文章的到最后显示了通知栏,那么为什么我要把通知的的操作单独放到一篇文章来进行讲解呢?因为里面有很多业务逻辑,还有通信的关系,所以才这么做的。

① 通知栏按钮点击监听

  要实现具体的业务功能,首先要监听到点击事件,这一点是毋庸置疑的,谁赞成,谁反对。首先增加几个全局变量,打开Constant

 /*** 歌曲播放*/public static final String PLAY = "play";/*** 歌曲暂停*/public static final String PAUSE = "pause";/*** 上一曲*/public static final String PREV = "prev";/*** 下一曲*/public static final String NEXT = "next";/*** 关闭通知栏*/public static final String CLOSE = "close";/*** 进度变化*/public static final String PROGRESS = "progress";

这些都是用来表明当前歌曲的状态的,至关重要。之前我通过RemoteViews来指定一个布局文件,从而实现自定义通知栏样式的效果,那么对于通知栏页面的按钮的点击事件,也是交给RemoteViews来完成来的,下面进行实例化,把它变成成员变量。

在Service中实例化

private static RemoteViews remoteViews;

然后单独写一个方法对RemoteViews进行初始化配置。

 /*** 初始化自定义通知栏 的按钮点击事件*/private void initRemoteViews() {remoteViews = new RemoteViews(this.getPackageName(), R.layout.notification);//通知栏控制器上一首按钮广播操作Intent intentPrev = new Intent(PREV);PendingIntent prevPendingIntent = PendingIntent.getBroadcast(this, 0, intentPrev, 0);//为prev控件注册事件remoteViews.setOnClickPendingIntent(R.id.btn_notification_previous, prevPendingIntent);//通知栏控制器播放暂停按钮广播操作  //用于接收广播时过滤意图信息Intent intentPlay = new Intent(PLAY);PendingIntent playPendingIntent = PendingIntent.getBroadcast(this, 0, intentPlay, 0);//为play控件注册事件remoteViews.setOnClickPendingIntent(R.id.btn_notification_play, playPendingIntent);//通知栏控制器下一首按钮广播操作Intent intentNext = new Intent(NEXT);PendingIntent nextPendingIntent = PendingIntent.getBroadcast(this, 0, intentNext, 0);//为next控件注册事件remoteViews.setOnClickPendingIntent(R.id.btn_notification_next, nextPendingIntent);//通知栏控制器关闭按钮广播操作Intent intentClose = new Intent(CLOSE);PendingIntent closePendingIntent = PendingIntent.getBroadcast(this, 0, intentClose, 0);//为close控件注册事件remoteViews.setOnClickPendingIntent(R.id.btn_notification_close, closePendingIntent);}

目前通知栏上看到的按钮只有四个,因为播放和暂停是一个按钮,到时候可以根据MediaPlayer的播放状态做进一步的处理,上面四个按钮,点击之后会发送一个广播,既然有广播,那自然要有一个广播接收器,就好比,你到淘宝上买衣服,别人给你发货了,你总要设置一个收货地址吧。这是一个道理的。至于广播接收器,可以写在Service里面,作为一个内部类使用。那么先创建这个内部类。

 /*** 广播接收器 (内部类)*/public class MusicReceiver extends BroadcastReceiver {public static final String TAG = "MusicReceiver";@Overridepublic void onReceive(Context context, Intent intent) {//UI控制UIControl(intent.getAction(), TAG);}}

然后来看看UIControl方法。

 /*** 页面的UI 控制 ,通过服务来控制页面和通知栏的UI** @param state 状态码* @param tag*/private void UIControl(String state, String tag) {switch (state) {case PLAY:BLog.d(tag,PLAY+" or "+PAUSE);break;case PREV:BLog.d(tag,PREV);break;case NEXT:BLog.d(tag,NEXT);break;case CLOSE:BLog.d(tag,CLOSE);break;default:break;}}

对应四个通知栏的按钮,这是是作为广播的接收。但是要实际收到,还要注册才行。
所以要注册动态广播。

 /*** 注册动态广播*/private void registerMusicReceiver() {musicReceiver = new MusicReceiver();IntentFilter intentFilter = new IntentFilter();intentFilter.addAction(PLAY);intentFilter.addAction(PREV);intentFilter.addAction(NEXT);intentFilter.addAction(CLOSE);registerReceiver(musicReceiver, intentFilter);}

在这里你可以发现,我对四个值进行了拦截过滤,也就是说当我点击通知栏的上一曲按钮时,会发送动作名为PREV的广播,而这个时候MusicReceiver拦截到PREV的广播,传递给onReceive。然后在onReceive对不同的动作做不同的处理,目前我只是打印了日志而已。
现在你可以将showNotification方法中的如下代码删除掉。

RemoteViews remoteViews = new RemoteViews(this.getPackageName(), R.layout.notification);

然后在Service中的onCreate中调用。

 @Overridepublic void onCreate() {super.onCreate();initRemoteViews();//注册动态广播registerMusicReceiver();showNotification();BLog.d(TAG, "onCreate");}

initRemoteViews 方法一定要在 showNotification之前调用,否则你就等着null Object 然后APP崩溃吧。
在服务销毁的时候要解绑广播接收者

 @Overridepublic void onDestroy() {super.onDestroy();if (musicReceiver != null) {//解除动态注册的广播unregisterReceiver(musicReceiver);}}

下面运行一下,日志如下:

现在通知栏的按钮点击事件就已经监听到了,下面做通知栏的点击事件。

② 通知栏点击监听

  只要是通知栏按钮以外的点击都属于通知栏的点击,这个要区分开,别搞混了。在写代码要想清楚一点,当我们点击通知栏的时候,要进入那个页面,我仔细观察过其他音乐APP的这个点击通知栏的效果,是从那个页面切换到后台,下次点击通知栏时就进入到那个页面,也就是说它点击跳转的页面是动态的,所以不能是写死的。有了这个业务需求那么就可以开始写代码了。这里也是需要用到广播的,只不过不再是写内部类了。在com.llw.goodmusic下面新建一个receiver的包,然后创建NotificationClickReceiver,里面的两个可以不用勾选。

创建好之后,打开AndroidManifest.xml你会看到如下代码:

<receiver android:name=".receiver.NotificationClickReceiver"/>

下面进入MusicService中,

     //点击整个通知时发送广播Intent intent = new Intent(getApplicationContext(), NotificationClickReceiver.class);PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0,intent, PendingIntent.FLAG_UPDATE_CURRENT);

然后通过.setContentIntent(pendingIntent)设置进去,如下图所示

下面进入到NotificationClickReceiver中。

package com.llw.goodmusic.receiver;import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.llw.goodmusic.utils.BLog;/*** 通知点击广播接收器  跳转到栈顶的Activity ,而不是new 一个新的Activity** @author llw*/
public class NotificationClickReceiver extends BroadcastReceiver {public static final String TAG = "NotificationClickReceiver";@Overridepublic void onReceive(Context context, Intent intent) {BLog.d(TAG,"通知栏点击");}
}

然后运行,运行之后点击通知栏,再看日志打印,如下所示:

③ 通知栏业务处理

  在上面已经实现了通知栏的点击监听了,下面就要开始进行业务逻辑的处理了。先解决通知栏的点击业务处理,再解决通知栏按钮的点击处理,打开AndroidManager,注意这是之前我自己写的,不是系统的。在里面增加

 /*** 弱引用*/private static WeakReference<Activity> activityWeakReference;private static Object activityUpdateLock = new Object();/*** 得到当前Activity* @return*/public static Activity getCurrentActivity() {Activity currentActivity = null;synchronized (activityUpdateLock){if (activityWeakReference != null) {currentActivity = activityWeakReference.get();}}return currentActivity;}/*** 设置当前Activity* @return*/public static void setCurrentActivity(Activity activity) {synchronized (activityUpdateLock){activityWeakReference = new WeakReference<Activity>(activity);}}

然后进入到BasicApplication中,在onCreate中写入:

registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {@Overridepublic void onActivityCreated(Activity activity, Bundle savedInstanceState) {}@Overridepublic void onActivityStarted(Activity activity) {}@Overridepublic void onActivityResumed(Activity activity) {ActivityManager.setCurrentActivity(activity);}@Overridepublic void onActivityPaused(Activity activity) {}@Overridepublic void onActivityStopped(Activity activity) {}@Overridepublic void onActivitySaveInstanceState(Activity activity, Bundle outState) {}@Overridepublic void onActivityDestroyed(Activity activity) {}});

通过上面得代码就可以得到栈顶的Activity,那么怎么来使用这个Activity呢,进入到NotificationClickReceiver

 @Overridepublic void onReceive(Context context, Intent intent) {BLog.d(TAG,"通知栏点击");//获取栈顶的ActivityActivity currentActivity = ActivityManager.getCurrentActivity();intent = new Intent(Intent.ACTION_MAIN);intent.addCategory(Intent.CATEGORY_LAUNCHER);intent.setClass(context, currentActivity.getClass());intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);context.startActivity(intent);}

这样就可以实现,点击通知栏时跳转到栈顶的Activity而不是新建一个Activity。
下面就是针对通知栏的信息显示做处理了,首先肯定要根据不同的音乐显示不同的歌曲信息,这一点毋庸置疑。那么这样的话就不能一开始就显示通知栏了,而是在点击播放按钮的时候显示通知栏,当切歌,或者暂停时更新这个通知栏的状态,于是就可以在MusicService中写入一个这样的方法。

 /*** 初始化通知*/private void initNotification() {String channelId = "play_control";String channelName = "播放控制";int importance = NotificationManager.IMPORTANCE_HIGH;createNotificationChannel(channelId, channelName, importance);//点击整个通知时发送广播Intent intent = new Intent(getApplicationContext(), NotificationClickReceiver.class);PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), 0,intent, PendingIntent.FLAG_UPDATE_CURRENT);//初始化通知notification = new NotificationCompat.Builder(this, "play_control").setContentIntent(pendingIntent).setWhen(System.currentTimeMillis()).setSmallIcon(R.mipmap.icon_big_logo).setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.icon_big_logo)).setCustomContentView(remoteViews).setVisibility(NotificationCompat.VISIBILITY_PUBLIC).setAutoCancel(false).setOnlyAlertOnce(true).setOngoing(true).build();}

这里就是把原来的showNotification方法改了一下,把显示通知的代码放到改变通知栏状态的时候使用。当然还是要在onCreate中调用这个方法的。
下面先在MusiceService中定义这些变量

 /*** 歌曲间隔时间*/private static final int INTERNAL_TIME = 1000;/*** 歌曲列表*/private static List<Song> mList = new ArrayList<>();/*** 音乐播放器*/public MediaPlayer mediaPlayer;/*** 记录播放的位置*/int playPosition = 0;/*** 通知*/private static Notification notification;/*** 通知栏视图*/private static RemoteViews remoteViews;/*** 通知ID*/private int NOTIFICATION_ID = 1;/*** 通知管理器*/private static NotificationManager manager;/*** 音乐广播接收器*/private MusicReceiver musicReceiver;

然后写入一个更改通知栏样式的方法,每次对音乐进行控制时都会调用。

 /*** 更改通知的信息和UI* @param position 歌曲位置*/public void updateNotificationShow(int position) {//播放状态判断if (mediaPlayer.isPlaying()) {remoteViews.setImageViewResource(R.id.btn_notification_play, R.drawable.pause_black);} else {remoteViews.setImageViewResource(R.id.btn_notification_play, R.drawable.play_black);}//封面专辑remoteViews.setImageViewBitmap(R.id.iv_album_cover, MusicUtils.getAlbumPicture(this, mList.get(position).getPath(), 0));//歌曲名remoteViews.setTextViewText(R.id.tv_notification_song_name, mList.get(position).getSong());//歌手名remoteViews.setTextViewText(R.id.tv_notification_singer, mList.get(position).getSinger());//发送通知manager.notify(NOTIFICATION_ID, notification);}

在这个方法里面我调用MusicUtils工具类的getAlbumPicture方法。这个方法我做了一点点改动
改动如下图所示:

下面就是点击播放时的音乐方法了。

 /*** 播放*/public void play(int position) {if (mediaPlayer == null) {mediaPlayer = new MediaPlayer();//监听音乐播放完毕事件,自动下一曲mediaPlayer.setOnCompletionListener(this);}//播放时 获取当前歌曲列表是否有歌曲mList = LitePal.findAll(Song.class);if (mList.size() <= 0) {return;}try {//切歌前先重置,释放掉之前的资源mediaPlayer.reset();playPosition = position;//设置播放音频的资源路径mediaPlayer.setDataSource(mList.get(position).path);mediaPlayer.prepare();mediaPlayer.start();//显示通知updateNotificationShow(position);} catch (IOException e) {e.printStackTrace();}}

  在上面的播放方法中,首先初始化了MediaPlayer,然后添加了播放完成的监听,这个在后面也是要实现的。然后获取当前的播放位置赋值给成员变量,之后通过位置得到歌曲的路径,通过路径来播放音乐,播放音乐之后将位置传递给显示通知栏的方法,此时通知栏的信息久会更改。

在onCreate方法中添加如下代码,获取本地歌曲数据。

mList = LitePal.findAll(Song.class);

这样做是避免空对象导致APP的崩溃。

接下来就是上一曲的方法

 /*** 上一首*/public void previousMusic() {if (playPosition <= 0) {playPosition = mList.size() - 1;} else {playPosition -= 1;}play(playPosition);}

通过播放位置,先判断当前是为第一首歌,是则将播放位置移动到最后一首,不是则直接减一,之后则调用play方法播放上一首歌曲。

下一曲的方法

 /*** 下一首*/public void nextMusic() {if (playPosition >= mList.size() - 1) {playPosition = 0;} else {playPosition += 1;}play(playPosition);}

先判断当前是否为最后一首,是的话则从移动到第一首,不是则加一到下一首。然后调用play方法播放下一首歌曲。

暂停继续音乐

 /*** 暂停/继续 音乐*/public void pauseOrContinueMusic() {if (mediaPlayer.isPlaying()) {mediaPlayer.pause();} else {mediaPlayer.start();}//更改通知栏播放状态updateNotificationShow(playPosition);}

更改播放状态

最后是关闭通知栏的方法

 /*** 关闭音乐通知栏*/public void closeNotification() {if (mediaPlayer != null) {if (mediaPlayer.isPlaying()) {mediaPlayer.pause();}}manager.cancel(NOTIFICATION_ID);}

下面就是调用的地方了

然后还要实现MediaPlayer的音乐播放完成的监听,

public class MusicService extends Service implements MediaPlayer.OnCompletionListener

然后重写onCompletion方法,在里面直接调用nextMusic播放下一曲即可。

 /*** 当前音乐播放完成监听** @param mp*/@Overridepublic void onCompletion(MediaPlayer mp) {//下一曲nextMusic();}

下面就要设置通知出现的入口,一般来说是在点击播放按钮,当前有音乐播放时,才会显示通知。然后在layout下面新建一个通用的底部通知布局。
play_control_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"><!--底部播放控制布局--><LinearLayoutandroid:id="@+id/lay_bottom"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_alignParentBottom="true"android:background="@color/bottom_bg_color"android:gravity="center_vertical"android:paddingLeft="@dimen/dp_8"android:paddingTop="@dimen/dp_8"android:paddingRight="@dimen/dp_16"android:paddingBottom="@dimen/dp_8"><!-- logo和播放进度 使用相对布局达成覆盖的效果--><RelativeLayoutandroid:layout_width="wrap_content"android:layout_height="wrap_content"><!--logo--><com.google.android.material.imageview.ShapeableImageViewandroid:id="@+id/iv_logo"android:layout_width="@dimen/dp_48"android:layout_height="@dimen/dp_48"android:padding="1dp"android:src="@mipmap/icon_music"app:shapeAppearanceOverlay="@style/circleImageStyle"app:strokeColor="@color/white"app:strokeWidth="@dimen/dp_2" /><!--播放进度  自定义View--><com.llw.goodmusic.view.MusicRoundProgressViewandroid:id="@+id/music_progress"android:layout_width="@dimen/dp_48"android:layout_height="@dimen/dp_48"app:radius="22dp"app:strokeColor="@color/gold_color"app:strokeWidth="2dp" /></RelativeLayout><!--歌曲信息  歌名 - 歌手 --><com.google.android.material.textview.MaterialTextViewandroid:id="@+id/tv_song_name"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_weight="1"android:ellipsize="marquee"android:focusable="true"android:focusableInTouchMode="true"android:marqueeRepeatLimit="marquee_forever"android:paddingLeft="@dimen/dp_12"android:paddingRight="@dimen/dp_12"android:singleLine="true"android:text="Good Music"android:textColor="@color/white"android:textSize="@dimen/sp_16" /><!--歌曲控制按钮--><com.google.android.material.button.MaterialButtonandroid:id="@+id/btn_play"android:layout_width="@dimen/dp_36"android:layout_height="@dimen/dp_36"android:insetLeft="@dimen/dp_0"android:insetTop="@dimen/dp_0"android:insetRight="@dimen/dp_0"android:insetBottom="@dimen/dp_0"android:theme="@style/Theme.MaterialComponents.Light.NoActionBar"app:backgroundTint="@color/transparent"app:cornerRadius="@dimen/dp_18"app:icon="@mipmap/icon_play"app:iconGravity="textStart"app:iconPadding="@dimen/dp_0"app:iconSize="@dimen/dp_36" /></LinearLayout>
</layout>

其实就是之前LocalMusicActivity的底部布局。


下面进入MainActivity中,

 /*** 底部logo图标,点击之后弹出当前播放歌曲详情页*/private ShapeableImageView ivLogo;/*** 底部当前播放歌名*/private MaterialTextView tvSongName;/*** 底部当前歌曲控制按钮, 播放和暂停*/private MaterialButton btnPlay;/*** 自定义进度条*/private MusicRoundProgressView musicProgress;/*** 列表位置*/private int listPosition = 0;

然后在initData中,通过引入的布局绑定控件,并且添加点击监听,下面就可以在

 @Overridepublic void onClick(View v) {switch (v.getId()) {case R.id.lay_local_music://本地音乐startActivity(new Intent(context, LocalMusicActivity.class));break;case R.id.btn_play:if (mList.size() == 0) {show("没有可播放的音乐,请到 “本地音乐” 进行扫描");return;}musicService.play(listPosition);break;default:break;}}

下面运行测试一波:

④ 运行效果图

结语

  现在已经搞定了后台播放和通知栏控制音乐,下一篇就该是通知栏和Activity的双向控制了。

源码地址:Good Music

Android 音乐APP(五)音乐通知栏、后台播放音乐相关推荐

  1. android mediaplayer 后台播放,Android服务—基于MediaPlayer后台播放音乐

    Android服务-基于MediaPlayer后台播放音乐 操作环境:Android Studio 4.0.0.SDK Level 21(版本5.0 Lollipop).Windows 10.集成显卡 ...

  2. android后台自播放音乐,Android实现后台播放音乐(Service方式)

    Android实现后台播放音乐(Service方式) 实现: 在res文件夹下添加raw文件夹,添加mp3/4格式的音乐文件 注意命名规则只能是a-z,0-9,和下划线_ 不能大写字母和- Andro ...

  3. Android后台播放音乐保活,安卓后台保活黑科技 播放无声音乐

    1.准备一段无声的音频,新建一个播放音乐的Service类,将播放模式改为无限循环播放.在其onDestroy方法中对自己重新启动. public class PlayerMusicService e ...

  4. IOS后台运行 之 后台播放音乐

    IOS后台运行 之 后台播放音乐 iOS 4开始引入的multitask,我们可以实现像ipod程序那样在后台播放音频了.如果音频操作是用苹果官方的AVFoundation.framework实现,像 ...

  5. 解决 后台播放音乐时,设置手机铃声,后台音乐不会暂停

    2019独角兽企业重金招聘Python工程师标准>>> 手机后台播放音乐时,设置手机铃声,后台音乐不会暂停,此现象的为设置手机铃声界面,并没要加入播放的foucs机制, 此修改在pa ...

  6. iOS- 关于AVAudioSession的使用——后台播放音乐

    1.前言 •AVAudioSession是一个单例,无需实例化即可直接使用.AVAudioSession在各种音频环境中起着非常重要的作用 •针对不同的音频应用场景,需要设置不同的音频会话分类 1.1 ...

  7. 后台播放音乐时进来电话或微信视频通话暂停音乐播放 网易云音乐 喜马拉雅...

    最近项目中遇到一个问题,app内音乐后台播放时,如果有电话或者微信视频通话进来,app后台音乐还在播放.这样就造成用户体验不好,研究了市面上的音乐播放器,比如网易云音乐就很好的做到了如果有微信视频或者 ...

  8. Android如何判断当前手机是否正在播放音乐,并获取到正在播放的音乐的信息

    我想实现如下的场景,判断当前Android手机上是否正在播放音乐,如果是,通过某个特定的手势, 或者点击某个按键,将当前我正在听的音乐共享出去. 第一步,就是判断当前是否有音乐正在播放. 最开始我想得 ...

  9. Android 音乐APP(一)扫描本地音乐

    效果图 音乐APP 扫描本地音乐 前言 正文 ① 新建项目 ② 第三方依赖 ③ 权限和基础配置 ④ 页面设计 ⑤ 权限请求 ⑥ 获取音乐数据 ⑦ 数据显示 结语 前言   这个项目纯粹的就是心血来潮, ...

最新文章

  1. C++对象模型2——编译器生成构造函数的几种情况
  2. whireshark过滤器学习与使用
  3. 后台设置 datakeynames
  4. TYVJ P1012 火柴棒等式 Label:枚举
  5. 天上地下,马斯克和贝佐斯终有一战?
  6. UBUNTU使用五笔98输入法
  7. python读取excel(xlrd)
  8. 标准正态分布表完整图 查询_正态分布基本概念及Excel实现
  9. C语言正交表测试用例,正交表设计用例(简单+实用) - Jackc的个人空间 - 51Testing软件测试网 51Testing软件测试网-软件测试人的精神家园...
  10. android 脚本发短信,Android使用Intent发送短信的实现方法
  11. SoftICE使用(3)—在VMware中配置远程SoftICE的另一种办法 zz xfocus
  12. 代码写得很牛逼但UI界面却搞得很丑?来,杨工带你!
  13. Q1成绩:华为可穿戴设备增幅亮眼,Uber亏损10亿美元!
  14. 致我们失去但美好回忆的青春
  15. 电脑长期未用或深度放电,电池欠压充不上电(充电指示灯不亮)
  16. bootstrap4导航栏居右
  17. 投稿前如何查询期刊的审稿周期
  18. [论文阅读笔记69]医学术语标准化-CODER
  19. Python Opencv 实现鼠标事件(包含一个练习)——事件触发讲解·以及鼠标回调函数的实现
  20. python bz2模块

热门文章

  1. 在线二维码生成工具html源码
  2. VMware 虚拟机 linux执行 ifconfig 命令 eth0没有IP地址(intet addr、Bcast、Mask) UP BROADCAST MULTICAST 问题
  3. BCIduino社区|HY-BCI Pro多通道科研级脑电放大器接收lsl脑电数据并进行显示
  4. 光猫及二级路由器Openwrt均开启IPv6,满足双层网络内IPv6的获取
  5. Chrome浏览器默认全屏启动(非--kiosk模式)
  6. CocosCreater 接入手Q (QQ小游戏)、小米快游戏 接入指南、脱坑指南
  7. 黑猴子的家:Kali Linux + Vmware 15 安装操作系统
  8. WSL 2 网络配置
  9. 自学python书籍怎么选-python自学Day07(自学书籍python编程从入门到实践)
  10. 禁止K8S容器内子进程拥有提升权限的能力