#1 总体实现思路:
(1.1)测试目的是:要测试手机帧率(每秒钟拍多少张照片!)(如果用apk调用相机进行预览
,则每秒钟拍多少张照片是需要手动用代码设置的,问题是:假设你设置
成30,但是实际上每秒钟不一定是拍到30,很有可能拍到27或33等!
而且,不同的手机之间差异很大!)
(1.2)测试思路:当打开预览之后,每拍到一张新图片都会执行onImageAvailable()方法一次,
所以,在该方法中起一个子线程ImageProcessor,在这个子线程中做的操作
是让perSecondImageCount++,所以,只要我们开一个计时器,让它每隔1秒钟
执行一次帧率计时器子线程,执行的内容就是拿到此刻的perSecondImageCount
的值展示……,并且立即让perSecondeImageCount置为零,让它统计下一秒的!
这样不就实现了:每隔1秒钟我就可以统计这1秒钟内实际拍到了多少张图片!
#2 收获的知识:
(2.1)动态申请各种权限:
第一,时机:一般是在app第一次安装时提醒用户把所有该app需要的权限都申请完,再打开
app的第一个页面!并不是在用到某个权限时才去申请!(可以这样:第一次
安装app时把所有的权限都申请完,强制用户每项都选允许;以后每次要
用到某个权限时再检查一次即可!)
第二,用法:安卓中对于android6.0以及6.0以后的才需要动态申请权限,其他的不需要动态
申请,意思是可以直接用,默认可以调用所有权限!
android版本和api level对应关系(比如android4.4------api 19):
http://blog.csdn.net/only2xlr/article/details/49510415
判断要不要动态申请:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) 表示6.0及以上!
判断是否具有了某个权限(表示如果不具有存储权限,则……):
if (ContextCompat.checkSelfPermission(MainActivity.this, MEMORY_PERMISSION) != PackageManager.PERMISSION_GRANTED)
弹出动态申请某个权限框的方法(执行完这个方法后会有如下现象)
(用户看到一个弹出框,系统会执行该Activity的onPause()方法、
系统会执行你重写好的onRequestPermissionsResult()方法):
ActivityCompat.requestPermissions(MainActivity.this, new String[]{MEMORY_PERMISSION}, MEMORY_REQUEST_CODE);
接下来看onRequestPermissionsResult()方法怎么响应用户的选择:
首先根据request_code判断你是哪个框的,然后(用户选了禁止就继续弹框):
if (grantResults.length > 0) { if (grantResults[0] < 0) { //表示用户刚刚选了禁止; ActivityCompat.requestPermissions(MainActivity.this, new String[]{MEMORY_PERMISSION}, MEMORY_REQUEST_CODE); } else { //表示用户刚刚选了允许; } }
(2.2)handler的使用场景之一以及具体使用方法:
第一,我这里遇到的场景:在onCreate()本来有两个方法: initMyLog();和firstEnterApp();
前者方法的目的是想在手机上创建一个文件,以便在以后的执行
中把过程log写到该文件中去!但是你想要创建文件就必须先
动态申请存储权限,所以,在initMyLog()执行之前,必须先
申请到了存储权限才行,否则会报错!
很容易想到这样解决:
onCreate() {
ActivityCompat.requestPermissions(存储权限);
initMyLog();
firstEnterApp();
}
但是问题来了,申请权限的时候不是会触发onRequestResult()
方法吗?只有用户选了允许之后,执行完onRequestResult()方
法之后,才算是真正申请到了存储权限!! 但是,系统哪里会
等你那么久,系统在执行完ActivityCompat.reque……()方法之后,
立马就会执行initMyLog(),而此时是没有存储权限的,所以,
会崩了!因为在initMyLog()中有在手机上写文件的代码,而实际
上还没有存储权限呢!!
第二,上面的场景总结:试想一下,如果在执行完onRequestResult()方法后,并且用户选了
允许存储权限之后再执行initMyLog()和firstEnterApp()方法,则
肯定没有问题吧! 但是如何做到呢?即怎么做到:让一个不确定
也不能控制时间顺序的回调方法执行结束之后再执行我们的某些
方法呢??
第三,场景再总结: 怎么保证y方法一定在x方法执行完了之后才会被执行?
而且x方法什么时候执行我们无法控制!!
第四,解决上面的场景:Handler登场了,对它来说,轻而易举!!!(这里先只讲一下
handler怎么处理上面那个场景,它怎么处理的即原理后面弄懂了
再讲!!)
第五,handler如何解决上面的场景:
目前这样理解,handler可以发送消息、也可以接收消息,假设对于同一个
handler,它发出了一条Message,不久后它便可以收到这条消息,然后做
相应的处理!!
我们可以这样:在MainActivity中先声明一个全局变量handler, 注意在new
handler对象时可以重写handMessage()方法,即处理消息的方法;然后
我们的思路是: 在onRequstResult()方法中用户选择了允许存储权限
之后,我们用这个handler发一条消息! 然后之前在new这个handler的
时候重写handleMessage()方法,并且处理消息的内容是在这里面
调用initMyLog()方法和firstEnterApp()方法!! 这样不就实现了??
具体如下:
class MainActivity {
private Handler handler = new Handler() {
//重写handleMessage(Message msg) {
if (msg.what == 1) {
initMyLog();
firstEnterApp();
}
}
};
定义initMyLog()方法{}
定义firstEnterApp()方法{}
onCreate() {
//申请存储权限;
}
onRequestResult() {
//确保用户选了允许存储权限之后发送一条handler消息:
Message msg = new Message();
msg.what = 1;
msg.setTarget(handler); //设置让谁发送!
msg.sendToTarget(); //发送的意思
}
}
第六,handler解决上面场景的原理:
(2.3)打Log的重要性以及技巧:(见2.9)
(2.4)runOnUiThread()的适用场景:
个人理解:这个一般用在这样的场景下:就是你在一个子线程中(非主线程的子线程),
如果你想做一个更改主线程UI的操作,则如果直接操作则会报错! 但如果加上这个就ok!
(2.5)Camera2、以及TextureView、ImageReader控件的使用:
Camera2怎么打开预览的过程笔记上写得很详细了,这里就不介绍了,这里假设你已经
成功打开了预览了,而预览的过程其实就是在连续地拍照,后两者的作用是: 每拍到
一张图片时,会把这张图片立马存到TextureView的Surface上,然后ImageReader可以
立马从TextureView上获取到那张图片,获取到了图片之后你可以count++、处理图片等
操作(比如yuv->rgb等)!! 所以,在打开预览时会有代码把TextureView和
ImageReader这两个家伙绑定! 而且事先会给ImageReader加一个监听者,
一旦你帮他们两个家伙绑定了,开始预览后,每来一张图片,则imagReader的监听者就
可以监听到TextureView的Surface改变了,即拍到了新的图片,那么,imagReader的监听
者就会执行它的方法onImageAvailable(),这个方法是你事先写好的,里面就可以写
当来了新图片时我们做的操作了!!
所以,大约有这样的逻辑:
a, TextureView和ImageReader这两个家伙绑定;
b, imageReader.set(监听者);
c, 而这个监听者对象主要监视和reader绑定的TextureView有没有新的图片;
d, 这个监听者有个方法: onImageAvailable(ImageReader reader) ,在这个
方法中,我们一般是这样: Image image = reader.acquireLatestImage();
处理图片;
image.close();
e,而预览的过程中每拍到一张新的图片,TextureView的Surface都会更新,
则会被imageReader的监听者监视到,所以会执行它的onImageAvailable()方法!!!
注意几个问题: 在哪个地方设置预览时到底每秒钟拍多少张图片呢?
实际上只有一个地方,就是在建立预览请求时设置的!不过我们在开启
预览之前一般还会设置另一个参数,就是iamgeReader的一个参数,在
开启预览之前,我们在new它的时候,一般会把maxImage也设置为刚刚那个
数!即: 假设setfps=30,则imageReader变量的maxImages参数也设置为
30!表示: 让预览时每秒钟拍30张照片,而且让imageReader最多每秒钟
会去TextureView上检查30次!
目前发现有一个这样的现象:即用reader调用了acquireLatestImage()方法后,
不是得到一张图片吗,你用完了这张图片之后一定要image.close()之后,才可以
再一次用reader调用acquireLatestImage()方法,否则会报错!!
(2.6)多线程的引入原因、引入需要注意的事项、通讯方式:
在2.5的基础上进一步分析:我们知道来了一张图片之后我们处理图片的代码
肯定在onImageAvailable()中写吧!一般是让perSecondImageCout++!问题来了,
假设这个处理图片的代码比较复杂,比较耗时间,那么,假设你之前设置成预览时每秒钟
拍30张照片,那么,理论上来说每秒钟内会执行30次onImageAvailable()方法,但是,
由于onImageAvailable()方法中的逻辑代码比较耗时,导致可能最终只执行了22次该方法!
因此,这样可能会影响我们的结果!!
事实发现,如果每次执行onImageAvailable()方法时,如果每次都开启一个子线程来处理,
则可以“省时”,即可以最终执行到30次!!
但是问题又来了,有些时候可能会出错,因为每个子线程的代码都是这样:
{
Image image = reader.acquireLatestImage();
处理图片;
image.close();
}
有可能出现这样的情况: 子线程1刚运行到处理图片,还没有运行完它的image.close()方法,
这个时候cpu被子线程2抢到了,子线程2就运行了reader.acquierLatestImage();语句,则
崩了!!(即多线程虽然提升了效率,但是却带来了安全问题,因为多线程情况下运行
时机的不确定性,所以,,,,)
这个时候,我们就得想办法实现: 一定要让第一个子线程的方法执行完了之后才可以
开始执行第二个线程! 我尝试过这两种方案,第一是在每个子线程开启之后加这个语句:
thread.start(); thread.join(); 则可以保证,但是效率又降低了,即假设你设置成fps=30,
则现在onImageAvailable()方法只能执行大约22次左右!! 另外一种方案是用线程池
中的单个线程来处理,即不用每次都new一个子线程,而是从线程池中取同一个线程来
处理,这样肯定可以保证这个线程第一次运行完、再运行第二次、再运行第三次、……
但是效率仍然是22!
目前采用的方法是用线程池:使用方法如下:
先定义一个线程池对象(Single的意思是这个从里面取线程时每次取的都是同一个线程)
private ExecutorService mSingleThreadPool = Executors.newSingleThreadExecutor();
然后你定义一个类实现Runnable(),并重写run()方法,在里面写好处理的逻辑代码;
然后在onImageAvailable()方法中只需要:
mSingleThreadPool.execute(new MyRunnable()); 即可!
**目前各线程之间的通讯方式是 共享同进程下的 全局变量!!
(2.7)adb介绍以及常用的一些命令:
adb介绍:
adb常用命令:
adb devices 查看和该电脑连接上的手机有哪些!
adb kill-server
adb start-server
adb install 将电脑上的某个apk文件拖过来, 然后回车!(装apk到手机上)
adb -s 设备号 install -r -d (电脑上连有多个手机、replace、delete)
adb pull /sdcard/fps d:/java (将手机根目录下的fps文件夹复制到电脑上)
(注意:事先在电脑上d盘下建一个名字叫做java的文件夹!)
(注意:这里的 /sdcard 目录是指 手机内置的内存根目录,不是我们装上去的卡!)
adb push /d:/java /sdcard/ (将电脑上d盘下的java文件夹复制到手机根目录!)
(2.8)简单windows脚本介绍:
首先设置一下后缀名可见:随便打开一个文件夹,组织,文件夹和搜索选项,查看,……
然后随便建一个文本文档,里面的内容等下说,然后改成.bat格式,那么,在windows
下双击这个文件就相当于在dos窗口中执行那里面的语句了!
所以,这个文件的内容可以是: adb pull /sdcard/fps d:/java等内容!
不过,貌似目前没有成功!
(2.9)假设把你的apk拿给别人测试你的Log要达到的要求:
要提前做好你的apk可能会崩掉的准备,那么,你就得提前做好Log了!
首先如果你自己有写文件记录过程日志,最好在过程日志中如下:
try { processBw.write("[" + new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss").format(new Date()) + "]----------" + "[" + android.os.Process.myPid() + "]----------" + "[" + android.os.Process.myTid() + "--------]----------" + "[ImageReader.OnImageAvailableListener类]----------" + "[onImageAvailable()方法]----------" + "[isStartTesting == 1]" + "\r\n"); processBw.flush(); } catch (IOException e) { e.printStackTrace(); }
即:时间+进程id+线程id+类名+方法名+描述
这样,就算崩了可以知道你运行到了哪里之后崩掉了!
另外,假设有些地方的执行,还没等到你创建你的日志文件,则你肯定要提前
在那些地方用Log.i写好过程语句!这样,就算你的代码很早就崩了,起码测试人员
可以帮你抓到Android Log信息,你可以根据这个查到在哪里崩的!
上面只是保证了有过程Log,但是错误信息呢?在你的代码中如果有try catch的地方,
一定要在catch的Exception那里,加上Log.i(TAG, e.toString());这样,测试人员帮你
抓到的Android Log中就可以看到具体的错误信息了!
(2.10)android中文件创建的方式参考:
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 processName = "/" + phoneFactory + "_" + phoneModel + "_" + phoneRelease + "_" + timeStamp + "_fps" + "_PROCESS" + ".txt"; String resultName = "/" + phoneFactory + "_" + phoneModel + "_" + phoneRelease + "_" + timeStamp + "_fps" + "_RESULT" + ".txt"; File sdcard = Environment.getExternalStorageDirectory(); File myFolder = new File(sdcard, "/fps"); myFolder.mkdirs(); if ((!sdcard.exists()) || (!myFolder.exists())) { return; } try { processTxt = new File(myFolder, processName); resultTxt = new File(myFolder, resultName); //可以追加! processFos = new FileOutputStream(processTxt, true); resultFos = new FileOutputStream(resultTxt, true); processBw = new BufferedWriter(new OutputStreamWriter(processFos)); resultBw = new BufferedWriter(new OutputStreamWriter(resultFos)); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); }
(2.11)编程规范中自己经历的一些比较重要的:
#3 遇到的大问题:
(3.1)问题1--
现象:将fps设置为1-200,在nova2手机上运行ok,但在honor6x、nubia、小米手机都直接崩,
fps设置成2时,基本没有问题,但有些时候有些手机会崩(虽然概率很小,但一定会出现)!
原因:就是每执行到onImageAvailable()时,都开启一个新的子线程,然后由于第一个子线程的
image.close()还没执行完,第二个子线程就执行了reader.acquireLatestImage()方法,所以,
崩了!!
解决: 用线程池中的单个线程处理图片;
(3.2)问题2--
现象:将fps设置成2,在nova2上运行正常,小米手机运行一轮就崩、honor6x手机也是运行一轮
就崩!(运行一轮的意思是:运行完setfps=2后准备运行setfps=3时,立马崩!)
原因: 每次运行完一轮不是要关闭预览嘛,即会让reader.close();并且 reader=null; 接着又会进行
下一轮的测试,即又要重新开启预览,所以,reader=new ……;
可以理解成每一轮都要这样:
{
iamgeReader = new ImageReader();
测该轮;
imageReader.close(); imageReader = null;
}
问题就在: 上一轮结束了,还没来得及关闭reader,而下一轮就开始了,即下一轮的
reader已经new好了,而这个时候才执行上一轮的关闭reader,这就导致下一轮
的reader已经为空了,而下一轮的reader在使用的时候就为空了!(这还是多线程抢cpu
引起的问题其实!!)
解决: 两轮之间隔久一点!确保上一轮关掉了,再执行下一轮的!
或者可以考虑两轮之间用不同的reader对象!!
(3.3)问题3--
现象:其他手机其他场景都正常,nubia手机一打开立马崩!
原因:文件权限问题,见#2的2.2
解决:
(3.4)问题4--遗留问题
现象:测试fps的demo在处理图片时,只要让perSecondImageCount++即可,所以,
在这里用线程池的单线程处理,效率仍然可以达到设置成30实际也是30;
但是将来测心率的demo中处理图片时就要加上yuv->rgb操作了,这样就比较
耗时,所以,设置成30可能实际只有22,这样就少了好多! 那么,你就得考虑
到底要效率但可能出现bug;还是要安全,宁愿少好多图片!
原因:
解决:
(3.5)问题5--
现象:
原因:
解决:
#4 感悟:
(4.1)出现bug时怎么办?:
**切忌:遇到bug时就根据现象去猜测原因,凭经验去揣测错误原因!!!
(这条是目前自己最大的最严重的一个问题,在学校写代码可能根据你的
经验可以才出来,但是企业中的代码你会遇到无限多的从来没经历过的
问题,,, 另外,猜测出的问题很浪费时间,而且根本不是解决问题
的方法!!!)(在#3中我故意强调各个手机不一样,故意告诉你
报的错跟现象完全不搭边啊,你怎么猜!!)(如果你根据现象去猜
则你就会想:不同的手机运行却不一样,这肯定是硬件的问题,跟我
的代码没有关系!! 实际上就是你的代码的问题!!
多线程之间共享变量时特别需要注意!!!所以不要凭猜!)
**怎么办?: 第一,一定、一定要定位到具体崩在哪一行:方法很简单,在这行代码
的前后、被调用地方等打上详细的log,比如进程id、线程id等也
加上……
第二, 一定、一定要得到具体的错误原因,
比如,你可以在这行崩的代码上加上try catch,然后在
catch(Exception e)中把e打印出来!
第三,当得出具体的错误原因之后,可能你对它完全陌生,甚至根本没法
分析,不要怕,接下来你可以两种方式同时进行,第一,在网上查
该错误,英文的不用怕,多尝试看英文的,百分之七十看不懂错误
原因,不怕,接下来,你在崩的这行所在的方法中打上具体的log信息,
比如,该方法一进来时间进程id线程id、该方法出去的……、该行
前后的语句的具体log、…… 结合log情况再去看那个出错原因,
很有可能可以知道为什么!!
第四,如果上面还没解决,则在这个基础上再去问别人,就很清晰了!
**举例:
(4.2)提高开发效率的方式:(及时交流、不要秀操作)
遇到问题不要自己闷在那里解决,及时反馈,说不定人家一眼就知道怎么
解决,或者提出很好的优化方案!所以,多交流、多对对!