开发过程中录音和播放这块碰到了一些问题,麻烦的主要有三个:

  1. 检测是否有声音输入设备
  2. 当有多个声音输出设备时,指定声音输出设备
  3. 检测耳机的插入和拔出

第一个问题,对于iTouch和iPad等本身不带麦克风的设备,需要检查是否插入了带录音功能的耳机;对于iphone,由于其本身已近自带麦克风,所以相对容易。第二个问题,当在本身带有外放的设备上插入耳机等输出设备时,就出现了多个输出设备,需要实现在程序中指定将声音输出到哪里。第三个问题,插入/拔出耳机必然引起声音输出设备的变化,而如果是在iTouch和iPad上插入/拔出了带麦克风的耳机,则必然引起声音输入设备的变化。

1. 检测声音输入设备

[plain]  view plain copy print ?
  1. - (BOOL)hasMicphone {
  2. return [[AVAudioSession sharedInstance] inputIsAvailable];
  3. }

2. 检测声音输出设备
对于输出设备的检测,我们只考虑了2个情况,一种是设备自身的外放(iTouch/iPad/iPhone都有),一种是当前是否插入了带外放的耳机。iOS已经提供了相关方法用于获取当前的所有声音设备,我们只需要检查在这些设备中是否存在我们所关注的那几个就可以了。
获取当前所有声音设备:

[plain]  view plain copy print ?
  1. CFStringRef route;
  2. UInt32 propertySize = sizeof(CFStringRef);
  3. AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &propertySize, &route);

在iOS上所有可能的声音设备包括:

[cpp]  view plain copy print ?
  1. /* Known values of route:
  2. * "Headset"
  3. * "Headphone"
  4. * "Speaker"
  5. * "SpeakerAndMicrophone"
  6. * "HeadphonesAndMicrophone"
  7. * "HeadsetInOut"
  8. * "ReceiverAndMicrophone"
  9. * "Lineout"
  10. */

每一项的具体代表的设备请查考iOS文档,此处我们关注的是是否有耳机,所以只需要检查在route中是否有Headphone或Headset存在,具体方法如下:

[plain]  view plain copy print ?
  1. - (BOOL)hasHeadset {
  2. #if TARGET_IPHONE_SIMULATOR
  3. #warning *** Simulator mode: audio session code works only on a device
  4. return NO;
  5. #else
  6. CFStringRef route;
  7. UInt32 propertySize = sizeof(CFStringRef);
  8. AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &propertySize, &route);
  9. if((route == NULL) || (CFStringGetLength(route) == 0)){
  10. // Silent Mode
  11. NSLog(@"AudioRoute: SILENT, do nothing!");
  12. } else {
  13. NSString* routeStr = (NSString*)route;
  14. NSLog(@"AudioRoute: %@", routeStr);
  15. /* Known values of route:
  16. * "Headset"
  17. * "Headphone"
  18. * "Speaker"
  19. * "SpeakerAndMicrophone"
  20. * "HeadphonesAndMicrophone"
  21. * "HeadsetInOut"
  22. * "ReceiverAndMicrophone"
  23. * "Lineout"
  24. */
  25. NSRange headphoneRange = [routeStr rangeOfString : @"Headphone"];
  26. NSRange headsetRange = [routeStr rangeOfString : @"Headset"];
  27. if (headphoneRange.location != NSNotFound) {
  28. return YES;
  29. } else if(headsetRange.location != NSNotFound) {
  30. return YES;
  31. }
  32. }
  33. return NO;
  34. #endif
  35. }

请注意,由于获取AudioRoute的相关方法不能再simulator上运行(会直接crush),所以必须先行处理。

3. 设置声音输出设备
在我们的项目中,存在当正在播放时用户会插入或拔出耳机的情况。如果是播放时用户插入了耳机,苹果会自动将声音输出指向到耳机并自动将音量调整为合适大小;如果是在用耳机的播放过程中用户拔出了耳机,声音会自动从设备自身的外放里面播出,但是其音量并不会自动调大。
经过我们的测试,我们发现当播放时拔出耳机会有两个问题(也许对你来说不是问题,但是会影响我们的app):
音乐播放自动停止
声音音量大小不会自动变大,系统仍然以较小的声音(在耳机上合适的声音)来进行外放
对于第一个问题,实际上就是需要能够检测到耳机拔出的事件;而第二个问题则是需要当耳机拔出时强制设置系统输出设备修改为系统外放。
强制修改系统声音输出设备:

