即时通讯

即时通信的要点就是消息内容不大,并且传输迅速,并且是即时到达,实时通知的
所以我们对语音进行一些处理,语音处理的过程如下:

  1. 录制录音
  2. 获取数据
  3. 编码保存
  4. 接收数据
  5. 数据解码
  6. 播放录音

为什么我们需要对数据进行编解码呢?原始的声音数据是非常大的,如果进行直接传输的话可能完全符合不了即时通讯的要求,所以我们要进行压缩。

所需要的API

  • 声音采集:MediaRecorder(直接录制成文件并且保存下来),AudioRecord(把声音的实时的字节数据返回)
  • 声音播放:MediaPlayer(基于声音文件播放的API),AudioTrack(基于字节数据播放的API)
  • 多线程:ExecutorService(因为对声音的处理比较耗时,所以我们不能在主线程进行处理,所以就需要多线程,有人可能会说why,因为主线程有个16ms的执行限制,一定要刷新的,所以不能执行I/O等耗时操作)

数据传输

  • 基于文件:HTTP文件上传下载(耗时)
  • 基于字节流:TCP/WebSocket(先比之下耗时稍微少一点)

语音录制

语音录制的方法上面也介绍过了,又两种方法,各有各的好处,一个是直接录制成文件存储,另一个是直接返回字节数据,其实区别是MediaRecorder录制的音频文件是经过压缩后的,需要设置编码器。并且录制的音频文件可以用系统自带的Music播放器播放;而AudioRecord录制的是PCM格式的音频文件,需要用AudioTrack来播放,AudioTrack更接近底层;在用MediaRecorder进行录制音视频时,最终还是会创建AudioRecord用来与AudioFlinger进行交互。下面我们来介绍下两种方法录制的方法是怎么写的:

MediaRecorder

我们着重介绍FileActivity类,而对于其中录制最重要的是doStart()以及doStop()方法,下面的代码有详细的介绍,我就不累述了,然后对于异常我们要进行抛出,并且对用户进行提示,因为提示总比直接闪退来的效果要好。

