Android音频(7)——项目实战——耳麦插拔

7.4.3 声音路由切换实例分析 · 深入理解Android:卷1 · 看云

一、驱动程序上报耳麦拔插事件

1. 在有些Android版本中并不会在状态栏上显示耳麦图标。切换声道也不在系统中实现,而是在驱动中实现的。

2. headset headPhone lineOut
headset:既有听筒又有Mic
headPhone:只有听筒,没有Mic
lineOut: 就是输出模拟信号到音箱

驱动需要上报三种设备的拔插:headset、headPhone、lineOut。

3. 怎么上报:
(1) 输入子系统:可以上报按键事件也可以上报开关事件(EV_SW),事件类型包括headset、headPhone、lineOut。

(2) switch class子系统:通过uevent向用户空间发送数据,Android中有个线程专门监听这类事件。

具体使用哪一个要看Android源代码。

对于输入设备都需要指定能产生同步类事件EV_SYN. 对于按键事件,输入子系统还需要设置按键值的范围,但是对于开关类事件不需要设置。

使用switch dev子系统时,名字必须要设置为"h2w",Android系统监听/sys/class/switch/h2w这个虚拟设备。

4. 驱动Demo代码
由于耳塞的检查Codec给过来的硬件中断线没有接到Soc上所以使用软件模拟。

#include <linux/module.h>
#include <linux/types.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/err.h>
#include <linux/switch.h>
#include <linux/input.h>static struct input_dev *g_virtual_input;static ssize_t input_test_store(struct device *dev,struct device_attribute *attr,const char *buf, size_t count)
{long code;long val;char *endp;/* 如果字符串前面含有非数字, simple_strtol不能处理 */while ((*buf == ' ') || (*buf == '\t'))buf++;code = simple_strtol(buf, &endp, 0);/* 如果字符串前面含有非数字, simple_strtol不能处理 */while ((*endp == ' ') || (*endp == '\t'))endp++;val  = simple_strtol(endp, NULL, 0);printk("emulate to report EV_SW: 0x%x 0x%x\n", code, val);input_event(g_virtual_input, EV_SW, code, val);input_sync(g_virtual_input);return count;
}static DEVICE_ATTR(test_input, S_IRUGO | S_IWUSR, NULL, input_test_store);static int register_input_device_for_jack(void)
{int err;/* 分配input_dev */g_virtual_input = input_allocate_device();/* 设置 *//* 2.1 能产生哪类事件 */set_bit(EV_SYN, g_virtual_input->evbit);set_bit(EV_SW, g_virtual_input->evbit);/* 2.2 能产生这类事件中的哪些 *//* headset = 听筒 + MIC = SW_HEADPHONE_INSERT + SW_MICROPHONE_INSERT*    同时上报 SW_HEADPHONE_INSERT 和 SW_MICROPHONE_INSERT, 就表示headset*    为了简化, 对于android系统只上报SW_MICROPHONE_INSERT也表示headset*/set_bit(SW_HEADPHONE_INSERT, g_virtual_input->swbit);set_bit(SW_MICROPHONE_INSERT, g_virtual_input->swbit);set_bit(SW_LINEOUT_INSERT, g_virtual_input->swbit);/* 2.3 这些事件的范围 */g_virtual_input->name = "alsa_switch"; /* 不重要 *//* 注册 */err = input_register_device(g_virtual_input);if (err) {input_free_device(g_virtual_input);printk("input_register_device for virtual jack err!\n");return err;}/** sysfs文件创建方法:* 创建/sys/class/input/inputX/test_input文件*   可以执行类似下面的命令来模拟耳麦的动作:*       触发上报headset插入: echo 4 1 > /sys/class/input/inputX/test_input*       触发上报headset取下: echo 4 0 > /sys/class/input/inputX/test_input*       触发上报headphone插入: echo 2 1 > /sys/class/input/inputX/test_input*       触发上报headphonet取下: echo 2 0 > /sys/class/input/inputX/test_input*/err = device_create_file(&g_virtual_input->dev, &dev_attr_test_input);if (err) {printk("device_create_file for test_input err!\n");input_unregister_device(g_virtual_input);input_free_device(g_virtual_input);return err;}return 0;
}static void unregister_input_device_for_jack(void)
{device_remove_file(&g_virtual_input->dev, &dev_attr_test_input);input_unregister_device(g_virtual_input);input_free_device(g_virtual_input);
}/**************************************************************************************************************/static struct switch_dev g_virtual_switch;static ssize_t state_test_store(struct device *dev,struct device_attribute *attr,const char *buf, size_t count)
{long val;val = simple_strtol(buf, NULL, 0);printk("emulate to report swtich state: 0x%x\n", val);switch_set_state(&g_virtual_switch, val);return count;
}static DEVICE_ATTR(test_state, S_IRUGO | S_IWUSR, NULL, state_test_store);static int register_switch_device_for_jack(void)
{int err;g_virtual_switch.name = "h2w"; /*名字必须是这个,Android系统中使用它判断*/err = switch_dev_register(&g_virtual_switch);if (err) {printk("switch_dev_register h2w err!\n");return err;}/* 创建/sys/class/switch/h2w/test_state文件*   可以执行类似下面的命令来模拟耳麦的动作:*       触发上报headset插入: echo 1 > /sys/class/switch/h2w/test_state*       触发上报headset取下: echo 0 > /sys/class/switch/h2w/test_state*/err = device_create_file(g_virtual_switch.dev, &dev_attr_test_state);if (err) {printk("device_create_file test err!\n");switch_dev_unregister(&g_virtual_switch);return err;}return 0;
}static void unregister_switch_device_for_jack(void)
{device_remove_file(g_virtual_switch.dev, &dev_attr_test_state);switch_dev_unregister(&g_virtual_switch);
}/**************************************************************************************************************/static int __init virtual_jack_init(void)
{int err;err = register_input_device_for_jack();err = register_switch_device_for_jack();return 0;
}static void __exit virtual_jack_exit(void)
{unregister_input_device_for_jack();unregister_switch_device_for_jack();
}module_init(virtual_jack_init);
module_exit(virtual_jack_exit);MODULE_AUTHOR("weidongshan@qq.com");
MODULE_DESCRIPTION("Virutal jack driver for sound card");
MODULE_LICENSE("GPL");/*
对应的Makefile:
KERN_DIR = /media/ubuntu/works/tiny4412/linux-3.0.86obj-m    += virtual_jack.oall:make -C $(KERN_DIR) M=`pwd` modules clean:make -C $(KERN_DIR) M=`pwd` modules cleanrm -rf modules.order*/

二、在状态栏显示耳麦图标

1. 优秀相关博文:

Android4.4监听耳机插入处理方法:https://blog.csdn.net/anndy_peng/article/details/30240135
直接给出的是补丁和图片①,可直接参考它来实现再状态栏上添加耳塞图标的App:
https://github.com/fire855/android_frameworks_base-mtk/commit/7661c081b037a32e273afaf70349a6a1518dab48

[整理]Android屏幕适配(不同的屏幕分辨率和尺寸),选择mic的图标大小的时候会使用到:https://blog.csdn.net/ttkatrina/article/details/50623043

2. 参考①,tiny4412上操作步骤:

a. 确定在状态栏上图标的位置修改 frameworks/base/core/res/res/values/config.xml添加一行:<item><xliff:g id="id">headset</xliff:g></item>b. 创建图标文件从①中下载对应图片保存在如下位置frameworks/base/packages/SystemUI/res/drawable-hdpi/stat_sys_headset_with_mic.pngframeworks/base/packages/SystemUI/res/drawable-hdpi/stat_sys_headset_without_mic.pngc. 修改源码, 当接收到消息时显示/清除图标 : frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java参考①中的修改。d. 编译源文件
d.1 mmm frameworks/base/core/res    // 编译config.xml得到  out/target/product/tiny4412/system/framework/framework-res.apk
d.2 mmm frameworks/base/packages/SystemUI   // 编译图标文件, 编译源码得到 out/target/product/tiny4412/system/priv-app/SystemUI/SystemUI.apke. 替换单板上的文件adb push到 /system/framework/framework-res.apkadb push到 /system/priv-app/SystemUI/SystemUI.apk f. 测试使用5.1所编译得到的zImage启动开发板,执行以下命令:触发上报headset插入:   echo 4 1 > /sys/class/input/input0/test_input  #4=SW_MICROPHONE_INSERT,1=PlugIn触发上报headset取下:   echo 4 0 > /sys/class/input/input0/test_input  触发上报headphone插入: echo 2 1 > /sys/class/input/input0/test_input  #2=SW_HEADPHONE_INSERT,1=PlugIn触发上报headphone取下: echo 2 0 > /sys/class/input/input0/test_input  触发上报lineout插入:   echo 6 1 > /sys/class/input/input0/test_input  #6=SW_LINEOUT_INSERT,1=PlugIn触发上报lineout取下:   echo 6 0 > /sys/class/input/input0/test_input  测试结果:input的测试成功。但是uevent的失败,原因见下文

三、耳麦拔插事件调用流程分析

1. Android系统使用input子系统还是使用Switch class(uevent)上报拔插操作,取决于 config_useDevInputEventForAudioJack 配置值,
该值为true时使用input子系统, 为false时使用uevent机制, 该值在下述文件中定义, 后一个文件会覆盖前一个文件,目前在4412的配置文件
中选择的是使用输入子系统的。
frameworks/base/core/res/res/values/config.xml
device/friendly-arm/tiny4412/overlay/frameworks/base/core/res/res/values/config.xml

mUseDevInputEventForAudioJack的赋值:
InputManagerService(Context context) //InputManagerService.javamUseDevInputEventForAudioJack = context.getResources().getBoolean(R.bool.config_useDevInputEventForAudioJack);

2. 输入子系统对耳麦插拔事件上报流程

InputReader::loopOnce() //InputReader.cppmEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);processEventsLocked(mEventBuffer, count);InputReader::processEventsForDeviceLockeddevice->process(rawEvents, count); //输入子系统中为每一个/dev/input/eventX都创建一个InputDevicemapper->process(rawEvent); //对于开关事件,这里的InputMapper mapper是SwitchInputMapperSwitchInputMapper::process(const RawEvent* rawEvent)case EV_SW: processSwitch(rawEvent->code, rawEvent->value); //只是简单记录下来按键值case EV_SYN: sync(rawEvent->when); //收到sync(type=EV_SYN,code=SYN_REPORT)后处理//获取监听器,调用监听器的notifySwitch,这里的监听器是InputDispatchgetListener()->notifySwitch(&args);InputDispatcher::notifySwitch(const NotifySwitchArgs* args) //InputDispatcher.cppmPolicy->notifySwitch(args->eventTime, args->switchValues, args->switchMask, policyFlags);//上面调用的就是com_android_server_input_InputManagerService.cpp中的下面这个函数NativeInputManager::notifySwitch(nsecs_t when, uint32_t switchValues, uint32_t switchMask, uint32_t policyFlags) //com_android_server_input_InputManagerService.cpp//调用java的同名函数notifySwitch,位于InputManagerService.java中env->CallVoidMethod(mServiceObj, gServiceClassInfo.notifySwitch, when, switchValues, switchMask);notifySwitch(long whenNanos, int switchValues, int switchMask) //InputManagerService.java//调用到mWiredAccessoryCallbacks中的notifyWiredAccessoryChangedif (mUseDevInputEventForAudioJack) //这个就是来自上面的config.xml文件配置,为true才会选择输入子系统mWiredAccessoryCallbacks.notifyWiredAccessoryChanged(whenNanos, switchValues, switchMask);mWiredAccessoryCallbacks是何时注册的:
startOtherServices() //SystemServer.java//WiredAccessoryManager作为一个callback传给了inputManagerinputManager.setWiredAccessoryCallbacks(new WiredAccessoryManager(context, inputManager));mWiredAccessoryCallbacks = callbacks;因此上面调用的就是
WiredAccessoryManager.notifyWiredAccessoryChanged
mSwitchValues = (mSwitchValues & ~switchMask) | switchValues;//根据上报的值确定变量headset的取值,显示那个图标由变量headset决定的switch (mSwitchValues & (SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT | SW_LINEOUT_INSERT_BIT)) {case 0: headset = 0; break;case SW_HEADPHONE_INSERT_BIT: headset = BIT_HEADSET_NO_MIC; break;case SW_LINEOUT_INSERT_BIT: headset = BIT_LINEOUT; break;case SW_HEADPHONE_INSERT_BIT | SW_MICROPHONE_INSERT_BIT: headset = BIT_HEADSET; break;case SW_MICROPHONE_INSERT_BIT: headset = BIT_HEADSET; break;default: headset = 0; break;}//WiredAccessoryManager的函数,这个函数将会进入Audio系统,uevent分支最终也是调用这个函数。updateLocked(NAME_H2W, (mHeadsetState & ~(BIT_HEADSET | BIT_HEADSET_NO_MIC | BIT_LINEOUT)) | headset);