[plain]  view plain copy print ?
  1. - (void)resetOutputTarget {
  2. BOOL hasHeadset = [self hasHeadset];
  3. NSLog (@"Will Set output target is_headset = %@ .", hasHeadset ? @"YES" : @"NO");
  4. UInt32 audioRouteOverride = hasHeadset ?
  5. kAudioSessionOverrideAudioRoute_None:kAudioSessionOverrideAudioRoute_Speaker;
  6. AudioSessionSetProperty(kAudioSessionProperty_OverrideAudioRoute, sizeof(audioRouteOverride), &audioRouteOverride);
  7. }

可以看到我们修改了AudioSession的属性“kAudioSessionProperty_OverrideAudioRoute”,该属性在iOS文档上的解释如下:
kAudioSessionProperty_OverrideAudioRoute  Specifies whether or not to override the audio session category’s normal audio route. Can be set with one of two values: kAudioSessionOverrideAudioRoute_None, which specifies that you want to use the normal audio route; and kAudioSessionOverrideAudioRoute_Speaker, when sends output audio to the speaker. A write-only UInt32 value.

Upon an audio route change (such as by plugging in or unplugging a headset), or upon interruption, this property reverts to its default value. This property can be used only with the kAudioSessionCategory_PlayAndRecord (or the equivalent AVAudioSessionCategoryRecord) category.

可以看到,该属性只有当category为kAudioSessionCategory_PlayAndRecord或者AVAudioSessionCategoryRecord时才能使用。所以我们还需要能够设置AudioSession的category。
4. 设置Audio工作模式(category,我当做工作模式理解的)
iOS系统中Audio支持多种工作模式(category),要实现某个功能,必须首先将AudioSession设置到支持该功能的工作模式下。所有支持的工作模式如下:

[java]  view plain copy print ?
  1. Audio Session Categories
  2. Category identifiers for audio sessions, used as values for the setCategory:error: method.
  3. NSString *const AVAudioSessionCategoryAmbient;
  4. NSString *const AVAudioSessionCategorySoloAmbient;
  5. NSString *const AVAudioSessionCategoryPlayback;
  6. NSString *const AVAudioSessionCategoryRecord;
  7. NSString *const AVAudioSessionCategoryPlayAndRecord;
  8. NSString *const AVAudioSessionCategoryAudioProcessing;

具体每一个category的功能请参考iOS文档,其中AVAudioSessionCategoryRecord为独立录音模式,而AVAudioSessionCategoryPlayAndRecord为支持录音盒播放的模式,而AVAudioSessionCategoryPlayback为普通播放模式。
设置category:

[java]  view plain copy print ?
  1. - (BOOL)checkAndPrepareCategoryForRecording {
  2. recording = YES;
  3. BOOL hasMicphone = [self hasMicphone];
  4. NSLog(@"Will Set category for recording! hasMicophone = %@", hasMicphone?@"YES":@"NO");
  5. if (hasMicphone) {
  6. [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord
  7. error:nil];
  8. }
  9. [self resetOutputTarget];
  10. return hasMicphone;
  11. }
  12. - (void)resetCategory {
  13. if (!recording) {
  14. NSLog(@"Will Set category to static value = AVAudioSessionCategoryPlayback!");
  15. [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback
  16. error:nil];
  17. }
  18. }

5. 检测耳机插入/拔出事件
耳机插入拔出事件是通过监听AudioSession的RouteChange事件然后判断耳机状态实现的。实现步骤分为两步,首先注册监听函数,然后再监听函数中判断耳机状态。
注册监听函数:

[java]  view plain copy print ?
  1. AudioSessionAddPropertyListener (kAudioSessionProperty_AudioRouteChange,
  2. audioRouteChangeListenerCallback,
  3. self);

我们的需求是当耳机被插入或拔出时做出响应,而产生AouteChange事件的原因有多种,所以需要对各种类型进行处理并结合当前耳机状态进行判断。在iOS文档中,产生AouteChange事件的原因有如下几种:

[java]  view plain copy print ?
  1. Audio Session Route Change Reasons
  2. Identifiers for the various reasons that an audio route can change while your iOS application is running.
  3. enum {
  4. kAudioSessionRouteChangeReason_Unknown                    = 0,
  5. kAudioSessionRouteChangeReason_NewDeviceAvailable         = 1,
  6. kAudioSessionRouteChangeReason_OldDeviceUnavailable       = 2,
  7. kAudioSessionRouteChangeReason_CategoryChange             = 3,
  8. kAudioSessionRouteChangeReason_Override                   = 4,
  9. // this enum has no constant with a value of 5
  10. kAudioSessionRouteChangeReason_WakeFromSleep              = 6,
  11. kAudioSessionRouteChangeReason_NoSuitableRouteForCategory = 7
  12. };

具体每个类型的含义请查阅iOS文档,其中我们关注的是kAudioSessionRouteChangeReason_NewDeviceAvailable有新设备插入、kAudioSessionRouteChangeReason_OldDeviceUnavailable原有设备被拔出以及kAudioSessionRouteChangeReason_NoSuitableRouteForCategory当前工作模式缺少合适设备。
当有新设备接入时,如果检测到耳机,则判定为耳机插入事件;当原有设备移除时,如果无法检测到耳机,则判定为耳机拔出事件;当出现“当前工作模式缺少合适设备时”,直接判定为录音时拔出了麦克风。
很明显,这个判定逻辑实际上不准确,比如原来就有耳机但是插入了一个新的audio设备或者是原来就没有耳机但是拔出了一个原有的audio设备,我们的判定都会出错。但是对于我们的项目来说,其实关注的不是耳机是拔出还是插入,真正关注的是有audio设备插入/拔出时能够根据当前耳机/麦克风状态去调整设置,所以这个判定实现对我们来说是正确的。
监听函数的实现:

[plain]  view plain copy print ?
  1. void audioRouteChangeListenerCallback (
  2. void                      *inUserData,
  3. AudioSessionPropertyID    inPropertyID,
  4. UInt32                    inPropertyValueSize,
  5. const void                *inPropertyValue
  6. ) {
  7. if (inPropertyID != kAudioSessionProperty_AudioRouteChange) return;
  8. // Determines the reason for the route change, to ensure that it is not
  9. //        because of a category change.
  10. CFDictionaryRef    routeChangeDictionary = inPropertyValue;
  11. CFNumberRef routeChangeReasonRef =
  12. CFDictionaryGetValue (routeChangeDictionary,
  13. CFSTR (kAudioSession_AudioRouteChangeKey_Reason));
  14. SInt32 routeChangeReason;
  15. CFNumberGetValue (routeChangeReasonRef, kCFNumberSInt32Type, &routeChangeReason);
  16. NSLog(@" ======================= RouteChangeReason : %d", routeChangeReason);
  17. AudioHelper *_self = (AudioHelper *) inUserData;
  18. if (routeChangeReason == kAudioSessionRouteChangeReason_OldDeviceUnavailable) {
  19. [_self resetSettings];
  20. if (![_self hasHeadset]) {
  21. [[NSNotificationCenter defaultCenter] postNotificationName:@"ununpluggingHeadse
  22. object:nil];
  23. }
  24. } else if (routeChangeReason == kAudioSessionRouteChangeReason_NewDeviceAvailable) {
  25. [_self resetSettings];
  26. if (![_self hasMicphone]) {
  27. [[NSNotificationCenter defaultCenter] postNotificationName:@"pluggInMicrophone"
  28. object:nil];
  29. }
  30. } else if (routeChangeReason == kAudioSessionRouteChangeReason_NoSuitableRouteForCategory) {
  31. [_self resetSettings];
  32. [[NSNotificationCenter defaultCenter] postNotificationName:@"lostMicroPhone"
  33. object:nil];
  34. }
  35. //else if (routeChangeReason == kAudioSessionRouteChangeReason_CategoryChange  ) {
  36. //    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
  37. //}
  38. [_self printCurrentCategory];
  39. }

当检测到相关事件后,通过NSNotificationCenter通知observers耳机(有无麦克风)拔出/插入事件拔出事件,从而触发相关操作。
6. 事件处理
对于耳机(有无麦克风)拔出/插入事件,一般需要做如下处理:
强制重设系统声音输出设备(防止系统以较小声音在外放中播放)
如果拔出前正在播放,则启动已经暂停的播放(当耳机拔出时,系统会自动暂停播放)
当拔出前正在录音,则需要检查麦克风情况并决定是否停止录音(如果录音时从iTouch/iPad等设备上拔出了带麦克风的耳机)

完整代码

AudioHelper.h

[java]  view plain copy print ?
  1. #import <Foundation/Foundation.h>
  2. @interface AudioHelper : NSObject {
  3. BOOL recording;
  4. }
  5. - (void)initSession;
  6. - (BOOL)hasHeadset;
  7. - (BOOL)hasMicphone;
  8. - (void)cleanUpForEndRecording;
  9. - (BOOL)checkAndPrepareCategoryForRecording;
  10. @end

AudioHelper.m

[plain]  view plain copy print ?
  1. #import "AudioHelper.h"
  2. #import <AVFoundation/AVFoundation.h>
  3. #import <AudioToolbox/AudioToolbox.h>
  4. @implementation AudioHelper
  5. - (BOOL)hasMicphone {
  6. return [[AVAudioSession sharedInstance] inputIsAvailable];
  7. }
  8. - (BOOL)hasHeadset {
  9. #if TARGET_IPHONE_SIMULATOR
  10. #warning *** Simulator mode: audio session code works only on a device
  11. return NO;
  12. #else
  13. CFStringRef route;
  14. UInt32 propertySize = sizeof(CFStringRef);
  15. AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &propertySize, ute);
  16. if((route == NULL) || (CFStringGetLength(route) == 0)){
  17. // Silent Mode
  18. NSLog(@"AudioRoute: SILENT, do nothing!");
  19. } else {
  20. NSString* routeStr = (NSString*)route;
  21. NSLog(@"AudioRoute: %@", routeStr);
  22. /* Known values of route:
  23. * "Headset"
  24. * "Headphone"
  25. * "Speaker"
  26. * "SpeakerAndMicrophone"
  27. * "HeadphonesAndMicrophone"
  28. * "HeadsetInOut"
  29. * "ReceiverAndMicrophone"
  30. * "Lineout"
  31. */
  32. NSRange headphoneRange = [routeStr rangeOfString : @"Headphone"];
  33. NSRange headsetRange = [routeStr rangeOfString : @"Headset"];
  34. if (headphoneRange.location != NSNotFound) {
  35. return YES;
  36. } else if(headsetRange.location != NSNotFound) {
  37. return YES;
  38. }
  39. }
  40. return NO;
  41. #endif
  42. }
  43. - (void)resetOutputTarget {
  44. BOOL hasHeadset = [self hasHeadset];
  45. NSLog (@"Will Set output target is_headset = %@ .", hasHeadset ? @"YES" : @"NO");
  46. UInt32 audioRouteOverride = hasHeadset ?
  47. kAudioSessionOverrideAudioRoute_None:kAudioSessionOverrideAudioRoute_Sper;
  48. AudioSessionSetProperty(kAudioSessionProperty_OverrideAudioRoute, sizeof(audioRouteOverride), &audioRouteOverride);
  49. [self hasHeadset];
  50. }
  51. - (BOOL)checkAndPrepareCategoryForRecording {
  52. recording = YES;
  53. BOOL hasMicphone = [self hasMicphone];
  54. NSLog(@"Will Set category for recording! hasMicophone = %@", Micphone?@"YES":@"NO");
  55. if (hasMicphone) {
  56. [[AVAudioSession sharedInstance] Category:AVAudioSessionCategoryPlayAndRecord
  57. error:nil];
  58. }
  59. [self resetOutputTarget];
  60. return hasMicphone;
  61. }
  62. - (void)resetCategory {
  63. if (!recording) {
  64. NSLog(@"Will Set category to static value = udioSessionCategoryPlayback!");
  65. [[AVAudioSession sharedInstance] Category:AVAudioSessionCategoryPlayback
  66. error:nil];
  67. }
  68. }
  69. - (void)resetSettings {
  70. [self resetOutputTarget];
  71. [self resetCategory];
  72. BOOL isSucced = [[AVAudioSession sharedInstance] setActive: YES error:NULL];
  73. if (!isSucced) {
  74. NSLog(@"Reset audio session settings failed!");
  75. }
  76. }
  77. - (void)cleanUpForEndRecording {
  78. recording = NO;
  79. [self resetSettings];
  80. }
  81. - (void)printCurrentCategory {
  82. return;
  83. UInt32 audioCategory;
  84. UInt32 size = sizeof(audioCategory);
  85. AudioSessionGetProperty(kAudioSessionProperty_AudioCategory, &size, dioCategory);
  86. if ( audioCategory == kAudioSessionCategory_UserInterfaceSoundEffects ){
  87. NSLog(@"current category is : dioSessionCategory_UserInterfaceSoundEffects");
  88. } else if ( audioCategory == kAudioSessionCategory_AmbientSound ){
  89. NSLog(@"current category is : kAudioSessionCategory_AmbientSound");
  90. } else if ( audioCategory == kAudioSessionCategory_AmbientSound ){
  91. NSLog(@"current category is : kAudioSessionCategory_AmbientSound");
  92. } else if ( audioCategory == kAudioSessionCategory_SoloAmbientSound ){
  93. NSLog(@"current category is : kAudioSessionCategory_SoloAmbientSound");
  94. } else if ( audioCategory == kAudioSessionCategory_MediaPlayback ){
  95. NSLog(@"current category is : kAudioSessionCategory_MediaPlayback");
  96. } else if ( audioCategory == kAudioSessionCategory_LiveAudio ){
  97. NSLog(@"current category is : kAudioSessionCategory_LiveAudio");
  98. } else if ( audioCategory == kAudioSessionCategory_RecordAudio ){
  99. NSLog(@"current category is : kAudioSessionCategory_RecordAudio");
  100. } else if ( audioCategory == kAudioSessionCategory_PlayAndRecord ){
  101. NSLog(@"current category is : kAudioSessionCategory_PlayAndRecord");
  102. } else if ( audioCategory == kAudioSessionCategory_AudioProcessing ){
  103. NSLog(@"current category is : kAudioSessionCategory_AudioProcessing");
  104. } else {
  105. NSLog(@"current category is : unknow");
  106. }
  107. }
  108. void audioRouteChangeListenerCallback (
  109. void                      *inUserData,
  110. AudioSessionPropertyID    inPropertyID,
  111. UInt32                    inPropertyValueS,
  112. const void                *inPropertyValue
  113. ) {
  114. if (inPropertyID != kAudioSessionProperty_AudioRouteChange) return;
  115. // Determines the reason for the route change, to ensure that it is not
  116. //      because of a category change.
  117. CFDictionaryRef routeChangeDictionary = inPropertyValue;
  118. CFNumberRef routeChangeReasonRef =
  119. CFDictionaryGetValue (routeChangeDictionary,
  120. CFSTR (kAudioSession_AudioRouteChangeKey_Reason));
  121. SInt32 routeChangeReason;
  122. CFNumberGetValue (routeChangeReasonRef, kCFNumberSInt32Type, uteChangeReason);
  123. NSLog(@" ===================================== RouteChangeReason : %d", teChangeReason);
  124. AudioHelper *_self = (AudioHelper *) inUserData;
  125. if (routeChangeReason == kAudioSessionRouteChangeReason_OldDeviceUnavailable)
  126. [_self resetSettings];
  127. if (![_self hasHeadset]) {
  128. [[NSNotificationCenter defaultCenter] tNotificationName:@"ununpluggingHeadse"
  129. object:nil];
  130. }
  131. } else if (routeChangeReason == dioSessionRouteChangeReason_NewDeviceAvailable) {
  132. [_self resetSettings];
  133. if (![_self hasMicphone]) {
  134. [[NSNotificationCenter defaultCenter] tNotificationName:@"pluggInMicrophone"
  135. object:nil];
  136. }
  137. } else if (routeChangeReason == dioSessionRouteChangeReason_NoSuitableRouteForCategory) {
  138. [_self resetSettings];
  139. [[NSNotificationCenter defaultCenter] postNotificationName:@"lostMicroPhone"
  140. object:nil];
  141. }
  142. //else if (routeChangeReason == kAudioSessionRouteChangeReason_CategoryChange  )
  143. //    [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
  144. //}
  145. [_self printCurrentCategory];
  146. }
  147. - (void)initSession {
  148. recording = NO;
  149. AudioSessionInitialize(NULL, NULL, NULL, NULL);
  150. [self resetSettings];
  151. AudioSessionAddPropertyListener (kAudioSessionProperty_AudioRouteChange,
  152. audioRouteChangeListenerCallback,
  153. self);
  154. [self printCurrentCategory];
  155. [[AVAudioSession sharedInstance] setActive: YES error:NULL];
  156. }
  157. - (void)dealloc {
  158. [super dealloc];
  159. }
  160. @end