package com.xjh.gin.im;import android.graphics.Color;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;import java.io.File;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** Created by Gin on 2017/11/28.*/public class FileActivity extends AppCompatActivity {private TextView mTvLog, mTvPressToSay;private ExecutorService mExecutorService;private MediaRecorder mMediaRecorder;private File mAudioFile;private long mStartRecordTime, mStopRecordTime;private Handler mMainThreadHandler;@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_file);initView();initEvent();}@Overrideprotected void onDestroy() {super.onDestroy();//activity销毁时,停止后台任务,避免内存泄露mExecutorService.shutdownNow();releaseRecorder();}private void initView() {mTvLog = findViewById(R.id.mTvLog);mTvPressToSay = findViewById(R.id.mTvPressToSay);//录音的JNI函数不具备线程安全性,所以要用单线程mExecutorService = Executors.newSingleThreadExecutor();//主线程的HandlermMainThreadHandler = new Handler(Looper.getMainLooper());}private void initEvent() {//按下说话,释放发送,所以我们不能使用OnClickListener//用OnTouchListenermTvPressToSay.setOnTouchListener(new View.OnTouchListener() {@Overridepublic boolean onTouch(View v, MotionEvent event) {//根据不同的touch action做出相应的处理switch (event.getAction()) {case MotionEvent.ACTION_DOWN:startRecord();break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:stopRecord();break;}return true;}});}//开始录音private void startRecord() {mTvPressToSay.setText("正在说话...");mTvPressToSay.setBackgroundColor(Color.GRAY);//提交后台任务,执行录音逻辑mExecutorService.submit(new Runnable() {@Overridepublic void run() {//释放之前录音的 MediaRecorderreleaseRecorder();//执行录音逻辑,如果失败提示用户if (!doStart()) {recordFail();}}});}//结束录音private void stopRecord() {mTvPressToSay.setText("按住说话");mTvPressToSay.setBackgroundColor(Color.WHITE);//提交后台任务,执行停止逻辑mExecutorService.submit(new Runnable() {@Overridepublic void run() {//执行停止录音逻辑,失败就提醒用户if (!doStop()) {recordFail();}//释放 MediaRecorderreleaseRecorder();}});}/*** 启动录音**/private boolean doStart() {try {//创建 MediaRecordermMediaRecorder = new MediaRecorder();//创建录音文件mAudioFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath()+"/sound/" + System.currentTimeMillis() + ".m4a");//获取绝对路径mAudioFile.getParentFile().mkdirs();//保证路径是存在的mAudioFile.createNewFile();//配置 MediaRecordermMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);//从麦克风采集mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);//保存为MP4格式mMediaRecorder.setAudioSamplingRate(44100);//采样频率(越高效果越好,但是文件相应也越大,44100是所有安卓系统都支持的采样频率)mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);//编码格式,AAC是通用的格式mMediaRecorder.setAudioEncodingBitRate(96000);//编码频率,96000是音质较好的频率mMediaRecorder.setOutputFile(mAudioFile.getAbsolutePath());//开始录音mMediaRecorder.prepare();//准备开始录音mMediaRecorder.start();//开始录音//记录开始录音时间,统计时长mStartRecordTime = System.currentTimeMillis();return true;} catch (IOException | RuntimeException e) {e.printStackTrace();//捕获异常,避免闪退 返回false 提醒用户失败return false;}}/*** 停止录音**/private boolean doStop() {//停止录音try {mMediaRecorder.stop();//记录停止时间mStopRecordTime = System.currentTimeMillis();//只接受超过3秒的录音,在UI上显示出来final int times = (int) ((mStopRecordTime - mStartRecordTime) / 1000);if (times > 3) {//在主线程改变UI,显示出来mMainThreadHandler.post(new Runnable() {@Overridepublic void run() {mTvLog.setText(mTvLog.getText() + "\n录音成功 " + times + "秒");}});//停止成功return true;}return false;} catch (RuntimeException e) {e.printStackTrace();return false;}}/*** 释放 MediaRecorder**/private void releaseRecorder() {//检查 MediaRecorder 不为空if (mMediaRecorder != null) {mMediaRecorder.release();mMediaRecorder = null;}}/*** 录音失败**/private void recordFail() {mAudioFile = null;//Toast必须要在主线程才会显示,所有不能直接在这里写mMainThreadHandler.post(new Runnable() {@Overridepublic void run() {Toast.makeText(FileActivity.this, "录音失败", Toast.LENGTH_SHORT).show();}});}
}

布局文件呢就是特别简单的布局,这个就没有必要详细的讲了,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@android:color/background_dark"android:padding="16dp"><TextView
        android:id="@+id/mTvLog"android:layout_marginTop="80dp"android:layout_width="match_parent"android:layout_height="match_parent"android:textColor="@android:color/white"android:textSize="30sp"android:text="123"/><TextView
        android:id="@+id/mTvPressToSay"android:layout_width="match_parent"android:layout_height="60dp"android:layout_gravity="bottom"android:layout_marginBottom="20dp"android:layout_marginLeft="5dp"android:layout_marginRight="5dp"android:background="@android:color/white"android:gravity="center"android:text="按住说话"android:textColor="#333333"android:textSize="30sp"/></FrameLayout>

效果我们就在后面编写完播放代码的时候一起截出来

AudioRecord

这个与之前的操作其实相差不大,只是这边是流的形式读取的,所以需要循环录制,所以我们需要主线程和后台线程进行状态的同步,因为后台线程在循环中读取状态值,所以需要主线程改变状态值让后台线程得以w,然后我们是直接对AudioRecord进行配置的,详细的配置可以看startRecord()方法,代码注释非常详细,可以帮助大家进行理解。

package com.xjh.gin.im;import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;/*** Created by Gin on 2017/11/28.*/public class StreamActivity extends AppCompatActivity implements View.OnClickListener {private Button btn_start;private TextView mTvLog;private volatile boolean mIsRecording;//volatile保证多线程内存同步private ExecutorService mExecutorService;private Handler mMainThreadHandler;private byte[] mBuffer;//不能太大private static final int BUFFER_SIZE = 2048;private File mAudioFile;private long mStartRecordTime, mStopRecordTime;private FileOutputStream mFileOutputStream;private AudioRecord mAudioRecord;@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_stream);initView();initEvent();}@Overrideprotected void onDestroy() {super.onDestroy();//activity销毁时,停止后台任务,避免内存泄露mExecutorService.shutdownNow();}private void initEvent() {btn_start.setOnClickListener(this);}private void initView() {btn_start = findViewById(R.id.mBtnStart);mTvLog = findViewById(R.id.mTvLogs);//录音的JNI函数不具备线程安全性,所以要用单线程mExecutorService = Executors.newSingleThreadExecutor();//主线程的HandlermMainThreadHandler = new Handler(Looper.getMainLooper());mBuffer = new byte[BUFFER_SIZE];}@Overridepublic void onClick(View v) {if (mIsRecording) {mIsRecording = false;btn_start.setText("开始");} else {mIsRecording = true;btn_start.setText("停止");//提交后台任务,执行录音逻辑mExecutorService.submit(new Runnable() {@Overridepublic void run() {if (!startRecord()) {recordFail();}}});}}private boolean startRecord() {try {//创建录音文件mAudioFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/sound/" + System.currentTimeMillis() + ".pcm");//获取绝对路径mAudioFile.getParentFile().mkdirs();//保证路径是存在的mAudioFile.createNewFile();//创建文件输入流mFileOutputStream = new FileOutputStream(mAudioFile);//配置 AudioRecordint audioSource = MediaRecorder.AudioSource.MIC;//从麦克风采集int sampleRate = 44100;//采样频率(越高效果越好,但是文件相应也越大,44100是所有安卓系统都支持的采样频率)int channelConfig = AudioFormat.CHANNEL_IN_MONO;//单声道输入int audioFormat = AudioFormat.ENCODING_PCM_16BIT;//PCM 16 是所有安卓系统都支持的量化精度,同样也是精度越高音质越好,文件越大int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);//计算 AudioRecord 内部 buffer 最小的大小mAudioRecord = new AudioRecord(audioSource, sampleRate, channelConfig, audioFormat, Math.max(minBufferSize, BUFFER_SIZE));            //buffer 不能小于最低要求,也不能小于我们每次读取的大小//开始录音mAudioRecord.startRecording();//记录开始时间mStartRecordTime = System.currentTimeMillis();//循环读取数据,写入输出流中while (mIsRecording) {int read = mAudioRecord.read(mBuffer, 0, BUFFER_SIZE);//返回长度if (read > 0) {//读取成功,写入文件mFileOutputStream.write(mBuffer, 0, read);} else {//读取失败,提示用户return false;}}//退出循环,停止录音,释放资源return stopRecord();} catch (IOException | RuntimeException e) {e.printStackTrace();return false;} finally {//释放资源if (mAudioRecord != null) {mAudioRecord.release();mAudioRecord = null;}}}/*** 结束录音**/private boolean stopRecord() {try {//停止录音,关闭文件输出流mAudioRecord.stop();mAudioRecord.release();//mAudioRecord = null;mFileOutputStream.close();//记录结束时间mStopRecordTime = System.currentTimeMillis();//大于3秒才成功,在主线程改变UIfinal int times = (int) ((mStopRecordTime - mStartRecordTime) / 1000);if (times > 3) {//在主线程改变UI,显示出来mMainThreadHandler.post(new Runnable() {@Overridepublic void run() {mTvLog.setText(mTvLog.getText() + "\n录音成功 " + times + "秒");}});//停止成功return true;}return false;} catch (IOException e) {e.printStackTrace();return false;}}private void recordFail() {//Toast必须要在主线程才会显示,所有不能直接在这里写mMainThreadHandler.post(new Runnable() {@Overridepublic void run() {Toast.makeText(StreamActivity.this, "录音失败", Toast.LENGTH_SHORT).show();//重置录音状态,以及UI状态mIsRecording = false;btn_start.setText("开始");}});}
}

