#1 android知识:
(1.1)为什么要使用Handler?、怎么使用Handler?、"ANR"错误、异步之类的
(1.2)在非主线程中更新主线程,runOnUiThread
(1.3)定时器的用法原理
(1.4)安卓Camer2相关用法参考
(1.5)Handler、Looper、Message机制:
(1.6)ThreadLocal用法原理(解决线程安全问题):
(1.7)Handler、Looper、Message异步通信机制:
(1.8)安卓Camera2各个类的用法:
(1.9)Activity的7个生命周期:
(1.10)ANR(Handler的作用)
(1.11)手机中的.apk文件转到电脑上 、 将电脑上的某个.apk文件安装到手机上:
通过手机助手-->应用管理-->选中某个应用-->导出 、 手机连上电脑-->打开adb-->
install 然后拖过来那个.apk文件-->回车即可!
(1.12)onRequestPermissionResult()方法作用(弹出框与用户交互的方法)
(1.13)四大组件作用描述(好理解):
(1.14)BroadcastReceiver理解:
(1.15)安卓工具自带两种测试:
测试Activity的登录方法:
http://www.2cto.com/kf/201507/421474.html
(1.16)yuv4208888几种格式的介绍:
(1.17)详细到吐的java多线程知识:
(1.18)异步通信原理(比如子线程如何更改主线程UI?)
一些铺垫:
http://www.cnblogs.com/lqminn/p/3751206.html (ThreadLocal原理)
(1.19)android中设置帧率的代码:
https://stackoverflow.com/questions/35048208/high-speed-capture-request-android-marshmallow
#2 算法知识:
(2.1)摄像头采集心率的总体原理
(2.2)摄像头采集心率的总体原理(有部分代码、带精度优化分析)
(2.3)滤波器原理:
(2.4)滤波器和傅里叶变换等原理:
(2.5)数字信号处理(书籍下载):
#3 遇到的大bug及解决方案:
(3.1)bug1:app第一次安装时,打开摄像头时,第一次总是打不开,第二次及以后每次都可以打开!
原因:目前猜测是这个方法onRequestPermissionsResult()没有执行到,
这个方法是当弹出“让用户开启权限的框时”,响应用户刚刚的选择的方法,
而目前没有执行到这个方法,那就不能及时响应到用户的刚刚的选择;所以,,
也有可能是版本的问题;
解决:已经解决;
收获:看完这个链接差不多懂了:
http://blog.csdn.net/beijinghsj/article/details/53581764
app中一般是一上来就让用户去选择允许这个权限,如果用户选择禁止,那么让它一直
弹框让用户选择允许为止!我这里已经实现了这个了!但是,还有一点小问题,就是:
如果用户勾了那个禁止后不再询问,然后再点禁止,则还是会有问题!
(3.2)bug2:app运行连续多次后会报错,(app只启动一次,这里的多次是指多次点击“开始测试”
,不是指关了app,多次启动app!)
原因:目前猜测可能是displayWave()方法有问题;
也猜测可能是onImageAvailable()方法有问题,因为,
根据Log来看,每次崩溃时都是在执行onImageAvailable时;
也猜测有可能摄像头长时间打开导致温度太高?自动结束的?这个概率渺茫;
解决: 经过调解,发现是 求心率的算法方法有问题,可能是他没有对入参做相应的判断处理导致出错,
因为,如果我把这个算心率的算法换成常数之后从来没有报过错!
收获:
(3.3)bug3:Hom键出去,然后回来,如果继续测试,那么第一秒内读到的图片数明显少很多,本来
正常情况下1秒内可以读到30张图片,但回来后的第一秒内只能读到大约8-10张,
这样就有点影响精度了!本来应该是30+30+30+30,然后交给算法算心率;
而你现在是8+30+30+30,然后交给算法算心率,当然有影响;
原因:
startPreview();
开始计时器开始计时了;
isStartTesting=1;
上面3句反应的是继续测试的代码,问题出在:执行了startPriview()方法之后不能立即打开
摄像头,而计时器已经开始计时了,那多久以后才能真正打开摄像头呢,目前的结果是
大约需要0.5秒textureview才变得可用了,所以监听器立马监听到,所以大约又花0.2秒
立马打开预览,所以,已经过去0.7秒了,目前肯定没有一张图片,所以,其实最后0.3秒
才开始捕获到图片,所以,会少了很多!
解决:目前的问题是我不知道textureview什么时候可用,这个我把握不了,所以,,,,
如果我知道了重新回到app后它什么时候可用了,那我就可以在那之后再调用startPreview()方法,
这样就可以解决了!
(3.4)bug4:先知道:我们目前是每来一张图片,然后立马启动一个线程去计算,将这张图片计算出一个数,
但是,问题来了,当来了一张图片之后,立马执行onImageAvailable()方法,然后立马开启一个
子线程,但是实际情况是: 本来应该先让子线程处理完刚刚的图片之后,再下一次执行
主线程的onImageAvailable()方法,但是,实际情况是:主线程每次都会抢,即子线程都还没有
走完主线程就已经抢先执行onImageAvailable()方法了! 这样造成的后果是:子线程中处理
图片的函数可能出错,ImageConvert.convertyuv2rgb()方法可能出错;还有就是图片不明确,
有可能第一张图片的数据没有得到、第二张图片的数据没有得到、而第三张得到了!
原因: 主线程的优先级略大一点、会抢走子线程的处理权;
解决: 尝试一、让主线程不能抢,即:
childThread.start();
childThread.join();
意思是只能等当前子线程全部执行完了之后,主线程才能开始执行!
但是又带来一个问题,就是帧率只能18左右,即使你设置成30、40、50,都只有18!
尝试二、处理图片的时候能不能不做成子线程去处理呢?即来了一张图片时,我干嘛要另外
开一个子线程去处理呢?我直接主线程处理呀!(开一个子线程的目的是防止处理
图片要很久,导致主线程“卡死”,ANR错误吧好像是!)
上面两次尝试之后,事实是: 如果不做成子线程处理,主线程不会卡死,但是,每秒读到的图片
数少了很多,几乎也降到了18,这就说明:用子线程处理会提高效率,说明还是应该
起一个子线程处理图片数据,让主线程去抢处理权!然后我又进一步验证了主线程到底
抢子线程处理权抢到什么程度? 其实约等于没抢,因为:处理图片的子线程运行了100次,
绿光数组就有100个正常值,一一对应! 其实,子线程已经算出了该图片的结果之后
主线程才抢的cpu,只是子线程想打印该图片结果时才被抢而已,所以,影响不大!
所以,目前仍然让主线程1抢吧!
(3.5)bug5: 第一秒总是会少好多帧,后面就正常了,这个后面考虑解决一下;
app一上来好像执行完onResume之后居然还会执行onPause()方法,初步怀疑是摄像头权限原因!
原因:
解决:
(3.6)bug6: 正常情况下,当开始计时以后,每来一张图片就执行一次onImageAvailable()方法,而且,当来了这张
图片后,这个方法内部会起一个子线程去处理这张图片,得到一个数,并把这个数存到数组中去,
这是正常情况下! 但是,现在总是有一个问题:当来第一张图片的时候,成功进到onImageAvailable()
方法中了,这个时候起了一个子线程吧,本来应该把子线程执行完的,但是子线程执行到一半,cpu就跳
走了,重新跳到onImageAvailable()方法中了,即又来了一张新的图片, 奇怪的是,从第二张图片起,
主线程不会抢了,会每来一张图片就让你的子线程执行完!而且,到了最后一张图片都执行完了的时候,
它这个时候又会继续把之前的第一张图片的子线程没有执行完的任务继续执行!!虽然不会有什么影响目前,
但是看着很不爽!!
解决:尝试着改主线程、子线程的优先级,改成一样,还是没用!
顺便说一下怎么获取当前线程的优先级、怎么设置当前线程的优先级:
获取:在当前线程中的任意一个位置: Thread.currentThread().getPriority()
设置:
在主线程中的任意位置Process.setThreadPriority(5);
在子线程中的run()方法中 Process.setThreadPriority(5);
(3.7)bug7: 大部分手机在测试它们帧率的时候,有个这样的现象:比如将帧率设置成30,然后在不同的手机上跑,
结果,一般第一秒不太稳定,具体是第一秒一般都好少,然后从第二秒开始几乎稳定在30内,也就是说,
第一秒内的数据影响很大,所以,我们应该考虑去掉第一秒的数据,考虑从第二秒内的数据开始!
解决: 比如本来测试10秒的,则故意测11秒,然后,读数据时,从第2秒开始读,第1秒的虽然有数据,但是
我不读它不就行了!(v5)
这个解决方案一箭双雕,既解决了多数手机第一秒内的帧数不稳定的问题;又解决了Home回来时第1秒
内帧数明显偏少的问题(即bug3中的问题)!
(3.8)bug8: 帧率测试的demo总是会报一个这样的错(有些手机上出错,有些手机正常):
java.lang.IllegalStateException: maxImages (1) has already been acquired, call #close before acquiring more.
原因: 先大约讲一下原理,手机预览每次拍到一张图片,就会放到textureview上,然后imageReader控件一嗅到
textureview上有了新图片,那么就会执行onImageAvailable()方法,去获取刚刚的图片并放到自己的图片库
中,我们的代码是这句:
final Image image = reader.acquireLatestImage();
即报错总是这句代码报错,我猜测原因是: 如果你把setfps设置为1,那么,表示1秒钟只拍一张,那么,
实际上有些手机确实1秒钟只拍一张,但有些手机会拍n张实际上,那么,恭喜,每秒钟真的拍1张的就会
有问题,因为,我们每次拿到最新的这张图片时,去处理,处理完之后就会image.close(),而,如果真的
1秒钟只拍了一张照片,则又close掉了,那么,你再次执行到reader.acquireLatestImage()时,没有图片呀,
所以,程序崩了!
解决: 目前的解决方案是跳过1即可,即setfps从2开始!(保险起见可以从3开始~)
上面的方法没有从根本上解决问题,后面的过程中可以搞清楚imageReader的工作原理,然后从acquireLate
stImage()方法试试解决方案!
(3.9)bug9: 如果主线程中某个地方用到了Thread.sleep(),则后面如果用到runOnUiThread(),则无效或者出错;
原因:
解决:
(3.10)问题:1秒钟内执行不了30次图片处理,怎么加快?
(3.11)问题:
(3.12)问题:
#4 收获andrlod开发经验:
(4.1)收获1:怎么查错误在哪?
第一,通过查看控制台的 “Error”,可以很清晰的看到;(注意,自己打Log时就不要用Log.e()了,
否则会干扰你找错误错在哪!) 这里顺便说一下Log的几种方式,如果你打Log.d(),
则在控制台中找“Debug”;如果你打Log.i(),则控制台中找“Info”;如果你打Log.w(),
则在控制台中找"warn";总之,如果你要方便找错误,千万不要打Log.e(),
因为你把过程打成Log.e(),到时候你想找错误在哪时,
你会发现找"Error"时都是你的过程,把真的错误都遮挡了!
第二,写代码时每个方法都要写日志,这样,你跑一遍程序,就可以看到全路径了,
对于排除错误很方便,这里注意了:假如你在A 方法的开始和结束处打了日志,
比如,A-start和A-end,假如,最后A-start和A-end都运行完了,你要看下A方法中是不是
开了一个子线程,如果A方法中开了一个子线程,那么,虽然A-end运行完了,
但是有可能A方法中的这个子线程出错误了,
这里想说的一点是:虽然某个方法可能运行完了,但是不能说明它这个方法里面就没有问题,
因为有可能这个方法中开了一个子线程,而这个子线程中有bug!所以,
建议每个子线程中都记得打上Log!(这点比较关键,这也是排除错误的
一个很重要的环节,因为这点很容易忽视,但往往很重要!)
第三,“控制变量法”:如果你不确定某个方法是不是有错误,你可以这样,比如原来是result=f();
你可以暂时把f()替换成一个常数,如替换成result=1; 然后再去反复地跑刚刚的程序,
如果现在没有一点问题之前有问题,则说明问题出在f()方法,有可能
是你传给它的参数没有判空之类的,也有可能是它的方法逻辑有问题;
如果现在跑仍然有问题,则说明之前的错误跟f()方法没有半毛钱关系!
第四,打Log时(过程Log),最好加上哪个线程,比如主线程、子线程1、子线程2等!
因为,万一出错了,就可以马上定位到是哪个线程中的错误!
第五,打Log时要清晰,尤其是if,else等语句处,一定要清晰,对分析结果很有帮助;
(4.2)收获2: Activity生命周期积累:
第一,正常启动时:onCreate-->onStart-->onResume;
第二,按返回键时:onPause-->onStop-->onDestroy;
第三,只要当前“页面”(Activity)被完全挡住,比如打电话来、按了Home键、跳到别的页面等,
则一定会执行:onPause--onStop; (如果被确定或取消提示框部分遮住当前页面,则
不会触发任何方法!)(所以,目前的结论是:如果当前页面被完全挡住了则会执行
onPause-->onStop方法;如果当前页面只被挡道了部分而失去了焦点则不会执行方法;)
(当前结论不一定完全对,后面如果能推翻它则再来推翻,目前视为是正确的!)
第四,假如当前页面之前被完全挡住了,如果之后又再一次回到该页面,则会触发哪些方法呢?
一般再次回来时都会执行:onRestart-->onStart-->onResume,比如,之前按了Home
键则再次回到该app时就会执行那3个方法、比如,之前突然收到一个电话,接完电话
后挂了再次回到该app的页面时就会执行那3个方法;等等……
但有一个特殊,就是,如果之前在A页面,如果主动跳转到B页面,按照前面的逻辑,
先会执行onPause-->onStop;如果在B页面跳到A页面,则,这个时候会执行A页面
的onCreate-->onStart-->onResume方法!注意区别!
第五,假如当前页面正在运行,突然弹出一个框,这个框并没有把当前页面完全挡住,只是
浮在它的上面,其实仍然可以看到当前页面,只是焦点在弹出的这个框上,则弹出的
那一瞬间会执行当前Activity的onPause()方法;假如那个小框消失了,焦点立马回到
原来的那个页面上时,则会立即执行之前那个Activity的onResume()方法!!
注意:这种小框比如 ” xx要求申请相机权限,禁止还是允许“!(注意:如果弹的
框是自己写的那种AlertDialog,则不一定会执行上面说的方法,自己有空以后可以试试)
(4.3)收获3: 关于打印日志,前面第一个参数的作用:
假如一个工程下有100个Activity,那么,控制台下的日志信息就会包含100个Activity中所有的
日志信息,这样,就不方便观察,比如每个Activity都有那几个方法:onCreate onStart onResume
等,如果好多个Activity的都混在一起,则很难分辨,所以,往往会这样做:在每个Activity中
定义一个常量TAG = "Activity1",然后在打印的时候就: Log.d(Tag, "onCreate");
这样,以后在控制台中查看日志时就很清晰,哪一条日志属于哪一个Activity中的!
(4.4)收获4: 弹框代码(确定取消):
new AlertDialog.Builder(this).setTitle("提示") .setMessage("确定退出?") .setPositiveButton("确定", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { //确定按钮事件 Log.d(TAG, "************选了确定"); } }) .setNegativeButton("取消", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { //取消按钮事件 Log.d(TAG, "************选了取消"); } }) .show();
(4.5)收获5:多线程相关的!
比如如下代码:
{
1, a = 5;
2, 开一个子线程,在子线程中输出a的值;
3, a = 0;
}
千万注意,此时很有可能输出的值是0! 这种是逻辑错误,有时很难发现!
(4.6)收获6:如果发现同样的代码在这个app中可以运行,但在另一个app中不能运行,要想到有可能
这两个app的build.gradle文件可能不一样,很有可能是版本不一样!!!!!!
同时也要想到:同一份代码如果配置成不同的版本则有些可以运行、有些不能运行!!
(4.7)收获7: 每个Activity都要在Manifest.xml中声明,否则会出错!
目前,主Activity和普通Activity的声明方式如下:
<activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="com.example.y81022671.a24_androidtest_use_03.ResultActivity"> </activity>
(4.8) 收获8: 打上全Log几乎不会影响系统的时间!(不要担心可能打Log导致1秒内少拍了几张图片)
(4.9) 收获9: 帧率值好像设置成好大时,程序会直接崩溃!(这个要注意!用户体验!
提前预判出哪些手机不能超过多少!)
(4.10)收获10:异步通信原理(纯粹自己的理解):
(a)为什么要异步通信?安卓中的主线程就是UI线程,首先,描绘UI可能比较耗时,如果
有耗时的操作,那么,主线程一直卡住,就会有那个错误;另外,有可能有那种临时需要
更改UI的操作,而主线程总不可能回过头去执行描绘UI的代码吧,显然会报错!
所以,异步通信,,,,,,
即:让一个子线程处理更改UI的代码,等它绘制完了,然后异步通知主线程,然后,主线程
更改UI即可!
(b)可能有多种异步通信方式,我这里分析Handler这种方式:
(b.1)首先,可以把某个线程变为 Looper线程,变为Looper线程之后意味着,该线程内部
多了一个消息队列,然后该线程会循环地去该消息队列中取消息,有就可以取到,没有
它也会过会儿又来取!怎么朝这个消息队列发消息、怎么处理取到的消息后面会讲!
(b.2)先说一下怎么让某个线程变为Looper线程?:简单点就是这样:
某个线程
{
Looper.prepare();
Looper.loop();
}
那么,上面那个线程就是一个Looper线程了!
(b.3)某个线程变为Looper线程之后,你就要有这样的意识:这个线程内部有个消息队列、而且,
这个线程会循环地去这个队列中取消息!
注意:某个线程变成了Looper线程之后,那么,这个线程内部就有一个Looper对象,
而且,假设有10个线程都是Looper线程,那么,其实,它们的Looper对象是同一个
Looper对象,但是,不会存在多线程共用一个变量的问题,再去看ThreadLocal!
注意:一个线程最多只有一个Looper对象!
注意:android中的主线程默认是一个Looper线程了!(具体原理先不用管)
(b.4) 怎么发送消息到某个消息队列呢?Handler发送消息!它发送的消息可以发到上面讲的消息队列
,所以,每个handler对象都要绑定一个Looper线程!显然:如果handler1绑定了Looper线程A,
那么,当用handler1发送一条消息,则这个消息就会进到Looper线程A的消息队列中!
(b.5)handler怎么绑定Looper线程呢? 我感觉是这样的:
方式1,默认的绑定:
Looper线程A
{
Handler h1 = new Handler();
}
那么,h1就默认绑定了Looper线程A的Looper对象了!也就意味着:以后只要用h1发送的消息
都会发到Looper线程A的消息队列中!
总结:这种方式说明:只要你在某个Looper线程中new一个Handler对象,那么,这个Handler
对象就默认会绑定该Looper线程!(具体是:绑定该线程的Looper对象中的消息队列!)
方式2,手动绑定:
线程B
{
Handler h2 = new Handler(Looper线程C.getLooper());
}
很显然,虽然h2是在线程B中创建的,但是它绑定的是Looper线程C的Looper对象下的消息队列!
(b.6)怎么用handler发送消息呢?
这3句解决:
{
Message msg = new Message();
msg.setTarget(h5);
msg.sendToTarget();
}
不要被上面的3句代码的表明意思迷惑了,上面3句代码的意思其实就是: h5发送消息到它绑定的
Looper对象下的消息队列中去!
注意:发送消息的代码可以在任何线程中,准确来说,只要你这个线程中有handler对象,你就可以
随便发了!
(b.7)怎么接收消息并处理消息呢?
这里说下,handler在new的时候,其实可以重写一个方法:handleMessage()方法,
可以定义好将来接收到这个handler发送的消息后该怎么做的代码!比如:
Handler h6 = new Handler(LooperM线程.getLooper()) {
重写handleMessage() {
//eg. setBackgroundColor(red);
}
}
那么,如果某个地方用这个h6发送了一条消息,则:会发到LooperM线程的Looper对象下
的消息队列中去,那么,LooperM线程循环查看消息队列时,肯定会发现这条消息,
当发现之后,则:LooperM线程此时就会执行handleMessage()方法中的代码!
(b.8)知道了上面的原理后,我们可以尝试着实现: 在某个自己定义的子线程中写更改UI操作的代码,
然后,让主线程在适当的时候执行该段代码!这不就是异步通知主线程更改UI吗?
如下:
自己定义的子线程 { 构造方法(传主线程的Looper对象过来); run()方法 { Handler h=new Handler(传主线程的Looper对象过来) { 重写handleMessage()方法 { //模拟改变主线程UI setBackgroundColor(red); } }; //发送消息 Message msg=new Message(); msg.setTarget(h); msg.sendToTarget(); } }
那么,当启动这个子线程的时候,根据逻辑,就会发送消息到主线程
的Looper对象下的消息队列了,那么,主线程自然将来会查到该消息,
那么,自然将来会执行那个方法(setBackg…………),这不就实现了异步
通知主线程更改ui 么!!!!
(b.9)android中早已封装好了一切,android中的封装跟上面很相似,
希望可以看懂:
mBackgroundHandlerThread = new HandlerThread("m_background_handler_thread"); mBackgroundHandlerThread.start(); mBackgroundHandler = new Handler(mBackgroundHandlerThread.getLooper());目前没看懂原理!
(4.11) 收获11: Log的5个级别,及应用场景(也是编程规范一部分)
第三,编程规范中: 平时写demo之类的v、d就够了,一般用d,v一般不用;
如果要上库,要输出日志信息,则i比较合适,i就是用于正常的
上库代码中的正常分析流程信息的语句;
而w,则稍微严重些,警告嘛,说明可能出错,比如try catch中
打印这种信息比较合适;
而e,则不用你手动写,一旦出现了这个则是一定是报错了,如果你
写了,则说明你早就知道这段代码一定会出错,这,,,,
难道你还会写你百分百确定出错的代码?? 所以,一般不用这个!
(4.12) 收获12: 修改app名称:
http://blog.sina.com.cn/s/blog_6f72ff900102v948.html
(4.13) 收获13: 当布局文件有问题时检查下包名有没有问题;
(4.14) 收获14: 如果你让主线程Thread.sleep(),然后在此期间如果再runOnUiThread(),则失效或者报错;
(4.15) 收获15: 如何修改app的版本(可以支持的版本) ,这个比较重要,因为如果你设置的太高,则好多
低版本的手机运行不了这个app;如果你设置的太低,则可能你的这个app的代码有些是比较
新的一些技术,则可能这些代码在某些比较低版本的手机上会出错(不支持);
低版本的手机运行不了这个app;如果你设置的太低,则可能你的这个app的代码有些是比较
新的一些技术,则可能这些代码在某些比较低版本的手机上会出错(不支持);
(4.16)收获16: ImageReader和它的acquireLatestImage()方法的那些事总结(不敢保证百分百对):
第一,在设置fps的值时,有两个地方,一个是建立预览request请求时,一个是在打开摄像头
之前,new ImageReader对象的时候,这里说下后者,假设设置成x,现在来分析:
以后来了图片每次都会拍到Textureview上,而imageReader跟这个TextureView绑定了
,而imageReader有个监听器,它可以监听到拍到了新图片,则会触发onImageAvailable()
方法,在这个方法中一般是这样:
onImageAvailable()
{
Image image = reader.acquireLatestImage();
处理image;(比如yuv->rgb)
image.close();
}
下面来分析fps设置的值(这里指的都是ImageReader的那个值):
x <= 0: 根据api的定义,一定要大于0,所以这种情况下回报错!
x 很大(比如80): 则会报错,因为,每张图片占内存,如果设置成80,则要占好大
内存,则系统可能直接不让你这么做,所以无条件挂了你!
x = 1: 很危险!!为什么?因为reader的这个值意思是最大可以从TextureView上
获取多少张图片!如果你设置成1,则说明只能调用1次reader.acquireLatestIamge()
方法,除非,你每次获取了该图片后,都及时关掉了该图片,则你还可以再获取!
否则很危险!而你设置成1时,一般大部分手机都会实际拍10多张!所以,你一定要
保证每acquireLatestImage()一张图片,都要及时close()它,则以后就不要紧!!
同理,如果你设置成15,假设,你acquireLatestIamge()了15次,一次都没有close()
过,则你不能再调用acquireLatestIamge()方法了,否则会崩!!
所以,在onImageAvailable()方法中接收图片时,即那三行代码,如果用单线程处理
,则肯定是串行的,则肯定 不会有问题; 如果你要提高效率,采用并行,即每次都
new一个子线程处理,则你一定要保证:假设你设置成15,则你要保证
不能让前15个子线程都执行了前面的语句,而没有一个线程close(),那么,如果第16
个子线程再执行acquireLatestIamge()时就崩了!!!!
第二,验证上面的结论:假设reader的fps设置为3,且在onImageAvailable()方法中这样:
{
Image image = reader.acquireLatestImage();
}
则:执行第四次的时候会报错!!
且错误为: java.lang.IllegalStateException: maxImages (3) has already been acquired,
call #close before acquiring more.
验证了上面的结论!!!
第三,讲一下预览:预览可以理解成是摄像,可以理解成连续不断地拍照,不多解释了,可以理解成
连续不断地执行onImageAvailalbe()方法!! android可能考虑到了一个问题,就是
图片比较大,占内存,如果它监测到你在预览的过程中好久都没 “处理图片”,那它
自动会停止拍照,即自动会停止预览,即不拍照了!! (不同的手机停止的条件
不一样,比如nova2如果开启了预览,如果拍了8张图片了,你都没有一次处理图片,
那它就会停了你的预览!! honor6x的条件是10多张后再停!!)
所以,如果你在onImageAvailable()方法中什么都不做,则过了一段时间预览会停止!
所以,你最好要在这个方法中有类似acquireLatestImage()这样的方法!!
这里有一个很隐蔽的错误(这个错误很隐蔽,你根本想不到为什么!!!!!!):
比如你在onImageAvailable()方法中这样:
{
if (isStartTesting == 1)
{
启动一个子线程处理图片;(包含acquire、deal、close等)
}
}
这样的后果是:当你开启预览后,如果有段时间没有让isStartTesting == 0,则预览
会自动关了!!!
所以,正确的方式是:
@Override public void onImageAvailable(ImageReader reader) { if (isStartTesting == 1) { Thread thread = new Thread(new ImageProcessor(reader)); thread.start(); } else { Image image = reader.acquireLatestImage(); if (null != image) { image.close(); } } }
第四,注意:reader.acquireLatestImage()的结果可能为空,所以,image在操作之前一定要判空!!!
第五,如果image关闭了,则你没办法再用这个图片资源了,一切都会报错!所以,别妄想先拿到该图片
,然后把它存到list中,然后image.close(),之后再从list中取出来用!!
因为一旦你关掉了这张图片资源,即使你放在list中的这张图片的引用对象,也不能用了!!!!
******第六,目前算心率处理图片的方式是这样的: 假设fps设置为25,则1秒钟内会执行25次onImageAva
ilable()方法,则我每次都开一个子线程,即1秒钟内开25个子线程处理图片,由于并行处理,所以
1秒钟内它们都可以处理完!!!! 即达到我们的效果了!! 现在的问题是要防止它崩了,
什么时候会崩?: 当这种情况下: 当25个线程都没有执行close()方法,而且这秒内又执行了
一次onImageAvailable()方法,而且这个子线程执行acquireLatestIamge()方法,则崩了!!
但是,这种概率很小很小!! 因为,25个线程没有一个执行close()方法的概率几乎为0,
其次,你设置成25,但却第26次执行onImageAvailable()方法的概率也很小!! 所以,
目前这种方式出错的概率很小,几乎不会出错!!!!
****展望一下:万一,上面那种方式下出错了,有没有什么更好的改进方法呢???
确实有!!有一个更高效又更安全的方式!!(大约如下)
@Override public void onImageAvailable(ImageReader reader) { if (isStartTesting == 1) { count++; Image image = reader.acquireLatestImage(); if (count <= SET_FPS) { list.add(image); } else { if (null != image) { image.close(); } } } else { Image image = reader.acquireLatestImage(); if (null != image) { image.close(); } } }大约思路就是: 每来一张图片,我先获取它、保存它,但不关闭它!
count的作用是保证连续获取的图片数小于set_fps,
多出来了之后就不会保存了,直接关闭!!
这样,不处理图片,就可以省时间!而且,保证不会
报那个超出图片数量的错误!! 而且不会有多线程之间的
抢优先级的错误!!
当时间到了要计算心率的时候,统一来处理这些图片,
处理完一张后再释放一张!!
(4.17)进度条的简单使用:
<ProgressBar android:id="@+id/progressBar" style="@android:style/Widget.ProgressBar.Horizontal" android:layout_width="280dp" android:layout_height="30dp" android:layout_marginTop="180dp" android:layout_marginLeft="40dp" android:visibility="gone"/>
有横向的进度条,就是上面的Horizontal类型,还有环形的,还可以自定义;
setProgress()、setMax()分别表示设置当前进度、设置最大值!(比如你可以设置最大为10),
然后每隔1秒让某个变量加1,然后重新设置一下当前值!这样就可以实现10秒钟进度条走到底了!
******这里说一下Handler的又一个场景(上一个场景见16_fps_2.2那里!)(这里的场景是在非主
线程中更改主线程中UI的场景的典型):因为进度条是主线程的UI控件嘛,它一开始有个值,但是
随着时间的推移要不断更改它的进度情况,即要更改它的UI,即要重绘它!而如果进度值的更改是
在主线程中改的还好,万一这个值是在一个非常耗时的子线程中执行了某个操作后变化的,那就
相当于在子线程中要请求改变主线程的UI吧,所以,场景就来了!
具体解决方法:先在主Activity中new一个全局变量progressHandler,在new它的时候把处理消息
的事情也做了,即更改进度条的进度!目前的理解是: 因为这个Handler是声明在主Activity的一个
全局变量,所以这个Handler就跟主线程绑定在一起了,而当以后这个Handler发了消息之后,就会
被主线程处理,因为主线程有个消息队列,它会不断检查有没有消息,如果有了,就处理!那么,
可能有多个不同的handler发消息过来呢,那你交给哪个处理呢,所以,发送消息的handler会交代
清楚是哪个handler发的消息,就是方便主线程交给谁来处理用的!
MainActivity的全局变量handler如下:
private Handler progressBarHandler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case PROGRESS_CHANGE: { progressBar.setProgress(currentProgressValue); break; } default: break; } } };
在某个非主线程的子线程中发送消息如下(指明了是哪个Handler发的消息!!):
currentProgressValue += ADD_PROGRESS_VALUE; Message msg = new Message(); msg.what = PROGRESS_CHANGE; msg.setTarget(progressBarHandler); msg.sendToTarget();
(4.18)自定义各种形状的Button按钮:
其实其他的都一样,该怎么用就怎么用,就一处改变,在布局文件中声明这个按钮的时候
给这个按钮的基本信息处加上这个 android:background="@drawable/myshape"
表示让这个按钮的形状为 myshape.xml文件中定义的形状!
然后在res/drawable-mdpi 目录(可能要手动建这个文件夹)下创建一个叫做 myshape.xml的文件
(其中corners会报错没有该图片,注释就好了不重要,然后报一个不存在的颜色错误,很简单!)
(4.19)布局的一些学习:
场景:让2个控件在同一个位置,某一时刻只显示一个控件,怎么做到?
场景:让一个控件叠在另一个的上面,同一时刻要两个控件都可以看到!
解决:帧布局!!(其他布局情况下,你要让两个控件在同一个位置则会挤掉另一个!但帧布局可以)
这个属性可以控制只显示一个控件:android:visibility="visible"
帧布局本身就可以保证一个控件叠在另一个上面,你可以考虑让另一个控件隐藏或者透明!!
(4.20)跳转页面带参数(A->B):
A跳转:
Intent intent = new Intent();
int maxHeartRate = 55;
intent.putExtra("result", maxHeartRate);
intent.setClass(StressMonitor.this, ResultPage.class);
StressMonitor.this.startActivity(intent);
intent.putExtra("result", maxHeartRate);
intent.setClass(StressMonitor.this, ResultPage.class);
StressMonitor.this.startActivity(intent);
B接收:
Intent intent = getIntent();
int result = intent.getIntExtra("result", 0);//这个0是给个默认值,如果没取到数据就放个0!
int result = intent.getIntExtra("result", 0);//这个0是给个默认值,如果没取到数据就放个0!
(4.21)更改横屏竖屏、设置app图标、app名字:
横竖屏:在清单文件中,定义某个Activity时,如果加上android:screenOrientation="sensorLandscape"就是横屏
设置app图标:在清单文件中application下面加上这个属性:android:icon="@drawable/logo" (logo.jpg)
设置app名字:在清单文件中application下面加上这个属性:android:label="heartrate"
(4.22)Camera2预览原理再解释(开预览后实时处理每张图片(放到文件或实时使用它!)、关预览后则结束):
第一,我先说下预览、拍照、录像的区别(个人理解):
其实,三者都可以看做是预览,只是表现形式不一样而已!
只要我们打开了摄像头,就是开启了预览实际,打开摄像头之后不是可以看到在动吗,那就是在预览,
即在不断地拍照!那点击“拍照”按钮,其实之前app一直都在拍照,只是并没有存这些图片而已,而
你点了“拍照”之后,则其实app做的事情就是获取并保存这一瞬间的这张图片,然后写到手机的某个
目录下去!其实预览的时候是一直在拍照的,只是没有保存图片并写到手机目录中去而已!
而摄像呢,原理就是:点了摄像按钮之后,其实就是开了预览,就是在不断地拍照,它跟预览不同的
是,预览时虽然在不断拍照,但不保存图片数据;而摄像也是在不断拍照,不过它每拍到一张照片
都会获取它的数据并以字节的形式加到一个.mp4的文件中去,等到摄像结束时,这个.mp4中就包括了
刚刚拍到的所有图片的字节形式的数据!
综上,总结就是:每个手机中自带的“摄像机”这个app它的原理就是调用相关代码开启预览,然后对
不同的操作执行不同的处理而已!比如,进来app之后就开启了预览了,这个时候只是预览但不主动
去保存图片,待你按拍照按钮那一瞬间,它就获取那张图片并保存到手机目录中!待你点击“摄像”按钮,
其实是重新开启了预览,并且预览过程中每拍到一张图片就把这张图片的字节流数据写到一个文件中
,待你点击结束摄像时,最后这个.mp4文件中就保存了刚刚拍到的所有图片的字节流信息!!
第二,上面已经知道,预览是核心,所以,下面来看Camera2打开预览的原理:
这里结合我们的应用场景来分析,我们的应用场景是:让预览时每次拍到的照片都放在控件TextureView
的Surface上,然后我们通过ImageReader对象跟这个TextureView绑定,绑定了之后就给reader对象加个
监听器,它监听TextureView上有没有来图片,只要来了一张图片就会被监听到,就会触发reader的
onImageAvailable()方法,在这个方法中我们可以提前写好处理图片的代码,这样就做到了预览过程中
的每张图片我都可以获取到!!
下面来看具体的(打开预览的过程)(具体过程不方便分析,现在只解释一下过程中一些比较重要的操作):
操作1(这里需要注意,一般情况下textureview都是有效的,一般什么时候无效呢,可能正在预览的时候突然
按了Home键,则重新回来时可能textureview无效!所以,无效的时候给这个textureview加个监听器,
这个监听器的作用是以后会时刻监视这个textureview的状态!)(为什么要加needopen这个参数,因为
难道我只要监听到textureview可以用了就一定要打开摄像头吗,看情况嘛,所以这个变量的作用就是这个!)
private void startPreview() { needOpen = true; if (textureView.isAvailable()) { //打开摄像头 openCamera(); } else { textureView.setSurfaceTextureListener(surfaceTextureListener); } }
//TextureView控件的监听者: private TextureView.SurfaceTextureListener surfaceTextureListener = new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { if (needOpen == true) { openCamera(); } } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return false; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { } };
操作2:在openCamera()方法中有两个比较重要的地方,一个是打开之前先设置相关参数、
另一个是真正打开摄像头的操作!
其中设置中重要的是拿到后置摄像头的id,初始化reader对象,并给reader加一个监听器,
用于监听将来和它绑定的textureview上有没有来图片!
设置好了之后,打开摄像头的代码是这句:
manager.openCamera(currentCameraId, mStateCallback, mBackgroundHandler);
操作3:如果后置摄像头空闲且可用,就会进入CameraDevice.StateCallback中的onOpned()方法,
在该方法中我们做的事情
就是设置预览的一些参数、以及创建一个预览请求、以及执行打开预览的操作:统一写在一个叫
createCameraPreviewSession()的方法中,名字可以随便取!
在这里要做的事情有: 设置textureview上将来图片的像素点的大小比如640*480,、
绑定reader对象和textureview控件、
然后在创建具体的预览请求对象之前先建立session会话:
操作4:如果建立session会话成功,则会执行CameraCaptureSession.StateCallback中的onConfigured()方法
,这个方法中的内容就是我们设置预览的代码以及创建具体预览请求的代码以及执行开启预览的代码了!
//可以理解成:如果建立Session会话成功; @Override public void onConfigured(CameraCaptureSession session) { if (null == mCameraDevice) { return; } //拿到该session会话对象; mCameraCaptureSession = session; //设置一些builder的属性; mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF);//设置自动对焦 //Range<Integer> fpsRange = new Range<>(SET_FPS, SET_FPS); Range<Integer> fpsRange = Range.create(SET_FPS, SET_FPS); mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);//设置预览时每秒产生帧的范围; mCaptureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);//设置闪光灯之类的; //通过builder对象产生一个具体的请求对象; mCaptureRequest = mCaptureRequestBuilder.build(); //执行预览; try { mCameraCaptureSession.setRepeatingRequest(mCaptureRequest, null, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } }
其中,builder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
表示打开预览的同时把该手机的闪光灯一并打开!!
其中,mCameraCaptureSession.setRepeatingRequest(mCaptureRequest, null, mBackgroundHandler);
表示真的执行打开预览的操作!(第二个参数可以为null的原因是我又不拍照动作,所以可以为空!)
(具体后面再研究)
(4.23)Camera2录像(其实也是预览),点按钮开始,点按钮结束,整个过程生成一个mp4文件放到某个地方:
(其实跟预览的机制很相似!记住:你让高、宽设置为640、480就可以了!)
(4.24)txt转excel
(4.25)打时间戳计时间(System.currentMillis……)
(4.26)建项目时可以自定义包名
(4.27)applicationId、versionId、label (application不一样就可以安装多个apk!)
String applicationName = null; String version = null; try { PackageInfo pkg = getPackageManager().getPackageInfo(getApplication().getPackageName(), 0); applicationName = pkg.applicationInfo.loadLabel(getPackageManager()).toString(); version = pkg.versionName; } catch (Exception e) { e.printStackTrace(); }
(4.28)acc数据:
开启:
sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); sensorManager.registerListener(sensorEventListener, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), ACC_RATE);
监听者对象重写的方法:
@Override public void onSensorChanged(SensorEvent event) { if (isStartTesting == 1) { AccData accData = new AccData(System.currentTimeMillis(), event.values[0], event.values[1], event.values[2]); if (null != allAccDataList) { allAccDataList.add(accData); allAccDataCount++; perSecondAccCount++; } } }
(4.29)java.util.ConconcumentModification Exception(遍历集合时)
一句话概括就是:某个集合在动态变化时,不能遍历它,否则会报这个错!
当然,你可以list.get(index),即你可以具体的获取某个索引处的值,但不能遍历它!
如果你想遍历,你可以tmp = list.clone(); 记得用完之后释放tmp!如果遍历tmp时做的
事情比较耗时,要想到用子线程去处理!
(4.30)释放资源
比如IO流
(4.31)clone()方法
(4.32)double(科学计数法)转String
new BigDecimal(1.1111111).toPlainString()
new DecimalFormat("#,##0.0000000").format(1.1111111111)
(4.33)集合转数组(用这种方式,不要自己去遍历!编程规范!)
list.toArray(new String[20]);
(4.34)文件名错误
带 / : 等等,会报错!
(4.35)sharePreference(可以实现app第一次运行它,记住这个配置,以后进来就
可以利用这个配置了!)
(4.36)ConcurrentHashMap、ConcurrentSkipListSet:
什么时候使用这两个家伙?: 当多个线程之间要用到共享变量,且这个变量
是个集合变量时,首选它们两个!!
因为不仅线程安全而且性能更好!!
原理:
(4.37)线程池的用法():
什么时候考虑用它?: 如果你的代码频繁创建线程处理任务时,则要想到他!
怎么用?:
(4.38)线程池使用:
提前创建线程池,如果有下面那条语句,则会提前创建好SET_CORE_POOL_SIZE个子线程:
threadPool = new ThreadPoolExecutor(SET_CORE_POOL_SIZE, SET_MAXIMUM_POOL_SIZE, SET_KEEP_ALIVE_TIME, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<Runnable>(SET_WORK_QUEUE_COUNT)); threadPool.prestartAllCoreThreads();//这个很重要!提前创建好线程,随时“恭候”你的任务!
以后需要用到子线程来处理任务的时候就:(其中execute中的参数就是任务!!)
(而任务就会交给线程池中的某个子线程来处理!):
threadPool.execute(new ImageProcessor(imageData));
关闭时,释放资源池资源:
if (null != threadPool) {
threadPool.shutdown();
threadPool.shutdown();
}
***注意:
第一,threadPool.getPoolSize(); 获取当前线程池已经创建好了多少个线程了!
第二,threadPool.getActiveCount(); 获取当前池中有多少个活跃线程!
第三,到底coreSize设置为多少合适,可以通过上面两个方法去看结果!
(4.39)已经做到的编程规范:
第一,对于需要频繁创建子线程的地方改用线程池解决,防止OOM;
第二,"\r\n"不能跨平台,所以改成: System.getProperty("line.separator"),
不过,java中有效,android中失效了;
第三,不能看到直接的数字,都改成比如: SET_CORE_POOL_SIZE;
第四,在变量调用方法之前,一定要判断这个变量为不为空,比如:
if (null != list) {
list.add(xx);
}
常见的有: 遍历某个集合前判空、关闭某个东西前判空、
某个变量调用toString()方法、……
第五,switch中break语句;
第六,类注释、方法注释、语句注释、代码根据逻辑适当换行;
(4.40)一些工作能力:
第一,开会的会议纪要;
第二,串讲代码要画流程图;
第三,发邮件展示结果时,一定要让人家看到标题就知道你的主要内容、
附件中一定要有详细的说明、以及让人一眼就可以看到结果的
东西,比如图表、图形、视频等!(不能让上级需要花时间去理解
你在写什么!);
(4.41)手机电脑文件传输的方法:
第一, adb pull /sdcard/heartrate d:test/heartrate
第二,如果在手机上可以找到某个文件,但它在手机里的具体路径很难定位
甚至定位不到,则可以在手机上把这个文件挪到手机根目录下的
比如111111111文件夹中,然后: adb pull /sdcard/11111111 d:test/
(4.42)Camera2录像的实现(参考4.23),这里讲一下核心实现:
第一,程序的UI,只要一个TextureView和一个Button即可,那个按钮就是点击
录像的按钮,点击后它就变灰,过10秒钟后就停止了!
第二,程序一上来,就开启了预览,不过这次的预览并没有ImageReader对象,
这个很容易实现,不介绍了!(就是在之前说过N遍的开启预览那里去掉
new ImageReader那一步即可!),当然,createCameraPreviewSession()
,这一步要稍微改动一下,后面说;
第三,当你按下录像按钮后,最重要的步骤就是,把原来的session连接断了,然后
自己开一个新的session连接,原来是CameraDevice.TEMPLATE_PREVIEW
而现在要改成CameraDevice.TEMPLATE_RECORD模式!
第四,待10秒钟后,又重新回到之前的预览状态(即断了刚刚的录像session连接,
重新建立一个新的预览session连接)!
****注意(难点在于):
第一,由于经常要涉及到session连接的切换,所以,在onOpened()方法中的
那个建立session连接的方法即createCameraPreviewSession()应该加个
参数,用以标记创建哪种类型的session连接!
第二,这个方法为:
private void createCameraPreviewSession(String sessionType) { if (null == mCameraDevice || !textureView.isAvailable()) { return; } if (mCameraCaptureSession != null) { mCameraCaptureSession.close(); mCameraCaptureSession = null; } switch (sessionType) { case PREVIEW_TYPE: { try { SurfaceTexture surfaceTexture = textureView.getSurfaceTexture(); if (null != surfaceTexture) { surfaceTexture.setDefaultBufferSize(previewResolutionWidth, previewResolutionHeight); } Surface surface = new Surface(surfaceTexture); //拿到预览的builder对象 mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mCaptureRequestBuilder.addTarget(surface); //试图建立session会话 mCameraDevice.createCaptureSession(Collections.singletonList(surface), new CameraCaptureSession.StateCallback() { //可以理解成:如果建立Session会话成功; @Override public void onConfigured(CameraCaptureSession session) { if (null == mCameraDevice) { return; } //拿到该session会话对象; mCameraCaptureSession = session; //设置一些builder的属性; mCaptureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);// // Range<Integer> fpsRange = Range.create(SET_FPS, SET_FPS); // mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);//设置预览时每秒产生帧的范围; //mCaptureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);//开启闪光灯; //通过builder对象产生一个具体的请求对象; mCaptureRequest = mCaptureRequestBuilder.build(); //执行预览; try { mCameraCaptureSession.setRepeatingRequest(mCaptureRequest, null, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } } //可以理解成:如果建立Session会话失败; @Override public void onConfigureFailed(CameraCaptureSession session) { Toast.makeText(StressMonitor.this, "Failed creating camera capture session", Toast.LENGTH_SHORT).show(); } }, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } break; } case RECORD_TYPE: { try { setUpMediaRecorder(); SurfaceTexture surfaceTexture = textureView.getSurfaceTexture(); if (null != surfaceTexture) { surfaceTexture.setDefaultBufferSize(previewResolutionWidth, previewResolutionHeight); } //拿到预览的builder对象 mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD); List<Surface> surfaces = new ArrayList<>(); Surface textureSurface = new Surface(surfaceTexture); surfaces.add(textureSurface); mCaptureRequestBuilder.addTarget(textureSurface); Surface recorderSurface = mMediaRecorder.getSurface(); surfaces.add(recorderSurface); mCaptureRequestBuilder.addTarget(recorderSurface); //试图建立session会话 mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() { //可以理解成:如果建立Session会话成功; @Override public void onConfigured(CameraCaptureSession session) { if (null == mCameraDevice) { return; } //拿到该session会话对象; mCameraCaptureSession = session; //设置一些builder的属性; mCaptureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO);// // Range<Integer> fpsRange = Range.create(SET_FPS, SET_FPS); // mCaptureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange);//设置预览时每秒产生帧的范围; mCaptureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);//开启闪光灯; //通过builder对象产生一个具体的请求对象; mCaptureRequest = mCaptureRequestBuilder.build(); timer = new Timer(); task = new TimerTask() { @Override public void run() { taskCount++; if (taskCount >= 1) { Log.i(TAG, "stop record:" + System.currentTimeMillis()); Log.i(TAG, "real time=" + (System.currentTimeMillis() - recordTime)); timeoverToStopRecord(); } } }; //执行预览; try { mCameraCaptureSession.setRepeatingRequest(mCaptureRequest, null, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } timer.schedule(task, 10000, 10000); //开始录像 mMediaRecorder.start(); recordTime = System.currentTimeMillis(); Log.i(TAG, "start record:" + recordTime); } //可以理解成:如果建立Session会话失败; @Override public void onConfigureFailed(CameraCaptureSession session) { Toast.makeText(StressMonitor.this, "Failed creating camera capture session", Toast.LENGTH_SHORT).show(); } }, mBackgroundHandler); } catch (CameraAccessException | IOException e) { e.printStackTrace(); } break; } default: break; } }第三, 设置录像的那个对象:
private void setUpMediaRecorder() throws IOException { mMediaRecorder = new MediaRecorder(); mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); //设置保存在哪个路径! if (mNextVideoAbsolutePath == null || mNextVideoAbsolutePath.isEmpty()) { File sdcard = Environment.getExternalStorageDirectory(); File myFolder = new File(sdcard, "/camera2videos"); myFolder.mkdirs(); if ((!sdcard.exists()) || (!myFolder.exists())) { return; } String applicationName = null; String version = null; try { PackageInfo pkg = getPackageManager().getPackageInfo(getApplication().getPackageName(), 0); applicationName = pkg.applicationInfo.loadLabel(getPackageManager()).toString(); version = pkg.versionName; } catch (Exception e) { e.printStackTrace(); } String timeStamp = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss").format(new Date()); String phoneFactory = android.os.Build.BRAND; String phoneModel = android.os.Build.MODEL; String phoneRelease = android.os.Build.VERSION.RELEASE; String name = "/" + applicationName + "_" + version + "_" + phoneFactory + "_" + phoneModel + "_" + phoneRelease + "_" + timeStamp + "_camera2video" + ".mp4"; File file = new File(myFolder, name); mNextVideoAbsolutePath = file.getAbsolutePath(); } mMediaRecorder.setOutputFile(mNextVideoAbsolutePath); mMediaRecorder.setVideoEncodingBitRate(10000000); mMediaRecorder.setVideoFrameRate(SET_FPS); mMediaRecorder.setVideoSize(previewResolutionWidth, previewResolutionHeight); mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); int rotation = this.getWindowManager().getDefaultDisplay().getRotation(); switch (mSensorOrientation) { case SENSOR_ORIENTATION_DEFAULT_DEGREES: mMediaRecorder.setOrientationHint(DEFAULT_ORIENTATIONS.get(rotation)); break; case SENSOR_ORIENTATION_INVERSE_DEGREES: mMediaRecorder.setOrientationHint(INVERSE_ORIENTATIONS.get(rotation)); break; } mMediaRecorder.prepare(); }
(4.43)
(4.44)
(4.45)
(4.46)
(4.47)
(4.48)
(4.49)
(4.50)netty知识: