iOS中的视频播放

引言

在数字时代,视频已成为我们日常沟通和娱乐不可或缺的一部分。从简短的社交媒体剪辑到全长电影,我们对流畅、高质量视频播放的需求从未如此强烈。对于开发者来说,为iOS用户提供无瑕疵的视频体验是一项挑战,也是一种艺术。本篇博客将带你深入了解苹果生态系统中视频播放的核心——AV Kit和AV Foundation。

AV Kit

我们将首先探讨AV Kit的强大功能,这是一个为iOS开发者设计的高级视频播放框架。它封装了复杂的底层技术,提供了易于使用的界面,允许你迅速地集成视频播放功能到你的应用中。从管理播放控制和状态,到处理各种屏幕大小和设备方向,AV Kit让一切变得简单。

介绍

AV Kit从iOS 8开始被引入到iOS平台,是一个非常简单的标准框架,只包含了一个AVPlayerViewContoller类。它继承自UIViewController,用于展示和控制AVPlayer实例的播放。AVPlayerViewController具有一个很简单的界面,并提供了许多属性,下面列举几个常用的属性:

  • player:用来播放媒体内容的AVPlayer实例。
  • showsPlayebackControls:用来表示播放器中控制控件是否显示。
  • videoGravity:设置视频的填充方式。
  • readyForDisplay:通过观察这个值来确定视频内容是否已经准备好进行播放。

实现

上面的页面几乎已经包括了视频播放的所有功能,播放、暂停、快进、音量,甚至还支持了AirPlay,我们来看一下它的实现有多么简单。

 NSURL * url = [[NSBundle mainBundle] URLForResource:@"video" withExtension:@".mov"];
    AVPlayerViewController * playerViewController = [[AVPlayerViewController alloc] init];
    playerViewController.player = [[AVPlayer alloc] initWithURL:url];
    playerViewController.modalPresentationStyle = UIModalPresentationFullScreen;
    [self presentViewController:playerViewController animated:YES completion:nil];

AV Foundation

然而,每一位热衷于精细控制和定制播放体验的开发者都知道,有时“标准”是不够的。这就是我们转向AV Foundation的地方。在这个强大的框架下,你将揭开视频播放的神秘面纱,学习如何构建一个高级播放控制的完全自定义播放器。无论你是想要优化性能、处理复杂的用户交互,还是只是为了给你的应用那独特的个性化触摸,AV Foundation都为你提供了必要的工具。

介绍

使用AV Foundation实现视频播放的方式,最终是通过使用AVPlayer来播放AVAsset,但是AVAsset模型只包含媒体资源的静态信息,这些不变的属性用来描述对象的静态状态。这就意味着仅适用AVAsset对象时无法实现播放功能的。当我们需要对一个资源及其相关曲目进行播放时,首先需要通过AVPlayerItem和AVPlayerItemTrack类构件相应的动态内容。

AVAsset

AVAsset是一个抽象类和不可变类,定义了媒体资源混合呈现的方式,将媒体资源的静态属性模块化成一个整体,比如它们的标题、时长和元数据等。

AVPlayerItem

AVPlayerItem会建立媒体资源动态视角的数据模型并保存AVPlayer在播放资源时的呈现状态。在这个类中我们会看到诸如seekToTime:的方法以及访问currentTime和presentationSize的属性。

AVPlayer

AVPlayer是一个用来播放基于时间的视听媒体的控制器对象。支持播放本地、分步下载或通过HTTP Live Streaming协议得到的流媒体,并在多种播放场景中播放这些视频资源。

AVPlayerLayer

AVPlayerLayer构建于Core Animation之上,AVPlayerLayer扩展了Core Animation的CALayer类,并通过框架在屏幕上显示视频内容。这一图层并不提供任何可视化空间或其他附件(根据开发者需求搭建的),但是它用作是内容的渲染面。创建AVPlayerLayer需要一个指向AVPlayer实例的指针,这就将图层和播放器紧密绑定在一起,保证了当播放器基于时间的方法出现时使二者保持同步。

简单实现

使用上面的关键类,实现一个非常简单的视频播放功能,获取资源,播放资源,将播放图层添加到当前控制器图层中。

#import "ViewController.h"
#import <AVFoundation/AVFoundation.h>
@interface ViewController ()
@property(nonatomic,strong)AVPlayer * player;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    //1.Define the asset URL
    NSURL * assetURL = [[NSBundle mainBundle] URLForResource:@"waves" withExtension:@"mp4"];
    //2.Create an instance of AVAsset
    AVAsset * asset = [AVAsset assetWithURL:assetURL];
    //3.Create an AVPlayerItem with a pointer to the asset to play
    AVPlayerItem * playerItem = [AVPlayerItem playerItemWithAsset:asset];
    //4.Create an instance of AVPlayer with a pointer to the player item
    self.player = [AVPlayer playerWithPlayerItem:playerItem];
    //5.create a player layer to driect the video content
    AVPlayerLayer * playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
    //6.attach layer into layer hierarchy
    [self.view.layer addSublayer:playerLayer];
}

完整实现

下面我们使用以上的关键类,来实现一个相对较为负责,功能较为全面的播放器,需要包括播放,暂停,播放进度显示,已经拖拽进度条等功能。

我们将实现分散到三个不同类当中THTransport,THOverlayView,THPlayerView,THPlayerController。

THTransport

代理,接口如下:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@protocol THTransportDelegate <NSObject>

- (void)play;
- (void)pause;
- (void)stop;

- (void)scrubbingDidStart;
- (void)scrubbedToTime:(NSTimeInterval)time;
- (void)scrubbingDidEnd;
- (void)jumpedToTime:(NSTimeInterval)time;

@end

@protocol THTransport <NSObject>

@property(nonatomic,weak)id <THTransportDelegate>  delegate;

- (void)setTitle:(NSString *)title;
- (void)setCurrentTime:(NSTimeInterval)time duration:(NSTimeInterval)duration;
- (void)setScrubbingTime:(NSTimeInterval)time;
- (void)playbackComplete;

@end

NS_ASSUME_NONNULL_END
THOverlayView

与用户交互的视图图层,上面绘制了播放,暂停按钮, 进度条,时间显示等等。接口如下

#import <UIKit/UIKit.h>
#import "THTransport.h"

NS_ASSUME_NONNULL_BEGIN

@interface THOverlayView : UIView<THTransport>

@property(nonatomic,weak)id <THTransportDelegate>  delegate;

- (void)setCurrentTime:(NSTimeInterval)time;


@end

NS_ASSUME_NONNULL_END

遵循了THTransport协议,并且有一个遵循THTransportDelegate的delegate属性,和设置当前时间的方法。

实现如下:

#import "THOverlayView.h"
#import "UIView+THAdditions.h"

@interface THOverlayView ()

@property(nonatomic,assign)BOOL  controlsHidden;
@property(nonatomic,strong)UINavigationBar * navBar;
@property(nonatomic,strong)UIButton * showButton;
@property(nonatomic,strong)UIToolbar * toolBar;
@property(nonatomic,strong)UIButton * playButton;
@property(nonatomic,strong)UISlider * slider;
@property(nonatomic,strong)UILabel * currentTimeLabel;
@property(nonatomic,strong)UILabel * durationTimeLabel;
@property(nonatomic,assign)BOOL  scrubbing;
@property(nonatomic,assign)CGFloat  lastPlaybackRate;

@property(nonatomic,strong)NSTimer * timer;

@end

@implementation THOverlayView


- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self setupView];
    }
    return self;
}

- (void)setupView{
    [self addNavBar];
    [self addToolBar];
    [self addGestureRecognizer];
    [self resetTimer];
}


- (void)addNavBar{
    self.navBar = [[UINavigationBar alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 44)];
    [self addSubview:self.navBar];
    
    UINavigationItem * navigationItem = [[UINavigationItem alloc] init];
    UIBarButtonItem * downItemButton = [[UIBarButtonItem alloc] initWithTitle:@"Down" style:UIBarButtonItemStyleDone target:self action:@selector(down)];
    self.showButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 40, 20)];
    [self.showButton setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    [self.showButton setTitle:@"Show" forState:UIControlStateNormal];
    [self.showButton setTitle:@"Hide" forState:UIControlStateSelected];
    [self.showButton addTarget:self action:@selector(showOnclick:) forControlEvents:UIControlEventTouchUpInside];
    UIBarButtonItem * showItemButton = [[UIBarButtonItem alloc] initWithCustomView:self.showButton];
    navigationItem.leftBarButtonItem = downItemButton;
    navigationItem.rightBarButtonItem = showItemButton;
    [self.navBar pushNavigationItem:navigationItem animated:YES];
}

- (void)addToolBar{
    self.toolBar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, [UIScreen mainScreen].bounds.size.height - 44, [UIScreen mainScreen].bounds.size.width, 44)];
    [self addSubview:self.toolBar];
    UIBarButtonItem * spaceItem0 = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
    
    self.playButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 33, 33)];
    self.playButton.selected = YES;
    [self.playButton setImage:[UIImage imageNamed:@"play_button"] forState:UIControlStateNormal];
    [self.playButton setImage:[UIImage imageNamed:@"pause_button"] forState:UIControlStateSelected];
    [self.playButton addTarget:self action:@selector(playButtonOnclick:) forControlEvents:UIControlEventTouchUpInside];
    UIBarButtonItem * playItem = [[UIBarButtonItem alloc] initWithCustomView:self.playButton];
    
    UIBarButtonItem * spaceItem1 = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
    
    self.currentTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 80, 20)];
    self.currentTimeLabel.textAlignment = NSTextAlignmentCenter;
    self.currentTimeLabel.font = [UIFont systemFontOfSize:15];
    self.currentTimeLabel.textColor = [UIColor darkTextColor];
    self.currentTimeLabel.text = @"00:00";
    UIBarButtonItem * currentItem = [[UIBarButtonItem alloc] initWithCustomView:self.currentTimeLabel];
    
    self.slider = [[UISlider alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width - 290, 0)];
    [self.slider addTarget:self action:@selector(showPopupUI) forControlEvents:UIControlEventValueChanged];
    [self.slider addTarget:self action:@selector(hidePopupUI) forControlEvents:UIControlEventTouchUpInside];
    [self.slider addTarget:self action:@selector(unhidePopupUI) forControlEvents:UIControlEventTouchDown];
    UIBarButtonItem * sliderItem = [[UIBarButtonItem alloc] initWithCustomView:self.slider];
    
    self.durationTimeLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 80, 20)];
    self.durationTimeLabel.textAlignment = NSTextAlignmentCenter;
    self.durationTimeLabel.font = [UIFont systemFontOfSize:15];
    self.durationTimeLabel.textColor = [UIColor darkTextColor];
    self.durationTimeLabel.text = @"00:00";
    UIBarButtonItem * durationItem = [[UIBarButtonItem alloc] initWithCustomView:self.durationTimeLabel];
    
    UIBarButtonItem * spaceItem2 = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
    
    UIBarButtonItem * subtitlesItem = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"subtitles"] style:UIBarButtonItemStyleDone target:self action:@selector(showSubtitleViewController)];
    UIBarButtonItem * spaceItem3 = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
    
    self.toolBar.items = @[spaceItem0,playItem,spaceItem1,currentItem,sliderItem,durationItem,spaceItem2,subtitlesItem,spaceItem3];
}

- (void)addGestureRecognizer{
    self.userInteractionEnabled = YES;
    UITapGestureRecognizer * tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(recognizer)];
    [self addGestureRecognizer:tapGestureRecognizer];
}

//MARK:播放按钮
- (void)playButtonOnclick:(UIButton *)button{
    button.selected = !button.selected;
    if (self.delegate) {
        if (button.selected) {
            [self.delegate play];
        }else{
            [self.delegate pause];
        }
    }
}


//MARK:按下搓擦条
- (void)unhidePopupUI{
    self.scrubbing = YES;
    [self resetTimer];
    [self.delegate scrubbingDidStart];
}

//MARK:搓擦条值改变
- (void)showPopupUI{
    self.currentTimeLabel.text = @"--:--";
    [self setScrubbingTime:self.slider.value];
    [self.delegate scrubbedToTime:self.slider.value];
}

//MARK:手指离开搓擦条
- (void)hidePopupUI{
    self.scrubbing = NO;
    [self.delegate scrubbingDidEnd];
}

//MARK:点击屏幕显示或隐藏控制栏
- (void)recognizer{
    [UIView animateWithDuration:0.35 animations:^{
        if (!self.controlsHidden) {
                self.navBar.frameY -= self.navBar.frameHeight;
                self.toolBar.frameY += self.toolBar.frameHeight;
        }else{
            self.navBar.frameY += self.navBar.frameHeight;
            self.toolBar.frameY -= self.toolBar.frameHeight;
            [self resetTimer];
        }
        self.controlsHidden = !self.controlsHidden;
    }];
}

//MARK:重新开始定时器
- (void)resetTimer{
    [self.timer invalidate];
    if (!self.scrubbing) {
        self.timer = [NSTimer scheduledTimerWithTimeInterval:5 repeats:NO block:^(NSTimer * _Nonnull timer) {
            if (self.timer.isValid && !self.controlsHidden) {
                [self recognizer];
            }
        }];
    }
}

//MARK:指定时间播放
- (void)setCurrentTime:(NSTimeInterval)time{
    [self.delegate jumpedToTime:time];
}

//MARK:THTransport 设置时间及进度条
- (void)setCurrentTime:(NSTimeInterval)time duration:(NSTimeInterval)duration{
    NSInteger currentSeconds = ceilf(time);
//    double remainingTime = duration - time;
    self.currentTimeLabel.text = [self formatSeconds:currentSeconds];
    self.durationTimeLabel.text = [self formatSeconds:duration];
    self.slider.minimumValue = 0.0f;
    self.slider.maximumValue = duration;
    self.slider.value = time;
}

//MARK:THTransport 设置时间
- (void)setScrubbingTime:(NSTimeInterval)time{
    self.currentTimeLabel.text = [self formatSeconds:time];
}

//MARK:THTransport 播放完成
- (void)playbackComplete{
    self.slider.value = 0.0f;
    self.playButton.selected = NO;
}


//MARK:返回
- (void)down{
    [self.delegate stop];
    [self.window.rootViewController dismissViewControllerAnimated:YES completion:nil];
}

- (NSString *)formatSeconds:(NSInteger)value {
    NSInteger seconds = value % 60;
    NSInteger minutes = value / 60;
    return [NSString stringWithFormat:@"%02ld:%02ld", (long) minutes, (long) seconds];
}

@end
THPlayerView

播放视图,作用相当于是AVPlayerLayer,接口如下:

#import <UIKit/UIKit.h>
#import "THTransport.h"

@class AVPlayer;

NS_ASSUME_NONNULL_BEGIN

@interface THPlayerView : UIView

- (id)initWithPlayer:(AVPlayer *)player;

@property(nonatomic,readonly) id <THTransport>  transport;

@end

NS_ASSUME_NONNULL_END

接口比较简单,有一个自定义的初始化方法,和一个遵循THTransport的transport。

实现如下:

#import "THPlayerView.h"
#import <AVFoundation/AVFoundation.h>
#import "THOverlayView.h"

@interface THPlayerView ()

@property(nonatomic,strong)THOverlayView * overlayView;

@end

@implementation THPlayerView

+ (Class)layerClass{
    return [AVPlayerLayer class];
}

- (id)initWithPlayer:(AVPlayer *)player{
    self = [super initWithFrame:CGRectZero];
    if (self) {
        self.backgroundColor = [UIColor blackColor];
        self.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
        [(AVPlayerLayer *)[self layer] setPlayer:player];
        [self addOverlayView];
    }
    return self;
}

- (void)addOverlayView{
    self.overlayView = [[THOverlayView alloc] initWithFrame:CGRectZero];
    [self addSubview:self.overlayView];
}

- (void)layoutSubviews{
    [super layoutSubviews];
    self.overlayView.frame = self.bounds;
}

- (id <THTransport>)transport{
    return self.overlayView;
}


@end

THPlayerController

控制器,但不是视图控制器,主要负责播放器的播放,暂停,快进等功能,是我们处理核心播放API方法的地方。

创建控制器

该类的接口比较简单,只有一个初始化方法和一个只读的view属性。

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface THPlayerController : NSObject
- (id)initWithURL:(NSURL *)assetURL;
@property(nonatomic,strong,readonly)UIView * view;
@end
NS_ASSUME_NONNULL_END

因为它是一个比较核心的类,所以实现较为复杂,我们可以差分开来进行说明,首先我们来看一下它的扩展

#import "THPlayerController.h"
#import <AVFoundation/AVFoundation.h>
#import "THTransport.h"
#import "THPlayerView.h"
#define STATUS_KEYPATH @"status"
#define REFESH_INTERVAL 0.5f
static const NSString * PlayerItemStatusContext;
@interface THPlayerController ()<THTransportDelegate>
@property(nonatomic,strong)AVAsset * asset;
@property(nonatomic,strong)AVPlayerItem * playerItem;
@property(nonatomic,strong)AVPlayer * player;
@property(nonatomic,strong)THPlayerView * playerView;
@property(nonatomic,weak)id <THTransport> transport;
@property(nonatomic,strong)id timeObserver;
@property(nonatomic,strong)id itemEndObserver;
@property(nonatomic,assign)float  lastPlaybackRate;
@end

里面定义了前三个属性,就是我们提到的关键类中的三个,而THPlayerView上面我们已经提到是用来显示播放画面的一个视图。

接下来看一下它的实现

@implementation THPlayerController
- (id)initWithURL:(NSURL *)assetURL{
    self = [super init];
    if (self) {
        _asset = [AVAsset assetWithURL:assetURL];
        [self prepareToPlay];
    }
    return self;
}
- (void)prepareToPlay{
    NSArray * keys = @[@"tracks",@"duration",@"commonMetadata"];
    self.playerItem = [AVPlayerItem playerItemWithAsset:self.asset automaticallyLoadedAssetKeys:keys];
    [self.playerItem addObserver:self
                      forKeyPath:STATUS_KEYPATH
                         options:0
                         context:&PlayerItemStatusContext];
    self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
    self.playerView = [[THPlayerView alloc] initWithPlayer:self.player];
    self.transport = self.playerView.transport;
    self.transport.delegate = self;
}

首先是一个自定义的初始化方法,传入资源的URL,开始准备播放的播放工作。

创建AVPlayerItem的时候使用新的初始化方法initWithAsset:automaticallyLoadedAssetKeys:方法并传入了一个keys作为参数,使用这个方法会让AVPlayerItem在初始化队列过程中载入tracks、duration和commonMetadata属性。

监听添AVPlayerItem的status属性,当它转换为AVPlayerItemStatusReadyToPlay时开始执行播放。

监听状态改变

下面我们来看一下监听状态的实现

//MARK:监听回调
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if (context == &PlayerItemStatusContext) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.playerItem removeObserver:self forKeyPath:STATUS_KEYPATH];
            if (self.playerItem.status == AVPlayerItemStatusReadyToPlay) {
                //设置时间监听器
                [self addPlayerItemTimeObserver];
                [self addItemEndObserverForPlayerItem];
                CMTime duration = self.playerItem.duration;
                //同步显示时间
                [self.transport setCurrentTime:CMTimeGetSeconds(kCMTimeZero) duration:CMTimeGetSeconds(duration)];
                [self.player play];
            }else{
                NSLog(@"加载视频失败");
            }
        });
    }
}
进度监听

AVPlayer提供了一个addPeriodicTimeObserverForInterval:queue:usingBlock:方法来让我们实现播放进度的监听。

- (void)addPlayerItemTimeObserver{
    //创建0.5秒刷新
    CMTime interval = CMTimeMakeWithSeconds(REFESH_INTERVAL, NSEC_PER_SEC);
    //主队列
    dispatch_queue_t queue = dispatch_get_main_queue();
    __weak  THPlayerController * weakSelf = self;
    //添加观察
    self.timeObserver = [self.player addPeriodicTimeObserverForInterval:interval
                                                                  queue:queue
                                                             usingBlock:^(CMTime time) {
        NSTimeInterval currentTime = CMTimeGetSeconds(time);
        NSTimeInterval duration = CMTimeGetSeconds(weakSelf.playerItem.duration);
        [weakSelf.transport setCurrentTime:currentTime duration:duration];
    }];
}
播放结束
- (void)addItemEndObserverForPlayerItem{
    NSString * name = AVPlayerItemDidPlayToEndTimeNotification;
    NSOperationQueue * queue = [NSOperationQueue mainQueue];
    __weak THPlayerController * weakSelf = self;
    self.itemEndObserver = [[NSNotificationCenter defaultCenter] addObserverForName:name
                                                                             object:self.playerItem
                                                                              queue:queue usingBlock:^(NSNotification * _Nonnull note) {
        [weakSelf.player seekToTime:kCMTimeZero completionHandler:^(BOOL finished) {
            [weakSelf.transport playbackComplete];
        }];
    }];
}
- (void)dealloc{
    if (self.itemEndObserver) {
        NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];
        [nc removeObserver:self.itemEndObserver
                      name:AVPlayerItemDidPlayToEndTimeNotification
                    object:self.player.currentItem];
        self.itemEndObserver = nil;
    }
}
代理方法

//MARK:播放
- (void)play {
    [self.player play];
}
//MARK:暂停
- (void)pause {
    self.lastPlaybackRate = self.player.rate;
    [self.player pause];
}
//MARK:停止
- (void)stop {
    [self.player setRate:0.0f];
    [self.transport playbackComplete];
}
//MARK:跳转到
- (void)jumpedToTime:(NSTimeInterval)time {
    [self.player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC)];
}
- (void)scrubbingDidStart {
    self.lastPlaybackRate = self.player.rate;
    [self.player pause];
    [self.player removeTimeObserver:self.timeObserver];
}
//MARK:拖拽结束
- (void)scrubbingDidEnd {
    [self addPlayerItemTimeObserver];
    if (self.lastPlaybackRate > 0.0f) {
        [self.player play];
    }
}
//MARK:拖拽到
- (void)scrubbedToTime:(NSTimeInterval)time {
    [self.playerItem cancelPendingSeeks];
    [self.player seekToTime:CMTimeMakeWithSeconds(time, NSEC_PER_SEC)];
}

结语

本博客将通过分析两个框架的特点和区别,带你走在打造完美iOS视频播放体验的前沿。我们将从AV Kit的简单集成开始,然后深入AV Foundation的定制深渊,揭示那些令你的应用在竞争激烈的App Store中脱颖而出的秘密。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/336568.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据结构之list类

前言 list是列表类。从list 类开始&#xff0c;我们就要接触独属于 Python 的数据类型了。Python 简单、易用&#xff0c;很大一部分原因就是它对基础数据类型的设计各具特色又相辅相成。 话不多说&#xff0c;让我们开始学习第一个 Python 数据类型一list。 1. list的赋值 输…