布局文件也非常简单就不累赘讲述了,不懂的话可以看代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@android:color/background_dark"android:orientation="vertical"android:padding="16dp"><Button
        android:id="@+id/mBtnStart"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="开始"android:textSize="30dp"/><TextView
        android:id="@+id/mTvLogs"android:layout_marginTop="10dp"android:layout_width="match_parent"android:layout_height="match_parent"android:textColor="@android:color/white"android:textSize="30sp"android:text="录音文件:"/></LinearLayout>

效果也是我们在后面编写完播放代码的时候会一起截出来

语音播放

语音的播放同样有两种模式,一种是文件格式的语音播放对应上面那个文件方式进行录制,使用MediaPlayer;还有一种是字节流模式的播放,对应的是字节流模式的录制,使用AudioTrack。
我们来分别介绍一下。

MediaPlayer

这个我们在前面的activity_file.xml中增加一个Button(播放),然后我们在FileActivity.java中新增几个方法,先加一个OnClick事件来触发播放功能:

mBtn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//检查当前状态if(mAudioFile != null&&!mIsPlaying){//设置当前的播放状态mIsPlaying=true;//提交后台任务,播放mExecutorService.submit(new Runnable() {@Overridepublic void run() {doPlay(mAudioFile);}});}}
});

然后新增下面几个方法来进行播放的逻辑

//播放逻辑
private void doPlay(File mAudioFile) {//配置播放器 MediaPlayermMediaPlayer = new MediaPlayer();try{//设置声音文件mMediaPlayer.setDataSource(mAudioFile.getAbsolutePath());//监听回调mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {@Overridepublic void onCompletion(MediaPlayer mp) {//播放结束,释放播放器stopPlay();}});mMediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {@Overridepublic boolean onError(MediaPlayer mp, int what, int extra) {//提示playFail();//释放播放器stopPlay();//错误已经处理return true;}});mMediaPlayer.setVolume(1,1);//配置音量(范围0~1,0为静音,1为原音量)mMediaPlayer.setLooping(false);//是否循环mMediaPlayer.prepare();//准备mMediaPlayer.start();//开始}catch (RuntimeException | IOException e){//异常处理,防止闪退e.printStackTrace();playFail();//释放播放器stopPlay();}
}//停止逻辑
private void stopPlay() {//重置状态mIsPlaying=false;//释放播放器if(mMediaPlayer != null){//重置监听器,防止内存泄漏mMediaPlayer.setOnCompletionListener(null);mMediaPlayer.setOnErrorListener(null);mMediaPlayer.stop();mMediaPlayer.reset();mMediaPlayer.release();mMediaPlayer=null;}
}//提醒用户播放失败
private void playFail() {//在主线程Toast提示mMainThreadHandler.post(new Runnable() {@Overridepublic void run() {Toast.makeText(FileActivity.this,"播放失败",Toast.LENGTH_SHORT).show();}});
}

AudioTrack

与上面那种方法一样我们也是要新增一个Button,然后我们在其中增加一个OnClicks事件,然后我们在StreamActivity.java中新增几个方法,来进行播放的处理,我们来说下其中的配置的传输模式吧,因为在代码中就这个没有进行注释,其他的代码中注释非常详细,就不进行叙述了。
Java和native层数据传输模式有两种:

  1. 流模式:AudioTrack.MODE_STREAM//循环一遍一遍的写
  2. 静态模式AudioTrack.MODE_STATIC//一次性写完
    先新建个OnClick事件:
case R.id.mBtnPlay:if(mAudioFile != null&&!mIsPlaying){//设置当前的播放状态mIsPlaying=true;//提交后台任务,播放mExecutorService.submit(new Runnable() {@Overridepublic void run() {doPlay(mAudioFile);}});}
break;

然后我们写以下的几个方法来执行播放逻辑,在使用AudioTrack 结束以后一定要记得把文件输入流关闭,然后把AudioTrack给关闭

//播放逻辑
private void doPlay(File mAudioFile) {Log.e("TAGSS",""+mAudioFile);//配置播放器 MediaPlayerint streamType = AudioManager.STREAM_MUSIC;//音乐类型,扬声器播放int sampleRate = 44100;//采样频率,要与录制时一样int channelConfig = AudioFormat.CHANNEL_OUT_MONO;//声道设置,要与录制时一样int audioFormat = AudioFormat.ENCODING_PCM_16BIT;//要与录制时一样int mode = AudioTrack.MODE_STREAM;//流模式int minBufferSize = AudioTrack.getMinBufferSize(sampleRate,channelConfig,audioFormat);//计算最小 buffer 的大小Log.e("TAGSS",""+minBufferSize);//创建 AudioTrackAudioTrack audioTrack = new AudioTrack(streamType,sampleRate,channelConfig,audioFormat,Math.max(minBufferSize,BUFFER_SIZE),mode);audioTrack.play();//启动AudioTrack//从文件流中读取数据FileInputStream inputStream = null;try{inputStream = new FileInputStream(mAudioFile);//循环读数据,写到播放器中去int read;while((read=inputStream.read(mBuffer))>0){Log.e("TAGSS",""+read);int ret = audioTrack.write(mBuffer,0,read);//检查返回值switch (ret){case AudioTrack.ERROR_INVALID_OPERATION:case AudioTrack.ERROR_BAD_VALUE:case AudioManager.ERROR_DEAD_OBJECT:playFail();return;default:break;}}}catch (RuntimeException | IOException e){//异常处理,防止闪退e.printStackTrace();playFail();}finally {mIsPlaying = false;//关闭文件输入流if(inputStream != null){colseQuietly(inputStream);}//播放器释放resetQuietly(audioTrack);}
}//提醒用户播放失败
private void playFail() {//在主线程Toast提示mMainThreadHandler.post(new Runnable() {@Overridepublic void run() {Toast.makeText(StreamActivity.this,"播放失败",Toast.LENGTH_SHORT).show();}});
}//关闭文件输入流
private void colseQuietly(FileInputStream inputStream) {try {inputStream.close();} catch (IOException e) {e.printStackTrace();}
}//播放器释放
private void resetQuietly(AudioTrack audioTrack) {try {audioTrack.stop();audioTrack.release();}catch (RuntimeException e){e.printStackTrace();}
}

总结

文件模式使用方便,字节流模式,使用起来比较复杂,但是非常灵活
因为录音和播放的处理都是耗时操作,所以我们要防止后台线程进行操作,不要照成主线程的阻塞,然后后台的线程为单线程,为了防止JNI函数的闪退,然后主线程要与后台线程数据同步(所以我们要用到volatile关键字)

即时通讯-语音录制及播放相关推荐

  1. IM软件中的语音录制与播放【iOS】

    前言 自从微信推出语音聊天后,人们的通讯方式发生了巨大变化,硬是把智能手机变成了对讲机.之后也成为了各种实时通讯软件不可或缺的功能.前一阵子微信公众号中展开了一场"发送语音消息利弊" ...

  2. [语音录制与播放]stm32+adc+dac

    2020-12-13更新: 最近总有人私信我要原理图和代码,贴一下我的公众号吧,有问题可以到这里联系我:[Golang梦工厂],下方点击联系人即可添加我的个人vx(建了一个语音录制交流群,加我VX拉你 ...

  3. android 调用系统自带录音实现,语音录制与播放

    相关权限: <uses-permission android:name="android.permission.RECORD_AUDIO"></uses-perm ...

  4. 百万并发电信级统一即时通讯(im+voip+多人语音)系统源码

    产品开发地点:广州  团队人数:7人,产品开发时间:3年7个月 产品模块: 完全自主研发的im客户端(没有使用任何第三方控件,完全自主开发) 服务端(openfire xmpp协议 mysql数据库) ...

  5. Flutter简单聊天界面布局及语音录制播放

    目录 前言: 注意事项: 用到的部分组件依赖及版本: 遇到的坑 遇到的坑1: 遇到的坑2: 遇到的坑3: 遇到的坑4: Fluuter语音录制及播放组件生命周期 Flutter录音组件生命周期图: F ...

  6. 音视频的流程:录制、播放、编码解码、上传下载等

    仿网易云音乐 安卓版-- https://github.com/aa112901/remusic Android本地视频播放器开发- http://blog.csdn.NET/jwzhangjie/a ...

  7. linux sdk 封装,集成方式-Linux开发集成-SDK开发集成-IM即时通讯-网易云信开发文档...

    集成方式 SDK内容 目录结构 nim | |-- include | |-- api |-- export_headers |-- util |-- libs | |-- x86 |-- x86-x ...

  8. 即时通讯源码-即时通讯集群服务免费-通讯百万并发技术-Openfire 的安装配置教程手册-哇谷即时通讯集群方案-哇谷云-哇谷即时通讯源码

    即时通讯源码-即时通讯集群服务免费-通讯百万并发技术-Openfire 的安装配置教程手册-哇谷即时通讯集群方案-哇谷云 1,openfire开发环境配置 很久没有写点东西了.最近很烦心,领导不给力. ...

  9. Android即时通讯与IOS端发送语音的问题。

    现在在做一个即时通讯,要发送语音.大家规定好的是aac文件. 现在是我这边录的发给IOS那边可以播放,我自己录的传到服务器再下载下来自己也可以播放,但是IOS那边录的我down下来之后就不行了.放不了 ...

最新文章

  1. 服务端php的更新手游客户端,PHP服务器安卓app下载|PHP服务器下载1.11.3 官方移动客户端-PHP服务器官方移动客户端1.11.3-蜻蜓手游网...
  2. 算法-----第一个错误的版本
  3. VS.NET 2005 Beta2的稳定性太差了:(
  4. javascript的eval和with使用小结
  5. 征战蓝桥 —— 2016年第七届 —— C/C++A组第7题——剪邮票
  6. PMP - 2011年6月考前辅导班
  7. [Leetcode]@python 90. Subsets II.py
  8. linux 配置 mysql_linux下mysql配置文件my.cnf最详细解释
  9. mysql 共享表空间存储_MySQL InnoDB共享表空间
  10. Rpc远程调用框架的设计与实现(1)
  11. 湖北孝感学校计算机好吗,湖北省孝感市2018年上半年计算机等级考试注意事项...
  12. Zabbix 数据清理
  13. Winform 按钮权限拦截AOP
  14. elasticsearch设置_search的size
  15. USB速度异常的问题
  16. (附源码)springboot 学生选课系统 毕业设计 612555
  17. 使用ssh工具连接window虚拟机中的linux系统
  18. python中gm11_python实现灰色预测模型(GM11)——以预测股票收盘价为例
  19. java中Graphics类的使用
  20. 智能家居之ESP8266接继电器接线方式

热门文章

  1. Qt美化调色控件(支持RGB,HSL调色,渐变色,十六进制,屏幕取色器,常用颜色)
  2. adb shell 小米手机_【转】【Android测试技巧】01. root后adb shell默认不是root用户时,如何将文件放入手机系统中...
  3. Java实现蓝桥杯方格计数
  4. python核心基础笔记(自总结,根据个人看书思路来写,收藏起来没事看看)
  5. 数据结构专题-学习笔记:李超线段树
  6. vue项目列表跳转详情返回列表页保留搜索条件
  7. c语言怎么把数字倒过来_c语言编程:实现数字的翻转
  8. 2018android 最新技术,2018年智能手机“新鲜事”,一起来看看,你的手机有这些新科技嘛...
  9. PayPal/Stripe商家工具-独立站轮询支付系统
  10. HashMap-----put方法详解