3. Switch class使用uevent对耳麦插拔事件上报流程

有一个UeventThread线程,循环读取socket的uevent事件并且发送uevent.

(1)初始化:
WiredAccessoryManager //WiredAccessoryManager.java//创建一个观察者mObserver = new WiredAccessoryObserver();//WiredAccessoryObserver构造函数调用WiredAccessoryObserver() //创建一个观察者事件的列表mUEventInfo = makeObservedUEventList();//当上面的config.xml中配置这个变量为false时就使用uevent机制if (!mUseDevInputEventForAudioJack) {//用于监听名为"h2w"这个Switch class驱动中创建的虚拟文件//shell@tiny4412:/sys # find ./ -name h2w                                        //./devices/virtual/switch/h2w//./class/switch/h2w//这里只监听这三个事件,所以驱动程序通过uevent上报事件也只能上报这三个事件之一uei = new UEventInfo("h2w", BIT_HEADSET, BIT_HEADSET_NO_MIC, BIT_LINEOUT);init() //WiredAccessoryManager.javastartObserving("DEVPATH="+uei.getDevPath());    //往UEventThread里面添加观察者UEventThread t = getThread();//这里的this就是子类WiredAccessoryObservert.addObserver(match, this);    //①(2)从native代码中获取uevent事件
run()//本地初始化nativeSetup();uevent_init() //android_os_UEventObserver.cpps = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT); //hardware/uevent.csetsockopt(s, SOL_SOCKET, SO_RCVBUFFORCE, &sz, sizeof(sz));bind(s, (struct sockaddr *) &addr, sizeof(addr))//等待下一个事件nativeWaitForNextEvent(); //UEventObserver.javauevent_next_event(char* buffer, int buffer_length) //hardware/uevent.cpoll(&fds, 1, -1);recv(fd, buffer, buffer_length, 0);//读取到数据后发送sendEvent(message);observer.onUEvent(event);//取出上面①中设置的观察者UEventObserver observer = mTempObserversToSignal.get(i);observer.onUEvent(UEventObserver.UEvent event) //WiredAccessoryManager.javaString devPath = event.get("DEVPATH");String name = event.get("SWITCH_NAME");int state = Integer.parseInt(event.get("SWITCH_STATE"));updateStateLocked(devPath, name, state);//WiredAccessoryManager的函数,两条分支(input和Switch class)在这里汇合了updateLocked(String newName, int newState)

4. uevent实现和input实现在这里汇合位置

updateLocked(String newName, int newState) //WiredAccessoryManager.java//获得message,然后发送messageMessage msg = mHandler.obtainMessage(MSG_NEW_DEVICE_STATE, headsetState, mHeadsetState, newName);mHandler.sendMessage(msg);上面发送消息会触发消息的handler被调用:
handleMessage(Message msg) {
case MSG_NEW_DEVICE_STATE:setDevicesState(msg.arg1, msg.arg2, (String)msg.obj);setDeviceStateLocked(curHeadset, headsetState, prevHeadsetState, headsetName);//终于进入了Audio系统了,设置有线设备的连接状态mAudioManager.setWiredDeviceConnectionState(inDevice, state, headsetName);IAudioService service = getService(); //AudioManager.javaservice.setWiredDeviceConnectionState(device, state, name);//指定一个延时时间后又把消息放到一个消息队列中delay = checkSendBecomingNoisyIntent(device, state);queueMsgUnderWakeLock(mAudioHandler, MSG_SET_WIRED_DEVICE_CONNECTION_STATE, delay);//会触发消息的handler被调用handleMessage(Message msg) onSetWiredDeviceConnectionState(msg.arg1, msg.arg2, (String)msg.obj);sendDeviceConnectionIntent(device, state, name);//构建一个Intent结构,然后向应用程序广播它,注册对这个Intent感兴趣的App就会收到它Intent intent = new Intent();intent.putExtra("state", state);intent.putExtra("name", name);    intent.setAction(Intent.ACTION_HEADSET_PLUG);                        ActivityManagerNative.broadcastStickyIntent(intent, null, UserHandle.USER_ALL);

5. App注册感兴趣的Intent:

//构造函数中添加
PhoneStatusBarPolicy(Context context, CastController cast) //PhoneStatusBarPolicy.javafilter.addAction(Intent.ACTION_HEADSET_PLUG);收到消息后onReceive被调用,updateHeadset是处理函数
onReceive(Context context, Intent intent)
if (action.equals(Intent.ACTION_HEADSET_PLUG))updateHeadset(intent);在状态栏中设置head set图标的状态
private final void updateHeadset(Intent intent) {final String action = intent.getAction();final int state = intent.getIntExtra("state", 4);final int mic = intent.getIntExtra("microphone", 4);switch (state) {case 0: //拔出mService.setIconVisibility("headset", false);break;case 1: //插入if (mic == 1) { //耳机上有mic显示这张图标mService.setIcon("headset", R.drawable.stat_sys_headset_with_mic, 0, null);} else {         //耳机上没有mic显示这张图标mService.setIcon("headset", R.drawable.stat_sys_headset_without_mic, 0, null);}mService.setIconVisibility("headset", true);break;}
}

6. native函数有些是在framework中,有些是在system中,有些是在hardware中,有些是在external中。

四、切换声音通道流程

1. 切换声音通道流程
a.在驱动程序中切换:
比如: 插上耳麦发生中断, 在中断处理程序中设置声卡让声音从耳机中输出
b.把输出通道的选择权交给android系统
目前这是主流的做法,驱动应该提供切换的能力,但是不应该替App进行决策。此处讲解此种方法。

3. 有个查看Android源码非常方便的网站:androidxref.com/5.0.0_r2/
输入audio_policy.conf,选择右边的"select all", 点search

一个完整复杂的audio_policy.conf的例子:http://androidxref.com/5.0.0_r2/xref/device/asus/grouper/audio_policy.conf

4. App只指定stream type, 从哪个声卡播放出去那是AudioFlinger的内容。

5. output支持哪些device在策略配置文件/system/etc/audio_policy.conf(/etc下也有?)中就指定了。

6. 对应关系:
一个output对应一个播放线程,也对应一个声卡,可能对应多个device; 一个device对应一个播放设备,比如喇叭或者耳机.

7. 怎样切换通道:

主要涉及的文件是AudioPolicyManager.cpp,参考0006_handleDeviceConnection UML一旦插上USB声卡,就会创建一个对应的播放线程驱动程序上报音频拔插事件, 该事件为某个device插入或拔出, Android系统需要切换声音通道。过程为(核心文件为frameworks/av/services/audiopolicy/AudioPolicyManager.cpp): (核心函数为 setDeviceConnectionState)
a. checkOutputsForDevice
针对该device, 打开新的output, 创建新的playbackthread.
方法:
从audio_policy.conf中确定"本该有多少个output"可以支持它, mOutputs表示"已经打开的output", 两者对比即可确定"尚未打开的output"b. checkOutputForAllStrategies / checkOutputForStrategy
对所有的strategy分组声音, 判断是否需要迁移到新的output, 如果需要则迁移对应Track到新的output,方法:
b.1 判断是否需要迁移
对于该strategy, 得到它的oldDevice, 进而得到它的outputs (srcOutputs);
对于该strategy, 得到它的newDevice, 进而得到它的outputs (dstOutputs);
如果这2个srcOutputs、dstOutputs不相同, 表示需要迁移b.2 如果迁移:
把对应的Track设置为invalidate状态即可, App写AudioTrack时发现它是invalidate状态, 就会重新创建新的Trackaudio_devices_t oldDevice = getDeviceForStrategy(strategy, true /*fromCache*/);audio_devices_t newDevice = getDeviceForStrategy(strategy, false /*fromCache*/);SortedVector<audio_io_handle_t> srcOutputs = getOutputsForDevice(oldDevice, mPreviousOutputs);SortedVector<audio_io_handle_t> dstOutputs = getOutputsForDevice(newDevice, mOutputs);c. getNewOutputDevice/setOutputDevice
这要操作HAL

8. output的参数信息会构成一个profile结构描述

audio_hw_modules {primary {outputs {primary {    //这些参数信息会构造成一个profile

【转载】Android音频(7)——项目实战——耳麦插拔相关推荐

  1. Android游戏开发项目实战

    Android游戏开发项目实战: 手机游戏开发工程师培训教程(Android4.3.Cocos2d-x.Untity2D/3D.跨平台引擎技术) 课程讲师:厉风行 课程分类:游戏开发 适合人群:初级 ...

  2. Android Compose Bloom 项目实战 (一) : 项目说明与配置

    1. 项目介绍 Bloom是谷歌 AndroidDevChallenge (Android 开发挑战赛) 中的一期活动,目的是为了推广Compose,非常适合用来练手,通过这个项目,我们可以很好的入门 ...

  3. C# Xamarin For Android移动开发项目实战篇

    一.课程介绍 在前面阿笨的<C# Xamarin移动开发基础进修篇>课程中,大家已经熟悉和了解了Xamarin移动App开发的基础知识和原理.本次分享课<C# Xamarin移动开发 ...

  4. android u盘挂载监听,Android SD卡及U盘插拔状态监听及内容读取

    本篇是通过系统方法来对sd卡及U盘插拔监听及数据获取,Android盒子端开发,有系统权限,当然,这个比较简单,知道具体方法,可以通过反射来实现. 先贴上效果图: 获取外置存储设备并监听插拔状态 获取 ...

  5. Android SD卡及U盘插拔状态监听和内容读取

    本篇是通过系统方法来对sd卡及U盘插拔监听及数据获取,Android盒子端开发,有系统权限,当然,这个比较简单,知道具体方法,可以通过反射来实现. 先贴上效果图: 获取外置存储设备并监听插拔状态 获取 ...

  6. android游戏开发项目实战——数独

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 一.程序 ...

  7. Android手机应用商城项目,Android手机助手项目实战:从0开发一款自己的应用商店...

    |- 资源.rar – 13.00 kB |- 直播-课时17-直播发起-定位实现.avi – 220.60 MB |- 手机助手课时57-主题详情.avi – 558.30 MB |- 手机助手课时 ...

  8. 【Android入门到项目实战-- 10.1】—— jsoup的详细使用教程

    目录 一.jsoup介绍 二.jsoup的使用 1.导入依赖 2.建立连接 3.获取数据 获取各节点的方法 1)获取文字 2)获取图片 一.jsoup介绍 Jsoup是一款Java的html解析工具. ...

  9. android简单app实例_Android安卓小项目实战视频教程集锦

    Android安卓小项目实战视频教程,点击进入视频教程: 一.安卓项目视频教程: 1蓝牙聊天APP介绍-分步骤介绍一个简单安卓蓝牙APP的开发过程 - 西瓜视频 2蓝牙聊天开发流程-分步骤介绍一个简单 ...

最新文章

  1. NLP数据分词小整理
  2. win32 实现死锁的小例子
  3. xsd java引用_web.xml文件的 xsd引用(或dtd引用)学习
  4. GPIO几种配置的作用
  5. Yet another nio framework for java
  6. 天宫初级认证答案_百度初级认证试题答案
  7. mysql数据库管理命令_MySQL数据库管理基本命令
  8. 浅析 Python 的类、继承和多态
  9. DeepLearning - Forard Backward Propogation
  10. NameError: name 'reload' is not defined等python版本问题解决方案
  11. Pr常见问题,如何找回不小心删除的视频音频
  12. 计算机win7卡顿如何解决方法,win7系统运行卡顿的解决方法
  13. CAD中把样条曲线改成多线段
  14. 整合X-Admin前端框架改造ABP
  15. 2022-2027年中国化纤行业市场调研及未来发展趋势预测报告
  16. 计算机学报——主题“区块链”,检索到25篇
  17. 安卓机更新系统会卡吗_手机经常提示系统升级,到底要不要升级,看完你就明白了!...
  18. 娶一位俄罗斯姑娘当老婆是一种什么样的感觉?
  19. WIFI基础入门--802.11--MAC基础--2
  20. 那些年踩过的坑希望你们不要踩!

热门文章

  1. 用三元组存储稀疏矩阵并实现转置
  2. matlab如何根据历年gdp找增长规律,请问如何根据中国统计年鉴数据计算历年的GDP平减指数?...
  3. 第十六篇玩转【斗鱼直播APP】系列之内容简介
  4. 数组的indexOf 方法
  5. 第四课: 易语言大漠剑侠情缘多线程木人巷主线程
  6. Archlinux YouCompleteMe+syntastic vim自动补全插件,显示缩进和状态栏美化,爽心悦目的vim
  7. 使用ArcGIS Portal10.5直接发布三维3D服务及文件支持哪些格式?
  8. CC00046.hadoop——|HadoopMapReduce.V19|——|Hadoop.v19|MapReduce数据压缩机制|
  9. 老机型MacAir安装win10踩坑填坑记录
  10. 昆明视频监控建设行之有效 明年底实现百分百覆盖