手把手教你使用 VS Code 运行和调试 Python 程序

本文以 Ubuntu 系统为例&#xff0c;介绍如何在 VS Code 上配置 Python 的编程环境&#xff0c;并把 Python 程序运行、调试起来。由于 Python 是解释型语言&#xff0c;并且 VS Code 中提供了内置的调试器可用于调试 Python 代码&#xff0c;因此配置和操作流程比调试 C/C 代码…

SpringSecurity+JWT前后端分离架构登录认证

目录 1. 数据库设计 2. 代码设计 登录认证过滤器 认证成功处理器AuthenticationSuccessHandler 认证失败处理器AuthenticationFailureHandler AuthenticationEntryPoint配置 AccessDeniedHandler配置 UserDetailsService配置 Token校验过滤器 登录认证过滤器接口配置…

C++将信息输入到文件内

第一步检查文件是否打开&#xff0c;用到头文件&#xff1a; #include <fstream> #include <sstream> 文件打开的函数为 file.isopen() 信息输入到文件应该为 file << "" << value; 注意是file<< 如图 定义file ofstream f…

计算机组成原理 第一弹

ps&#xff1a;本文章的图片来源都是来自于湖科大教书匠高老师的视频&#xff0c;声明&#xff1a;仅供自己复习&#xff0c;里面加上了自己的理解 这里附上视频链接地址&#xff1a;1-2 计算机的发展_哔哩哔哩_bilibili ​​ 目录 &#x1f680;计算机系统 &#x1f680;计…

UI测试脚本录制器已上线,RunnerGo :UI自动化测试平台

想快速配置可视化UI自动化测试脚本&#xff1f;RunnerGo近期上线脚本录制器&#xff0c;根据你的测试操作直接生成UI自动化测试脚本&#xff0c;下面是使用方法 Step1:下载录制器 点击RunnerGo上方插件按钮下载录制器 Step2:录制器使用 将插件文件拖入浏览器扩展程序 点击打…

Zabbix 系统监控详解

1 介绍 1.1 摘要 本文深入浅出&#xff0c;切近实际运维应用&#xff0c;由 zabbix 3.4 版本入手&#xff0c;学习 zabbix 监控告警实现方式&#xff0c;由 zabbix 5.0 浅出实现快速部署、快速应用。本人从业多年&#xff0c;关注 zabbix 开源社区&#xff0c;以及 zabbix 官…

【计算机网络】3、IPv6、网络三层模型、网络的规划与设计、网络的规划与设计、网络存储技术、网络地址翻译NAT、默认网关、虚拟局域网VLAN、虚拟专用网VPN、URL

文章目录 IPv6IPv6的特点IPv4和IPv6的过渡期间主要采用三种基本技术双协议栈隧道技术翻译技术 网络三层模型核心层汇聚层接入层 网络的规划与设计工作区子系统水平布线子系统管理子系统垂直干线子系统设备间子系统建筑群子系统总结 廉价磁盘网络存储技术直接附加存储(DAS)网络附…

Git学习笔记(第1章):Git概述

目录 1.1 版本控制 1.1.1 何为版本控制 1.1.2 为什么需要版本控制 1.1.3 版本控制工具 1.2 发展历史 1.3 工作机制 1.4 代码托管中心&#xff08;远程库&#xff09; Git是一个免费的、开源的分布式版本控制系统&#xff0c;可以快速高效地处理从小型到大型的各种项目。…

LeetCode19:删除链表的倒数第N个结点

力扣题目链接 思路&#xff1a;由于本题有可能删除头结点&#xff0c;为保证删除头结点和其他结点的操作一致&#xff0c;因此首先创建一个虚拟头结点dummy。 其次&#xff0c;本题需要删除倒数第N个结点&#xff0c;由于单链表只有next指针&#xff0c;因此需要找到倒数第N1…

事件驱动架构