[iOS] iphone检测耳机插入/拔出相关推荐

  1. iphone检测耳机插入/拔出

    iphone检测耳机插入/拔出 开发过程中录音和播放这块碰到了一些问题,麻烦的主要有三个: 检测是否有声音输入设备 当有多个声音输出设备时,指定声音输出设备 检测耳机的插入和拔出 第一个问题,对于iT ...

  2. IOS成长之路-检测耳机插入/拔出

    导入苹果的两个框架是必不可少的环节... 代码部分+小解: [cpp] view plaincopy - (void)viewDidLoad { [super viewDidLoad]; // Do  ...

  3. Android P检测USB插入拔出消息并基于libaums实现读取USB文件

    Android设备中检测USB插入消息,并且从USB中读取文件. 一.导入libaums包 libaums开源项目地址:https://github.com/magnusja/libaums buil ...

  4. Android实现检测耳机插入和拔出

    在Android下实现检测耳机插入和拔出,也即建立一个Broadcast Receiver,监听"android.intent.action.HEADSET_PLUG"广播 但直接 ...

  5. 在.NET中探测U盘的插入/拔出

    当设备被插入/拔出的时候,WINDOWS会向每个窗体发送WM_DEVICECHANGE 消息,当消息的wParam 值等于 DBT_DEVICEARRIVAL 时,表示Media设备被插入并且已经可用 ...

  6. win10系统,主机箱的前置耳麦插孔用不了,“设置——声音”麦克风或者耳机已拔出未修复,“输入设备”无插座信息,更新驱动也无效,控制面板——小图标里查不到realtek高清晰音频管理器——一招解决

    win10系统,有一天忽然固态硬盘坏了,拆了重装系统之后主机箱的前置耳麦插孔里,插了耳机只能听到声音,麦克风无效,微信语音电话打不出去了. 查了一圈,"设置--声音"显示:麦克风或 ...

  7. linux udev 检测u盘的插入和拔出,在Linux中C检测插入/拔出USB串行设备

    我需要检测何时在我的嵌入式系统上插入或拔出USB串行设备,并知道与之相关的tty是什么. 我的系统运行在2.6 Linux内核上. 由于我没有对udev规则的写访问权限,现在我正在尝试从文件系统获取此 ...

  8. Android APP 检测和监听当前USB设备插入拔出以及读取VID/PID

    一.列出所有的usb device设备,打印vip pid private boolean AllDeviceConnected(){UsbManager manager = (UsbManager) ...

  9. Qt检测U盘插入拔出Demo

    要做这个,要先知道Qt的QAbstractNativeEventFilter类,虚函数nativeEventFilter.这个类的主要作用是捕获全局windows消息. 先看一下效果: 基本注意以下两 ...

最新文章

  1. 小程序:js获取验证码时(倒计时模块)
  2. [kuangbin带你飞]专题六-生成最小树
  3. 2021 倒计时,编程日历倒计时,但伟大与经典历久弥新
  4. JS重复引用也会导致错误
  5. Java面试题详解三:比较器
  6. 虚拟机实现二层交换机_局域网SDN技术硬核内幕 5 虚拟化网络的实现
  7. 【Java基础】容器
  8. Xianfeng轻量级Java中间件平台:用户管理
  9. 1027. 打印沙漏(20)
  10. 以太网帧格式、最少字节介绍(arp)
  11. 空间平面,空间直线及它们的方程
  12. 数据库开启了闪回和归档,关闭归档日志alter database noarchivelog的时候报错:ORA-38781: cannot disable media recovery
  13. 高斯滤波器原理及其实现
  14. 软件测试周刊(第21期):不要告诉我你想干什么
  15. 冒泡排序图解及代码实现
  16. Java工程师两周面试突击-中华石杉-Java面试
  17. 送书 | 聊聊阳光问政
  18. echarts设置tooltips样式以及调取数据
  19. android t渐变立体按钮,Android 多色渐变按钮
  20. statsby: 不用循环语句的循环

热门文章

  1. Linux内核参数调优以应对SYN攻击
  2. redis缓存的雪崩、击穿、穿透,淘汰策略,持久化
  3. 如何回答join到底释不释放锁?
  4. 全网最详细的一篇 SpringCloud 总结
  5. 不要抱怨电脑网速慢,只能怪自己不会调快网速
  6. 牛客白月赛26【题解】
  7. JavaScript跨域方法汇总
  8. Alpine Docker镜像字体问题解决
  9. Excel多条件查找之lookup
  10. 算作自我监督的第一篇博客