请求驱动 服务注册&#xff0c;服务发现&#xff0c;虽然调用地址隐藏了&#xff0c;但是调用stub必须相同。 rpc通信&#xff0c;远程调用。 生产者和消费者要有相同的stub存根。 消费者和生产者的调用接口是耦合的。 事件驱动 核心&#xff1a;上下游不进行通信 中间通过M…

AP5101C 高压线性 LED恒流驱动器 DFN2*2 LED灯汽车雾灯转向灯

产品描述 AP5101C 是一款高压线性 LED 恒流芯片 &#xff0c; 简单 、 内置功率管 &#xff0c; 适用于6- 100V 输入的高精度降压 LED 恒流驱动芯片。电流2.0A。AP5101C 可实现内置MOS 做 2.0A,外置 MOS 可做 3.0A 的。AP5101C 内置温度保护功能 &#xff0c;温度保护点为 130 …

「Kafka」Broker篇

「Kafka」Broker篇 主要讲解的是在 Kafka 中是怎么存储数据的&#xff0c;以及 Kafka 和 Zookeeper 之间如何进行数据沟通的。 Kafka Broker 总体工作流程 Zookeeper 存储的 Kafka 信息 启动 Zookeeper 客户端&#xff1a; [atguiguhadoop102 zookeeper-3.5.7]$ bin/zkCli.sh通…

go语言(一)----声明变量

package mainimport ("fmt""time" )func main() {fmt.Print("hello go!")time.Sleep(1 * time.Second)}运行后&#xff0c;结果如下&#xff1a; 1、golang表达式中&#xff0c;加&#xff1b;和不加&#xff1b;都可以 2、函数的{和函数名一…

范围运算between...and和空判断

目录 between...and 空判断 Oracle从入门到总裁:https://blog.csdn.net/weixin_67859959/article/details/135209645 between...and between...and的主要功能是用户进行范围查询,语法如下: select 字段 | 数值 between 最小值 and 最大值; 1.查询工资在 1500 ~ 3000 的所…

【Qt】信号和槽

需要云服务器等云产品来学习Linux的同学可以移步/-->腾讯云<--/-->阿里云<--/-->华为云<--/官网&#xff0c;轻量型云服务器低至112元/年&#xff0c;新用户首次下单享超低折扣。 目录 一、Qt中的信号和槽 1、信号 2、槽 3、Q_OBJECT 二、Qt中的connect函…

【Go面试向】实现map稳定的有序遍历的方式

问题 大家好 我是寸铁&#x1f44a; 总结了一篇实现map稳定的有序遍历的方式探讨的文章✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 你对 map 了解多少&#xff1f;如果要实现第一个稳定的有序遍历有哪些方式&#xff1f; 回答 你对 map 了解多少&#xff1f; 我对map有一定的…

RHCSA上课笔记(前半部分)

第一部分 网络服务 第一章 例行性工作 1.单一执行的例行性工作 单一执行的例行性工作&#xff08;就像某一个时间点 的闹钟&#xff09;&#xff1a;仅处理执行一次 1.1 at命令&#xff1a;定时任务信息 [rhellocalhost ~]$ rpm -qa |grep -w at at-spi2-core-2.40.3-1.el9.x…

Qt文件和目录相关操作

1.相关说明 QCoreApplication类、QFile类、QDir、QTemporaryDir类、QTemporaryFile类、QFileSystemWatcher类的相关函数 2.相关界面 3.相关代码 #include "dialog.h" #include "ui_dialog.h" #include <QFileDialog> #include <QTemporaryDir>…

【JavaEE】网络原理:网络中的一些基本概念

目录 1. 网络通信基础 1.1 IP地址 1.2 端口号 1.3 认识协议 1.4 五元组 1.5 协议分层 什么是协议分层 分层的作用 OSI七层模型 TCP/IP五层&#xff08;或四层&#xff09;模型 网络设备所在分层 网络分层对应 封装和分用 1. 网络通信基础 1.1 IP地址 概念:IP地址…
最新文章