0%

有个需求是把原生的日历app换成修改源码后编译生成的新app
尝试进入adb,须有root权限(完整的root权限)
用以下代码删除原生的calendar

1
2
3
adb shell
su
pm uninstall -k --user 0 com.android.calendar

然后要把/system挂载成可读写的(rw,一开始是只读的ro)
执行

1
# mount

可以发现/system是只读的 ro
然后执行

1
2
3
sudo su
mount -o rw,remount /system
mount

可以看到/system 是可读写的:rw
此时退出adb shell,把已经编译好的新的日历apk push进手机

1
2
3
adb push ~/newapk/Calendar.apk /system/app/Calendar
adb shell
reboot //重启手机

此时就可以使用改后的calendar了
————————————————
版权声明:本文为CSDN博主「TheRockMaster」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43752854/article/details/84647709

任务3. 在 Android 平台使用 Camera API 进行视频的采集,分别使用 SurfaceView、TextureView 来预览 Camera 数据,取到 NV21 的数据回调

由于这次的任务牵涉面十分广,所以用了很久才搞懂了一些知识,以后的任务也应该会做的越来越慢,不过十分合理,慢工出细活,上来一会儿就完成的东西,要不就是含金量不高,要不就是没有深究,对于学习阶段,虽然说有时候要管中窥豹不要太深究,但有些事情不搞清楚就不能算掌握知识了对吧?

下面是使用CameraAPI进行拍照和录视频,当然还有预览的全过程。虽然CameraAPI在5.0后就被弃用了(原因之一是它不能同时预览和拍摄),取而代之的是camera2,但由于众所周知的原因,仍然要好好学习。

添加权限后,如果是android6以上的,必须在设置的应用里面手动打开申请的权限

1
2
3
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="true"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>

相机的角度,如果不加调整的打开相机,默认的角度是0°,(这一点很奇怪。。)一般竖屏状态下是90度,横屏则为0或180度(应该是取决于传感器的方向)

拍摄照片

使用Camera拍摄照片的步骤:
要使用此类拍摄照片,请使用以下步骤:

  1. 从open(int)获取Camera的实例。(首先使用以下方法获得设备摄像头数目)
    int cameraNum = Camera.getNumberOfCameras();
    //上面open的有效取值是 0 ~ cameraNum-1 代表了摄像头的设备id
  2. 使用getParameters()获取现有(默认)设置。如有必要,修改返回的Camera.Parameters对象并调setParameters(Camera.Parameters)
  3. 调用setDisplayOrientation(int)以确保正确的预览方向。
    重要提示:将完全初始化的SurfaceHolder传递给 setPreviewDisplay(SurfaceHolder)。没有surface,相机将无法启动预览。
    重要说明:调用startPreview()开始更新预览曲面。必须先开始预览才能拍照。
  4. 如果需要,可以调用takePicture(Camera.ShutterCallback,Camera.PictureCallback,Camera.PictureCallback,Camera.PictureCallback)来捕获照片。等待回调提供实际的图像数据。
  5. 拍照后,预览显示将停止。要拍摄更多照片,请先再次调用startPreview()。
  6. 调用stopPreview()以停止更新预览surface。
    重要提示:调用release()以释放相机以供其他应用程序使用。应用程序应立即在Activity.onPause()中释放相机(并在Activity.onResume()中重新打开它)。

一般自定义的来说,surface显示的图像都会被拉伸,或者压扁,反正就是图像比例不对,这是由于surfaceView和Camera.Size不匹配导致的,所以要根据surfaceView的宽高,在Camera所支持的Size里面找一个最合适的

在启用startPreview后,就可以通过Camera.takePicture()方法拍摄一张照片,返回的照片数据通过Callback接口获取。takePicture()接口可以获取三个类型的照片:

  • 第一个,ShutterCallback接口,在快门瞬间被回调,通常用于播放“咔嚓”这样的音效;
  • 第二个,PictureCallback接口,返回未经压缩的RAW类型照片(字节数组);
  • 第三个,PictureCallback接口,返回经过压缩的JPEG类型照片(字节数组);

这三个都可以不实现,第一个一般没啥影响,但第二个第三个不实现则相当于拍的东西就没了。(第二第三个可以选一个实现)调用此方法后,在返回JPEG回调之前,不得调用startPreview()或拍摄另一张照片。

camera.setDisplayOrientation(90); //设定的是预览的方向,预览和数据是独立的,如果只设置了DisplayOrientation,则在surfaceView中显示正常,但保存的数据仍然是0°的

修改拍摄数据角度的两种方法(以下均是竖屏情况,横屏情况不用额外处理,因为默认是横屏情况):

  1. 直接在设置camera参数的时候设置 parameters.setRotation(90); //这样拍摄出的data就是修正过后的数据,直接写到文件里即可
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Override
    public void onPictureTaken(byte[] data, Camera camera) {
    File file = new File(fileName);
    try {
    FileOutputStream os = new FileOutputStream(file);
    os.write(data);
    os.flush();
    os.close();
    } catch (FileNotFoundException e) {
    e.printStackTrace();
    } catch (IOException e) {
    e.printStackTrace();
    }

    if (mCamera != null) {
    mCamera.startPreview();
    }
    }
  2. 不设置相机的Rotation,这样得到的数据是横屏情况下的,要使用Matrix和Bitmap进行修正
    parameters.setPreviewFormat(ImageFormat.NV21); //默认预览帧格式为NV21,注意很多格式设备可能不支持,程序就会崩,如nexus6p不支持YUY2

录制视频

使用surfaceView

  1. 像上面一样获得camera的实例并初始化它,并开启preview
  2. 调用unlock()允许mediaRecorder的工作线程使用设定好参数的camera
  3. 把camera实例传给MediaRecorder.setCamera(Camera)
  4. 设置用于预览的surface:MediaRecorder.setPreviewDisplay(如果camera没绑定surface,则此步骤会替换camera的surface为本参数)
    //以下为设置录制的参数
    设置视频源setVideoSource: 一般为摄像头
    设置音频源setAudioSource:一般是CAMCORDER(单纯录视频是没声音的) 如果是用mediaRecorder单录音频的话,一般用MIC
    设置输出文件格式:setOutputFormat
    设置视频的角度:setOrientationHint //预览角度在camera的参数里设置
    //下面的必须要放在输出格式确定后
    设置视频编码:setVideoEncoder
    设置音频编码:setAudioEncoder
    //下面的必须要放在编码确定后
    设置视频分辨率:setVideoSize; 这里的分辨率必须要选择设备摄像头支持的分辨率,否则报错(Camera.getParameters().getSupportedPreviewSizes()的值之一)
    设置录制视频的捕获帧速率:setVideoFrameRate(); //必须要选择摄像头支持的帧率,否则报错(mCamera.getParameters().getSupportedPreviewFrameRates()的值之一)
    设置所录制视频的编码位率:setVideoEncodingBitRate(3 * 1024 * 1024);
    设置记录会话的最大持续时间(毫秒)setMaxDuration(30 * 1000);
    设置输出文件的路径:setOutputFile
  5. 当完成录制后,调用camera.reconnect把camera的使用权从mediaRecorder的工作线程转回到设置了它的本线程
  6. 调用mediaRecorder调用stop和release 释放资源
    一些总结性的概述:
    surfaceView 只是一个用来占位置的控件,是用来显示surface内容的,它会显示它绑定的SurfaceHolder所持有的surface, 真正用于显示的是surface(它是具体的要显示的数据),surface的持有者是SurfaceHolder,每个surfaceView生来就有一个默认与其绑定的SurfaceHolder,通过getHolder来获取,SurfaceHolder会决定如何去显示surface,每个surface有且只有一个SurfaceHolder,
    surfaceView:画布
    surface:画的内容
    surfaceHolder,画内容的持有者
    surfaceView、surfaceHolder、surface三位一体,是一个不可分的整体

一个camera设置好参数,它本身是被本线程持有的,持有的话是被lock的,mediaRecorder的工作是在单独的线程中完成的
使用setCamera给mediarecorder设置一个被设置好相机。而如果不调用setCamera设置相机的话,分配给的是一个默认情况下的相机,也就是说什么参数都没有被设置,这样一般不可能满足拍摄需求,因此一般要给mediaRecorder设置camera

在mediaRecorder.start之前,它的camera必须要调用unlock(如果设置了的话),因为mediaRecorder的工作是在单独的线程中完成的,而camera默认是被创建它的线程所持有的,这样mediaRecorder的工作线程无法使用它),如果start顺利结束,则会自动调用lock归还camera,如果start调用失败,则要手动lock来回收camera的所有权

mediaRecorder.setPreviewDisplay(Surface s)
给mediaRecorder设置surface,surface是用来显示mediaRecorder的相机的预览。如果s已经被一个设置给了camera(只是设置给了,还没有被持有资源),而且这个camera已经通过mediaRecorder.setCamera被设置给了mediaRecorder,那么这个方法不需要调用。如果s已经被设置给了一个另一个camera,只是设置给了,还没有被持有资源),但这个camera没有被设置给mediaRecorder,那么这个s将会被设置给给mediaRecorder自己的camera(如果没设置,则是默认的camera)。此时mediaRecorder自己的camera和另一个camera被设置了同一个surface.
如果s为空,则完全不会发生任何事


camera.setHolder
给camera设置一个holder,即surface;但此时holder的surface资源并不为camera所持有,只有当开启预览(即startPreview)时,surface资源才会被camera所持有(下面所说的surface和相应的surfaceHolder都是绑定的,彼此一体,surface资源也就相当于holder的资源)

如果对一个camera持有一个surface的资源,再次让另一个camera再次去持有这个surface资源,则会报错(mediaRecorder.start中会调用类似于它自己的camera.startPreview,startPreview会试图去占有surface资源)也就是说,同一个surfaceHolder可以被多个camera设置,但同时只能有一个camera持有它的surface的资源,否则就会报错。可以对同一个camera连续多次试图持有资源(反正资源就是你的,反复持有还是这些).

当用startPreview成功开启预览后,surface的资源就被camera持有,此时单纯调用stopPreview关闭预览并不能释放它所持有的surface资源,必须要用camera.release(完全释放camera),或者直接setDisplayHolder(null)(只释放camera占有的surface资源,不改变camera的其他参数设置)才能释放它占用的surface资源。但不能直接把camera置为null来释放资源,因为这样只能减少它的引用计数,不是真正的释放资源。

mediaRecorder 不会吧自己的surface主动连接到自己设置的camera, 除非是不设置camera而使用默认的camera,因为mediaRecorder会默认自己设置的camera有surface

camera.setDisplayOrientation设置相机预览的角度,mediaRecorder.setOrientationHini设置存储的视频的角度,两者互相独立

使用TextureView

TextureViewView:
TextureView可用于显示内容流。 这样的内容流可以例如是视频或OpenGL场景。 内容流可以来自应用程序的进程以及远程进程。

TextureView只能在硬件加速窗口中使用。 在软件中渲染时,TextureView将不会绘制任何内容。(例如在xml中给textureView设置背景色,就直接会报错)

与SurfaceView不同,TextureView不会创建单独的窗口,而是表现为常规View。 这个关键区别允许它进行移动,转换,动画等。例如,您可以通过调用myView.setAlpha(0.5f)使TextureView半透明。

使用TextureView很简单:您需要做的就是获得它的SurfaceTexture。 然后,把SurfaceTexture用来呈现内容。 以下示例演示如何将相机预览渲染到TextureView中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class LiveCameraActivity extends Activity implements TextureView.SurfaceTextureListener {
private Camera mCamera;
private TextureView mTextureView;

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

mTextureView = new TextureView(this);
mTextureView.setSurfaceTextureListener(this);

setContentView(mTextureView);
}

public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
mCamera = Camera.open();

try {
mCamera.setPreviewTexture(surface);
mCamera.startPreview();
} catch (IOException ioe) {
// Something bad happened
}
}

public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
// Ignored, Camera does all the work for us
}

public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
mCamera.stopPreview();
mCamera.release();
return true;
}

public void onSurfaceTextureUpdated(SurfaceTexture surface) {
// Invoked every time there's a new Camera preview frame
}
}

可以通过调用getSurfaceTexture()或使用TextureView.SurfaceTextureListener来获取TextureView的SurfaceTexture。 重要的是要知道只有在将TextureView附加到窗口(并且已调用onAttachedToWindow()之后)才能使用SurfaceTexture。因此,强烈建议您使用listener在SurfaceTexture可用时进行通知。

值得注意的是,只有一个生产者可以使用TextureView。 例如,如果使用TextureView显示相机预览,则无法使用lockCanvas()同时绘制到TextureView。
如同surface一样,TextureView的surfaceTexture资源同一时间也只能被一个对象所持有

———————————————————————————————————————————————————————————————
SurfaceTexture:
从图像流中捕获帧作为OpenGL ES纹理。

图像流可以来自相机预览或视频解码。从SurfaceTexture创建的Surface可以用作android.hardware.camera2,MediaCodec,MediaPlayer和Allocation API的输出目标。调用updateTexImage()时,将更新创建SurfaceTexture时指定的纹理对象的内容,以包含图像流中的最新图像。这可能导致跳过一些流的帧。

在指定旧版Camera API的输出目标时,也可以使用SurfaceTexture代替SurfaceHolder。这样做会导致图像流中的所有帧都被发送到SurfaceTexture对象而不是设备的显示。

从纹理中采样时,应首先使用通过getTransformMatrix(float [])查询的矩阵变换纹理坐标。每次调用updateTexImage()时变换矩阵都会改变,因此每次更新纹理图像时都应该重新查询。该矩阵将形式为(s,t,0,1)的传统2D OpenGL ES纹理坐标列向量转换为包含区间[0,1]上的s和t到流式纹理中的适当采样位置。此变换可补偿图像流源的任何属性,使其看起来与传统的OpenGL ES纹理不同。例如,可以通过使用查询的矩阵变换列向量(0,0,0,1)来完成从图像左下角的采样,而从图像的右上角进行采样可以通过变换来完成( 1,1,0,1)。

纹理对象使用GL_TEXTURE_EXTERNAL_OES纹理目标,该目标由GL_OES_EGL_image_external OpenGL ES扩展定义。这限制了纹理的使用方式。每次绑定纹理时,它必须绑定到GL_TEXTURE_EXTERNAL_OES目标而不是GL_TEXTURE_2D目标。此外,从纹理中采样的任何OpenGL ES 2.0着色器必须使用例如“#extension GL_OES_EGL_image_external:require”指令声明其对此扩展的使用。此类着色器还必须使用samplerExternalOES GLSL采样器类型访问纹理。

可以在任何线程上创建SurfaceTexture对象。 updateTexImage()只能在包含纹理对象的OpenGL ES上下文的线程上调用。在任意线程上调用可用帧的回调,因此除非特别小心,否则不应直接从回调中调用updateTexImage()。
———————————————————————————————————————————————————————————————

录制流程:
其他步骤均和surfaceView一样,只有第四步有区别,由于此时不能获得surfaceHolder,只要把TextureView的SurfaceTexture绑定到相机即可(给camera设置用于预览的surfaceTexture:camera.setPreviewTexture(代替camera.setPreviewDisplay),同时而且也不需要给mediaRecorder调用setPreviewDisplay)

MediaRecorder:用来录制音频和视频,录制控制基于简单的状态机,如下图:
在这里插入图片描述

使用MediaRecorder录制音频的流程:

1
2
3
4
5
6
7
8
9
10
11
12
A common case of using MediaRecorder to record audio works as follows:
MediaRecorder recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
recorder.setOutputFile(PATH_NAME);
recorder.prepare();
recorder.start(); // Recording is now started
...
recorder.stop();
recorder.reset(); // You can reuse the object by going back to setAudioSource() step
recorder.release(); // Now the object cannot be reused

//这个不需要手动另开线程写数据,体积小使用方便,但很大的一个缺点就是录下的音质不太好。相比之下AudioRecord虽然麻烦一点,但录制的更像是无损音质

application可能希望注册信息和错误事件,以便在录制期间获知某些内部更新和可能的运行时错误。 通过设置适当的监听器(通过调用(到setOnInfoListener(OnInfoListener)setOnInfoListener和/或setOnErrorListener(OnErrorListener)setOnErrorListener)来注册此类事件。为了接收与这些侦听器关联的相应回调,应用程序需要在运行Looper的线程上创建MediaRecorder对象 (默认情况下,主UI线程已经运行了Looper)。

1
2
3
4
5
6
7
8
9
10
11
使用MediaRecorder报错at android.media.MediaRecorder.start(Native Method):很可能是如下原因: 
1.使用下面两个函数但参数不被当前硬件所支持
mediaRecorder.setVideoFrameRate()和mediaRecorder.setVideoSize(videoWidth, videoHeight)
//注释掉后,会使用硬件支持的默认的参数,就不会出错了,
要获得硬件支持的参数用如下的函数:
mCamera.getParameters().getSupportedPreviewSizes()的值之一
mCamera.getParameters().getSupportedPreviewFrameRates()的值之一

2.已经有其他摄像头开启过预览,持有了SurfaceView或SurfaceTexture的surface资源,但停止预览后没有释放资源

另:mediaRecorder.setVideoSize不能设置的太大,否则点击录像后画面会停住
相机预览数据的获取

转自:https://blog.csdn.net/lb377463323/article/details/53338045
首先定义一个类实现Camera.PreviewCallback接口,然后在它的onPreviewFrame(byte[] data, Camera camera)方法中即可接收到每一帧的预览数据,也就是参数data。
然后使用setPreviewCallback()、setOneShotPreviewCallback或setPreviewCallbackWithBuffer()注册回调接口,下面介绍一下这些方法:

1,void setPreviewCallback (Camera.PreviewCallback cb) 

一旦使用此方法注册预览回调接口,onPreviewFrame()方法会一直被调用,直到camera preview销毁

注意,onPreviewFrame()方法跟Camera.open()是运行于同一个线程,所以为了防止onPreviewFrame()会阻塞UI线程,将Camera.open()放置在子线程中运行。

2,void setOneShotPreviewCallback (Camera.PreviewCallback cb) 

使用此方法注册预览回调接口时,会将下一帧数据回调给onPreviewFrame()方法,调用完成后这个回调接口将被销毁。也就是只会回调一次预览帧数据。

3,void setPreviewCallbackWithBuffer (Camera.PreviewCallback cb) 

它跟setPreviewCallback的工作方式一样,但是要求指定一个字节数组作为缓冲区,用于预览帧数据,这样能够更好的管理预览帧数据时使用的内存。它一般搭配addCallbackBuffer方法使用,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
byte[] mPreBuffer = new byte[size];//首先分配一块内存作为缓冲区,size的计算方式见第四点中
mCamera.addCallbackBuffer(mPreBuffer);
mCamera.setPreviewCallbackWithBuffer(Camera.PreviewCallback cb);
mCamera.startPreview();

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (mPreBuffer == null) {
mPreBuffer = new byte[size];
}
mCamera.addCallbackBuffer(mPreBuffer);//将此缓冲区添加到预览回调缓冲区队列中
}

setPreviewCallbackWithBuffer需要在startPreview()之前调用,因为setPreviewCallbackWithBuffer使用时需要指定一个字节数组作为缓冲区,用于预览帧数据,所以我们需要在setPreviewCallbackWithBuffer之前调用addCallbackBuffer,这样onPreviewFrame的data才有值。

总结一下,设置addCallbackBuffer的地方有两个,一个是在startPreview之前,一个是在onPreviewFrame中,这两个都需要调用,如果在onPreviewFrame中不调用,那么预览帧数据就不会回调给onPreviewFrame了

4,void addCallbackBuffer (byte[] callbackBuffer) 

添加一个预分配的缓冲区到预览回调缓冲区队列中。应用程序可一添加一个或多个缓冲器到这个队列中。当预览帧数据到达时并且缓冲区队列仍然有至少一个可用的缓冲区时,这个 缓冲区将会被消耗掉然后从队列中移除,然后这个缓冲区会调用预览回调接口。如果预览帧数据到达时没有剩余的缓冲区,这帧数据将会被丢弃。当缓冲区中的数据处理完成后,应用程序应该将这个缓冲区添加回缓冲区队列中。
对于非YV12的格式,缓冲区的Size是预览图像的宽、高和每个像素的字节数的乘积。宽高可以使用getPreviewSize()方法获取。每个像素的字节数可以使用ImageFormat.getBitsPerPixel(mCameraParameters.getPreviewFormat()) / 8获取。
对于YU12的格式,缓冲区的Size可以使用setPreviewFormat(int)里面的公式计算,具体详见官方文档。
这个方法只有在使用setPreviewCallbackWithBuffer(PreviewCallback)时才有必要使用。当使用setPreviewCallback(PreviewCallback) 或者setOneShotPreviewCallback(PreviewCallback)时,缓冲区会自动分配。当提供的缓冲区如果太小了,不能支持预览帧数据时,预览回调接口将会return null,然后从缓冲区队列中移除此缓冲区。

完整代码:只需在activity里面调用runTask3即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.media.MediaRecorder;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.widget.Button;

import com.example.lll.va.R;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class Task3 implements View.OnClickListener,
TextureView.SurfaceTextureListener,
SurfaceHolder.Callback {
private final static String tag = Task3.class.getName();
private Camera mCamera;
private SurfaceHolder mHolder;
private CameraUtil mCameraUtil;
private Context mContext;
private int height;
private int width;
private SurfaceView surfaceView;
private TextureView textureView;
private static Task3 task3;
private Activity mActivity;
private boolean isSurfaceMode; //切换surfaceview和textureview的演示

static String videoFileName = Environment.getExternalStorageDirectory() + "/test_vedio.mp4";

public static void runTask3(Activity activity) {
task3 = new Task3(activity);
task3.initView();

}

public Task3(Activity activity) {
mActivity = activity;
mContext = activity;
}

private void initView() {
mActivity.setContentView(R.layout.activity_task3);

surfaceView = mActivity.findViewById(R.id.sv_t3);
textureView = mActivity.findViewById(R.id.texture_view_1);

//为了知道surface的生命周期,一般只有surface建好后才开始下一步动作
//surfaceView必须给holder添加callback,TextureView必须给自己添加listener
if (surfaceView.getVisibility() == View.VISIBLE) {
isSurfaceMode = true;
surfaceView.getHolder().addCallback(this);
mHolder = surfaceView.getHolder();
} else {
isSurfaceMode = false;
textureView.setSurfaceTextureListener(this);
}

Button btnTakePic = mActivity.findViewById(R.id.btn_take_pic);
Button btnStartVideo = mActivity.findViewById(R.id.btn_start_video);
Button btnStopVideo = mActivity.findViewById(R.id.btn_stop_video);
btnTakePic.setOnClickListener(this);
btnStartVideo.setOnClickListener(this);
btnStopVideo.setOnClickListener(this);
}

private void getWidthAndHeight() {
width = isSurfaceMode ? surfaceView.getWidth() : textureView.getWidth();
height = isSurfaceMode ? surfaceView.getHeight() : textureView.getHeight();
}

public void initCamera() {
if (mCamera != null) {
mCamera.release();
mCamera = null;
}
Log.d(tag, "is surfaceview = " + isSurfaceMode);
mCameraUtil = CameraUtil.getInstance();
mCamera = mCameraUtil.openCamera(0);
Bundle paramBundle = new Bundle(); //感觉干脆用bundle传参好了
getWidthAndHeight();
paramBundle.putInt("width", width);
paramBundle.putInt("height", height);
mCameraUtil.setCameraParamter(mContext, mCamera, paramBundle);

//根据不同的view,选择不同的预览载体
if (isSurfaceMode) {
try {

mCamera.setPreviewDisplay(surfaceView.getHolder());
} catch (IOException e) {
e.printStackTrace();
}
} else {
try {
mCamera.setPreviewTexture(textureView.getSurfaceTexture());
} catch (IOException e) {
e.printStackTrace();
}
}
//预览监听
mCamera.setPreviewCallback(mCameraCallback);
// mCamera.setPreviewCallbackWithBuffer(mCameraCallback);
//开始预览
mCamera.startPreview();

}


/************************** 录视频部分 *******************************/
MediaRecorder mediaRecorder;

public void startVideoRecord() {

mediaRecorder = new MediaRecorder();// 创建mediarecorder对象
// 设置录制视频源为设置好参数的Camera(相机)
mediaRecorder.setOnInfoListener(mediaCallbackListener);
mediaRecorder.setOnErrorListener(mediaCallbackListener);
mCamera.unlock();

mediaRecorder.setCamera(mCamera);

if (isSurfaceMode)//如果给camera设置了setPreviewDisplay,则这句可以不加
mediaRecorder.setPreviewDisplay(mHolder.getSurface());


Log.d(tag, "camera + " + mCamera);


mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
// 设置录制完成后视频的封装格式THREE_GPP为3gp.MPEG_4为mp4
mediaRecorder
.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
// 设置录制的视频编码h264
//音频编码为AAC
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mediaRecorder.setOrientationHint(90);
// 设置视频录制的分辨率。必须放在设置编码和格式的后面,否则报错
mediaRecorder.setVideoSize(1600, 1200);
mediaRecorder.setVideoEncodingBitRate(1024 * 1024 * 5);// 设置编码位率,图像模糊的设置了这个图像就清晰了

// 设置录制的视频帧率。必须放在设置编码和格式的后面,否则报错
mediaRecorder.setVideoFrameRate(24);
// mediaRecorder.setPreviewDisplay(mHolder.getSurface());
// 设置视频文件输出的路径
mediaRecorder.setOutputFile(Task3.videoFileName);
try {
// 准备录制
mediaRecorder.prepare();
// 开始录制
mediaRecorder.start();
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

public void stopVideoRecord() {
if (mediaRecorder == null) return;
mediaRecorder.stop();
mediaRecorder.release();

try {
mCamera.lock();
mCamera.reconnect();

//这或许是camera的bug,预览的时候不能同时进行拍摄,如果拍摄结束后还需要预览
//要再setPreviewCallback和startPreview,貌似在camera2中解决了
mCamera.setPreviewCallback(mCameraCallback);
mCamera.startPreview();
// mCamera.lock();
} catch (IOException e) {
e.printStackTrace();
}

}

private CameraCallback mCameraCallback = new CameraCallback();
class CameraCallback implements Camera.PreviewCallback{

@Override
public void onPreviewFrame(byte[] data, Camera camera) {
int len = data.length;
//当相机的预览分辨率为1600 * 1200,预览图像的编码格式为 NV21 即 12bit/pixel 时
//每一帧图像的大小应该为 1600 * 1200 * 12 / 8 = 2880000 Bytes
Log.d(tag, "onPreviewFrame data.data len=" + len);

}
}


MediaCallbackListener mediaCallbackListener;

/************************ SurfaceTextureListener **********************************/
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
if (!isSurfaceMode)
initCamera();
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {

}


/************************ SurfaceCallback **********************************/
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (isSurfaceMode)
initCamera();
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {

}

/**********************************************************/

class MediaCallbackListener implements MediaRecorder.OnInfoListener, MediaRecorder.OnErrorListener {

@Override
public void onError(MediaRecorder mr, int what, int extra) {
Log.d(tag, "error");
}

@Override
public void onInfo(MediaRecorder mr, int what, int extra) {
Log.d(tag, "info");
}
}


public void onClick(View v) {
// if (mCamera == null) return;
if (v.getId() == R.id.btn_take_pic) //拍照
takePicture();
else if (v.getId() == R.id.btn_start_video)
startVideoRecord();
else if (v.getId() == R.id.btn_stop_video)
stopVideoRecord();
}


/************************** 拍照部分 *******************************/

private void takePicture() {
mCamera.takePicture(null, null, mPictureCallback);
}


String fileName = Environment.getExternalStorageDirectory() + "/t3.jpg";
private Camera.PictureCallback mPictureCallback = new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {

SavePicAsyncTask saveBitmapTask = new SavePicAsyncTask();
saveBitmapTask.execute(data);
Thread t = new Thread();


if (mCamera != null) {
mCamera.startPreview();
}
}
};

private Camera.ShutterCallback mShutterCallback = new Camera.ShutterCallback() {
@Override
public void onShutter() {

}
};

//这是设置了rotation时调用保存图片的方法
public void savePic(byte[] data) {
try {
File file = new File(fileName);
FileOutputStream os = new FileOutputStream(file);
os.write(data);
os.flush();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

if (mCamera != null) {
mCamera.startPreview();
}
}

//使用bitmap保存时,因为compress是耗时方法,放在onPictureTaken中会体验极差,所以放个后台任务来压缩保存图片
private class SavePicAsyncTask extends AsyncTask {
@Override
protected Object doInBackground(Object[] objects) {
byte[] data = (byte[]) objects[0];
Bitmap bm0 = BitmapFactory.decodeByteArray(data, 0, data.length);

//由于camera.
Matrix m = new Matrix();
m.setRotate(90, (float) bm0.getWidth() / 2, (float) bm0.getHeight() / 2); //后面两个是旋转轴心坐标
Bitmap bitmap = Bitmap.createBitmap(bm0, 0, 0, bm0.getWidth(), bm0.getHeight(), m, false);

File file = new File(fileName);
try {
FileOutputStream os = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 85, os);
os.flush();
os.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
};
}

CameraUtil

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.ImageFormat;
import android.hardware.Camera;
import android.media.Image;
import android.os.Bundle;
import android.util.Log;

import java.security.PublicKey;
import java.util.List;

public class CameraUtil {
private static final String tag = CameraUtil.class.getName();
private static CameraUtil util;

public static CameraUtil getInstance() {
if (util == null) {
util = new CameraUtil();
}
return util;
}

int CameraNum = 0;
boolean mIsPortrait = true;

private CameraUtil() {
CameraNum = Camera.getNumberOfCameras();
Log.d(tag, "cam_num=" + CameraNum);
}


public Camera openCamera(int id) {
if (CameraNum == 0) return null;
Camera camera = Camera.open(id); //打开第一个摄像头
return camera;
}

public void setCameraParamter(Context context, Camera camera, Bundle paramBundle) {
if (camera == null) return;

Camera.Parameters parameters = camera.getParameters();
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO); //闪光灯模式
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);

int width = paramBundle.getInt("width");
int height = paramBundle.getInt("height");
Log.d(tag, "width = " + width + " height =" + height);
List<Camera.Size> sizeList = parameters.getSupportedPreviewSizes();
Camera.Size lastSize = getOptimalPreviewSize(context, sizeList, width, height);
parameters.setPreviewSize(lastSize.width, lastSize.height);
Log.d(tag, "surwidth = " + width + " surheight =" + height
+ " size width=" + lastSize.width + " size height=" + lastSize.height);
parameters.setPreviewFormat(ImageFormat.NV21); //默认预览帧格式
//拍照分辨率和预览分辨率
// parameters.setPictureSize();
// parameters.setPreviewSize(); //预览分辨率只能从上面的sizeList里面选取

followScreenOrientation(context, camera);
camera.setParameters(parameters);
//有的手机这么设置会出错,则使用camera.getParameters().setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);的形式设置


}

public void followScreenOrientation(Context context, Camera camera) {
final int orientation = context.getResources().getConfiguration().orientation;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
camera.setDisplayOrientation(180);
mIsPortrait = false;
} else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
camera.setDisplayOrientation(90);
mIsPortrait = true;
}
}


//虽然这样画面还是有一点点扁,还是不知道系统相机是如何做到画面不扁而且还像素高的
private Camera.Size getOptimalPreviewSize(Context context, List<Camera.Size> sizes, int w, int h) {
final double ASPECT_TOLERANCE = 0.2; //camera宽高比与surface宽高比的最大误差阈值,越小越精确,但可能没有合适的分辨率,一般为0.1或0.2
final int orientation = context.getResources().getConfiguration().orientation;
int targetHeight = 0;
double targetRatio = 0d;
//由于横竖屏不一样,系统相机的尺寸默认是横屏状态下的,
// 所以竖屏状态下,比例就是高宽比(因此此时高大于宽),而与系统中高度比较时则应该用宽(哪个小用哪个)
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
targetRatio = (double) w / h;
targetHeight = h;
} else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
targetRatio = (double) h / w;
targetHeight = w;
}
//获得surfaceView的宽高比
if (sizes == null) return null;

Camera.Size optimalSize = null;
double minDiff = Double.MAX_VALUE; //在比例合适的情况下,两种高度最低能达到的差值(肯定是越小越符合),一开始设为最大浮点数


Log.d(tag, "targetHeight=" + targetHeight);
// Try to find an size match aspect ratio and size
for (Camera.Size size : sizes) { //遍历当前摄像头支持的分辨率,并计算宽高比
double ratio = (double) size.width / size.height;
Log.d(tag, "width=" + size.width + " height=" + size.height);
Log.d(tag, "target Ratio=" + targetRatio + " ratio=" + ratio);
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) continue; //如果两个宽高比之差大于阈值,不符合
if (Math.abs(size.height - targetHeight) < minDiff) { //找到了更小的差值,则替换最合适的比例
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}

// Cannot find the one match the aspect ratio, ignore the requirement
if (optimalSize == null) { //当使用阈值找不到时,则忽略比例阈值,直接用最小的高度差的那组分辨率
minDiff = Double.MAX_VALUE;
for (Camera.Size size : sizes) {
if (Math.abs(size.height - targetHeight) < minDiff) {
optimalSize = size;
minDiff = Math.abs(size.height - targetHeight);
}
}
}

return optimalSize;
}
}

布局文件,只让surfaceview和textureview其中之一显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?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:orientation="vertical">

<!--surfaceView 和 TextureView选一个隐藏 -->
<SurfaceView
android:id="@+id/sv_t3"
android:visibility="visible"
android:layout_width="match_parent"
android:layout_height="500dp" />


<TextureView
android:id="@+id/texture_view_1"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="500dp" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<Button
android:id="@+id/btn_take_pic"
android:text="btn_take_pic"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_start_video"
android:text="btn_start_video"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_stop_video"
android:text="btn_stop_video"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>


</LinearLayout>

先做任务7,是因为做完任务4感觉想要稍微深入了解一下MediaCodec,感觉这样更能把刚学到的知识连贯一下。

任务7. 学习 MediaCodec API,完成音频 AAC 硬编、硬解

MediaCodec的作用是转换编码或解码文件,支持已编码的特定格式转成原始raw数据,
原始raw数据转成特定编码的格式
解码时,mediaCodec设定的格式是输入文件的格式,输出原始raw数据
编码时,mediaCodec设定的格式是输出文件的格式,输入为原始raw数据

解码时文件的格式和解码器设置的格式一定要一样,否则报错

音频编解码:

解码:

  • 解码是吧audio/XXX给变成audio/raw,即pcm文件,最原始的未经压缩编码处理的文件。所以解码后的文件一般会比被压缩文件大得多
    流程:
  • 首先要使用MediaExtractor把音轨从文件中提取出来,把format也提取出来,根据要解码的mime创建codec:
    mDCodec = MediaCodec.createDecoderByType(srcMIMTType);
  • 把format设置到codec:mDCodec.configure(mFormat, null, null, 0); //视频文件如果要播放解码出的数据,则第二个参数选择要用于播放的surface
  • 数据回调监听:mDCodec.setCallback(mDecodeCallback); (异步处理,感觉异步的好一点)
  • 开始解码:mDCodec.start();
    callback中:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    //输入
    //onInputBufferAvailable:当有可用的缓冲区时,会转到这里,在该函数中往可用的缓冲区写入要解码的数据,然后把缓冲区加入待处理队列
    public void onInputBufferAvailable(@NonNull MediaCodec codec, int index) {
    //获得可用的缓冲
    ByteBuffer inputBuffer = codec.getInputBuffer(index);

    MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();

    int size = mExtractor.readSampleData(inputBuffer, 0);
    int flag = mExtractor.getSampleFlags();
    long presentation = mExtractor.getSampleTime();
    int offset = 0;
    Log.d(tag, "size= " + size + " flag=" + flag + " presentation=" + presentation);
    //要对文件是否读到末尾单独处理,若读到末尾了,size,presentation都要置为0,flag要标注为 MediaCodec.BUFFER_FLAG_END_OF_STREAM
    if (size != -1) {
    inputSize += size;
    codec.queueInputBuffer(index, offset, size, presentation, flag);
    mExtractor.advance();
    } else { //当文件达到末尾时,用flag
    size = 0;
    presentation = 0;
    flag = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
    codec.queueInputBuffer(index, offset, size, presentation, flag);
    }
    }

    //输出
    //onOutputBufferAvailable:当有待处理数据完成解码时,会放入输出缓冲区,并转到这里,在该函数中获得解码后的raw数据,并且每次读出数据后要release当前缓冲,让它能够继续被使用,否则就那么一点缓冲区,不释放的话几下就没了
    @Override
    public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
    if (fos == null) try {
    throw new Exception("fos == null!");
    } catch (Exception e) {
    e.printStackTrace();
    }

    MediaFormat format = codec.getOutputFormat(index);
    Log.d(tag, "out format=" + format.getString(MediaFormat.KEY_MIME));

    //获得可用的输出缓冲区(也就是解码好的数据)
    ByteBuffer outputBuffer = codec.getOutputBuffer(index);

    //如果直接定义 data = new byte[100 * 1024]; 然后调用outputBuffer.get(data),
    // 则会使用data的长度去操作buffer,但buffer没这么大,导致BufferUnderflowException
    //所以下面两种方式选一种

    byte[] data = new byte[outputBuffer.limit()];
    outputBuffer.get(data);

    // byte[] data = new byte[100 * 1024];
    // outputBuffer.get(data,0, outputBuffer.limit());
    //
    outputSize += data.length;
    try {
    fos.write(data); //写入流中
    fos.flush();
    } catch (IOException e) {
    e.printStackTrace();
    }
    Log.d(tag, "flag=" + info.flags);
    Log.d(tag, "outputSize= " + outputSize);
    //用完后释放这个buffer,使其可以接着被使用
    codec.releaseOutputBuffer(index, false);
    //当到达末尾时,释放资源
    if (info.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
    Log.d(tag, "BUFFER_FLAG_END_OF_STREAM");
    stopAndrealseCodec();
    exPCM2WAV();
    }
    }
    //释放资源
    private void stopAndrealseCodec() {
    if (mDCodec != null) {
    mDCodec.release();
    mDCodec = null;
    mExtractor.release();
    mExtractor = null;
    Log.d(tag, "outputBuffer BUFFER_FLAG_END_OF_STREAM");
    try {
    fos.flush();
    fos.close();
    } catch (IOException e) {
    e.printStackTrace();
    }

    Log.d(tag, "stopAndrealseCodec");
    Log.d(tag, "inputSize=" + inputSize);
    Log.d(tag, "outputSize=" + outputSize);
    }
    }

    //把pcm文件加入wav头变成wav文件,wav文件会被识别成audio/raw,而原始的pcm文件会被识别错误
    private void exPCM2WAV(){
    //为什么从视频里(只试了mp4和3gp)剥离出来的要 原本的sampleRate/2才是正常速度?
    int sampleRate = mFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
    PcmToWavUtil util = new PcmToWavUtil(sampleRate, mFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT),
    AudioFormat.ENCODING_PCM_16BIT);
    Log.d(tag, "sampleRate=" + sampleRate);
    util.pcmToWav(outputFileName, outputFileName + ".wav");
    }

编码:

编码是吧audio/raw给变成audio/XXX,是对原始数据压缩编码,但这个有一个问题要注意的是硬件是否支持这种编码方式,比如nexus6p,由于mp3格式编码是不开源的,所以不能编码成mp3文件。下面以把wav的原生格式编码成aac
流程:
编码要比解码多一个步骤,就是设置要编码的格式的各项数据,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
outFormat = MediaFormat.createAudioFormat(desMIMEType, SAMPLE_RATE, CHANNEL_COUNT); //根据编码格式,采样率,声道数创建format
outFormat.setInteger(MediaFormat.KEY_BIT_RATE, 96000); //比特率
outFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); //aac特有的profile
outFormat.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 100 * 1024);//输入缓存的最大值,设置这个防止一帧的数据量太大而超过默认的buffer大小

//添加aac的csd-0,如果是视频,则csd-0和csd-1都要有(这个是干什么的还不清楚)
ByteBuffer csd = ByteBuffer.allocate(2);
csd.put((byte) ((aacObjLC << 3) | (sampleIndex >> 1)));
csd.position(1);
csd.put((byte) ((byte) ((sampleIndex << 7) & 0x80) | (channelCount << 3)));
csd.flip();
outFormat.setByteBuffer("csd-0", csd); // add csd-0
System.out.println(Arrays.toString(csd.array()) + "===++”);
查看设备支持的编码和解码器:
MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS);
MediaCodecInfo[] infos = list.getCodecInfos();
for (int i = 0; i < infos.length; i++) {
String name = infos[i].getName();
Log.d(tag, "i=" + i + " name=" + name);
}
根据format创建相应的编码器
//根据相应的格式找到是否有合适的编码器,如果有则返回其名称,然后创建相应的编码器
String encodeName = list.findEncoderForFormat(outFormat);
Log.d(tag, "encodeName=" + encodeName);
mECodec = MediaCodec.createByCodecName(encodeName);
} catch (IOException e) {
e.printStackTrace();
}
mECodec.setCallback(mEncodeCallback); //设置回调
mECodec.configure(outFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); //编码配置,编码时要设置最后一个参数
mECodec.start(); //开始编码

callback中:

1
onInputBufferAvailable和解码器的并无不同,都是由MediaExtractor读出数据放到缓存队列里等待处理

onOutputBufferAvailable和默认流程和解码器类似,都是从缓冲队列中取出已经处理好的数据(每次取一帧),但编码器不同的一点是,这个数据只是真实数据流的编码,仅仅有这些没办法封装成可读的格式,还必须要加上可识别的部分,比如wav文件是在所有数据头部加上wav头,aac文件是在每一帧数据前面加上adts头,
adts见 https://www.cnblogs.com/lihaiping/p/5284547.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
       public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index, @NonNull MediaCodec.BufferInfo info) {
if (fos == null) try {
throw new Exception("fos == null!");
} catch (Exception e) {
e.printStackTrace();
}
int outIndex = index;
MediaFormat format = codec.getOutputFormat(outIndex);

ByteBuffer outputBuffer = codec.getOutputBuffer(outIndex);

int size = outputBuffer.limit();
outputSize += size;
byte[] packedData = new byte[size + 7];
addADTStoPacket(packedData, packedData.length);
outputBuffer.get(packedData, 7, size);
try {
fos.write(packedData);
fos.flush();
} catch (IOException e) {
e.printStackTrace();
}
//用完后释放这个buffer,使其可以接着被使用,如果一直不释放,如果文件太大则会导致缓冲区不够用
outputBuffer.clear();
codec.releaseOutputBuffer(outIndex, false);
if (info.flags == MediaCodec.BUFFER_FLAG_END_OF_STREAM) {
Log.d(tag, "BUFFER_FLAG_END_OF_STREAM");
stopAndrealseCodec();
}
}




//每一帧前面都要加上ADTS头,可以看做是每一个AAC帧的帧头
private void addADTStoPacket(byte[] packet, int packetLen) {
int profile = 2; //AAC LC
//39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;
int freqIdx = 4; //44.1KHz
int chanCfg = 2; //CPE

// byte[] packet1 = new byte[packetLen];
// fill in ADTS data
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;


// //把这些清晰的放出来,但最好还是用上面的方式
// //syncword
// packet[0] = (byte) 0xFF;
// packet[1] |= 0xF << 4;
// //id
// packet[1] |= 0x1 << 3;
// //layer
// packet[1] |= 0x00 << 1;
// //protection_abscent
// packet[1] |= 0x1;
// //profile
// packet[2] |= (profile -1) << 6;
// //sampling_frequency_index
// packet[2] |= freqIdx << 2;
// //private bit
// packet[2] |= 0x0 << 1;
// //channel_config
// packet[2] |= chanCfg >> 2; //高1位
// packet[3] |= chanCfg << 6; //低2位
// //copy and home;
// packet[3] |= 0x00 << 4;
// //cib and cis
// packet[3] |= 0x00 << 2;
// //frame_length,单位是字节。不用管int是几个字节,把它当13位就行了,一帧的长度一般不可能超过13位能表达的最大值(8KB),
// packet[3] |= packetLen >> 11;
// packet[4] = (byte) (packetLen >> 3);
// int x = packetLen << 5;
// packet[5] |= packetLen << 5;
//
// packet[5] |= 0x7FF >> 6;
// packet[6] |= 0x7FF << 2;
//
}

//释放资源
private void stopAndrealseCodec() {
if (mECodec != null) {
mECodec.release();
mECodec = null;
mExtractor.release();
mExtractor = null;
Log.d(tag, "outputBuffer BUFFER_FLAG_END_OF_STREAM");
try {
fos.flush();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}

Log.d(tag, "stopAndrealseCodec");
Log.d(tag, "inputSize=" + inputSize);
Log.d(tag, "outputSize=" + outputSize);
}
}

代码见 https://github.com/lujianyun06/VATask/tree/master/app/src/main/java/com/example/lll/va/Task7

任务4. 学习 Android 平台的 MediaExtractor 和 MediaMuxer API,知道如何解析和封装 mp4 文件

  • MediaMuxer和MediaCodec算是比较年轻的,它们是JB 4.1和JB 4.3才引入的。前者用于将音频和视频进行混合生成多媒体文件。缺点是目前只能支持一个audio track和一个video track,目前支持mp4,3gp,webm输出
  • MediaCodec用于将音视频进行压缩编码,它有个比较牛X的地方是可以对Surface内容进行编码,如KK 4.4中屏幕录像功能就是用它实现的。
  • MediaFormat用于描述多媒体数据的格式。
  • MediaRecorder用于录像+压缩编码,生成编码好的文件如mp4, 3gpp,视频主要是用于录制Camera preview。
  • MediaPlayer用于播放压缩编码后的音视频文件。
  • AudioRecord用于录制PCM数据。
  • AudioTrack用于播放PCM数据。PCM即原始音频采样数据,可以用如vlc播放器播放。

MediaExtractor可以从数据源中提取解复用的,编码后的媒体数据。MediaExtractor用于音视频分路,比如从一个视频中分别提取音频(音轨)和视频(视频轨)
它既可以从视频里提取视频轨或音频轨,也可以单从音频里提取音频轨

//如下使用`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(…);
int
int numTracks = extractor.getTrackCount(); //当前视频中有几个轨道
for (int i = 0; i < numTracks; ++i) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
boolean isVideoTrack = mime.startsWith("video/“); //当前是不是视频轨,视频轨的MIME以”video/“开头,音频轨的MIME以“audio/”开头
if (isVideoTrack) {
extractor.selectTrack(i); //选择视轨,当确定感兴趣的轨道时,一定要选取!
int index = mediaMuxer.addTrack(format) //如果要进行这个视轨的合成合成器除了需要数据,也需要mediaMuxer需要设置当前视轨的format,在写入的时候也需要视轨的index
}
}
ByteBuffer inputBuffer = ByteBuffer.allocate(…) //这个容量必须大一点,否则下面readSampleData会崩,实测是1000*1024可以
//下面readSampleData会自动更新buffer的limit和postion,就和read,write一样
while (extractor.readSampleData(inputBuffer, ...) >= 0) {
int trackIndex = extractor.getSampleTrackIndex();
long presentationTimeUs = extractor.getSampleTime();
...
extractor.advance(); //前进到下一个样本(下一个视频帧或音频帧);readSampleData看起来不会自动更新读过的数据所以需要这个
}

extractor.release();
extractor = null;

MediaMuxer可以复用基本流。 目前MediaMuxer支持MP4,Webm和3GP文件作为输出。 它还支持自Android Nougat以来在MP4中复用B帧。是extractor的反作用类型,用于把视频轨和音频轨进行合成,和MediaExtractor正好是反过程。
不支持mp3,wav音频源(AAC支持)
只能支持一个audio track和一个video track,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//如下使用
MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);
mediaMuxer.setOrientationHint(90); //设置混合后视频的旋转角度
// More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()
// or MediaExtractor.getTrackFormat().
MediaFormat audioFormat = new MediaFormat(...);
MediaFormat videoFormat = new MediaFormat(...);
int audioTrackIndex = muxer.addTrack(audioFormat); //返回的是混合器里的轨道号,也就是新文件里的轨道号
int videoTrackIndex = muxer.addTrack(videoFormat);
ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);
boolean finished = false;
BufferInfo bufferInfo = new BufferInfo();

muxer.start(); //开始混合
while(!finished) {
// getInputBuffer() will fill the inputBuffer with one frame of encoded
/* sample from either MediaCodec or MediaExtractor, set isAudioSample to true when the sample is audio data, set up all the fields of bufferInfo,and return true if there are no more samples.*/
finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);
if (!finished) {
int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;
/*
bufferInfo.presentationTimeUs = extractor.getSampleTime(); //直接从extractor获取
bufferInfo.offset = 0; //如果没有特殊需求一般是0
bufferInfo.flags = extractor.getSampleFlags();
bufferInfo.size = size; //本次写入的数据量
*/
muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo); //每次写入数据都要同时写入index和info,info要明确如上面几点,注意这里的index是新合成的视频的相应轨道,应该是由addTrack返回的值
}
};
muxer.stop();
muxer.release();

元数据跟踪
每帧元数据用于携带与视频或音频相关的额外信息以便于离线处理,例如,来自传感器的陀螺仪信号可以在进行离线处理时帮助稳定视频。仅在MP4容器中支持元数据跟踪。添加新元数据轨道时,轨道的mime格式必须以前缀“application /”开头,例如“application/gyro”。元数据的格式/布局将由应用程序定义。写入元数据与编写视频/音频数据几乎相同,只是数据不会来自mediacodec。应用程序只需要将包含元数据的字节缓冲区以及相关的时间戳传递给writeSampleData(int,ByteBuffer,MediaCodec.BufferInfo)api。时间戳必须与视频和音频的时间基准相同。生成的MP4文件使用ISOBMFF的第12.3.3.2节中定义的TextMetaDataSampleEntry来表示元数据的mime格式。当使用MediaExtractor提取具有元数据轨道的文件时,元数据的mime格式将被提取到MediaFormat中。
//如下例,把陀螺仪信息也传给生成的MP4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);

// SetUp Video/Audio Tracks.

MediaFormat audioFormat = new MediaFormat(...);

MediaFormat videoFormat = new MediaFormat(...);

int audioTrackIndex = muxer.addTrack(audioFormat);

int videoTrackIndex = muxer.addTrack(videoFormat);



// Setup Metadata Track

MediaFormat metadataFormat = new MediaFormat(...);

metadataFormat.setString(KEY_MIME, "application/gyro");

int metadataTrackIndex = muxer.addTrack(metadataFormat);



muxer.start();

while(..) {

// Allocate bytebuffer and write gyro data(x,y,z) into it.

ByteBuffer metaData = ByteBuffer.allocate(bufferSize);

metaData.putFloat(x);

metaData.putFloat(y);

metaData.putFloat(z);

BufferInfo metaInfo = new BufferInfo();

// Associate this metadata with the video frame by setting

// the same timestamp as the video frame.

metaInfo.presentationTimeUs = currentVideoTrackTimeUs;

metaInfo.offset = 0;

metaInfo.flags = 0;

metaInfo.size = bufferSize;

muxer.writeSampleData(metadataTrackIndex, metaData, metaInfo);

};

muxer.stop();

muxer.release();

}

MediaCodec类可用于访问低级媒体编解码器,即编码器/解码器组件。 它是Android低级多媒体支持基础架构的一部分(通常与MediaExtractor,MediaSync,MediaMuxer,MediaCrypto,MediaDrm,Image,Surface和AudioTrack一起使用。)

从广义上讲,编解码器处理输入数据以生成输出数据。它异步处理数据并使用一组输入和输出缓冲区。在简单的级别,您请求(或接收)一个空的输入缓冲区,用数据填充它并将其发送到编解码器进行处理。编解码器使用数据并将其转换到它的空输出缓冲区之一。最后,您请求(或接收)到一个填充了数据的输出缓冲区,使用其内容并将其释放回编解码器。

数据类型
编解码器对三种数据进行操作:压缩数据,原始音频数据和原始视频数据。可以使用ByteBuffers处理所有三种数据,但是您应该使用Surface for raw视频数据来提高编解码器性能。 Surface使用本机视频缓冲区而不映射或将它们复制到ByteBuffers;因此,效率更高。使用Surface时通常无法访问原始视频数据,但您可以使用ImageReader类访问不安全的解码(原始)视频帧。这可能仍然比使用ByteBuffers更有效,因为一些本机缓冲区可能会映射到直接ByteBuffers。使用ByteBuffer模式时,可以使用Image类和getInput / OutputImage(int)访问原始视频帧。

原始音频缓冲区
原始音频缓冲区包含整个PCM音频数据帧,这是通道顺序中每个通道的一个样本。 每个样本都是本机字节顺序的16位有符号整数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
short[] getSamplesForChannel(MediaCodec codec, int bufferId, int channelIx) {

ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId);

MediaFormat format = codec.getOutputFormat(bufferId);

ShortBuffer samples = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer();

int numChannels = formet.getInteger(MediaFormat.KEY_CHANNEL_COUNT);

if (channelIx < 0 || channelIx >= numChannels) {

return null;

}

short[] res = new short[samples.remaining() / numChannels];

for (int i = 0; i < res.length; ++i) {

res[i] = samples.get(i * numChannels + channelIx);

}

return res;

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//音频抽取后的format信息
0 = {HashMap$HashMapEntry@4594} "mime" -> "audio/mp4a-latm"
1 = {HashMap$HashMapEntry@4595} "aac-profile" -> "2"
2 = {HashMap$HashMapEntry@4596} "channel-count" -> "2"
3 = {HashMap$HashMapEntry@4597} "track-id" -> "1"
4 = {HashMap$HashMapEntry@4598} "durationUs" -> "192911760"
5 = {HashMap$HashMapEntry@4599} "csd-0" -> "java.nio.HeapByteBuffer[pos=0 lim=2 cap=2]"
6 = {HashMap$HashMapEntry@4600} "sample-rate" -> "44100"

//视频抽取后的format信息
0 = {HashMap$HashMapEntry@4620} "csd-1" -> "java.nio.HeapByteBuffer[pos=0 lim=8 cap=8]"
1 = {HashMap$HashMapEntry@4621} "rotation-degrees" -> "90"
2 = {HashMap$HashMapEntry@4622} "track-id" -> "1"
3 = {HashMap$HashMapEntry@4623} "height" -> "1200"
4 = {HashMap$HashMapEntry@4624} "profile" -> "1"
5 = {HashMap$HashMapEntry@4625} "color-standard" -> "1"
6 = {HashMap$HashMapEntry@4626} "durationUs" -> "2157877"
7 = {HashMap$HashMapEntry@4627} "color-transfer" -> "3"
8 = {HashMap$HashMapEntry@4628} "mime" -> "video/avc"
9 = {HashMap$HashMapEntry@4629} "frame-rate" -> "30"
10 = {HashMap$HashMapEntry@4630} "width" -> "1600"
11 = {HashMap$HashMapEntry@4631} "color-range" -> "2"
12 = {HashMap$HashMapEntry@4632} "max-input-size" -> "123106"
13 = {HashMap$HashMapEntry@4633} "csd-0" -> "java.nio.HeapByteBuffer[pos=0 lim=21 cap=21]"
14 = {HashMap$HashMapEntry@4634} "level" -> "2048"

这里要注意的是,要分清楚原视频中的视/音轨号和新合成的视频中的视/音轨号,一般来说前者是为了让extractor选中相应的轨道,而后者是在合成视频写数据的时候需要。这里犯了一个错就是提取视频轨的时候,视频轨在视频中的轨道号是0,提取音频帧时,音轨在音频中的轨道号也是0,实际给muxer添加轨道的时候,视轨被添加到了新视频的轨道0,音轨被添加到了新视频的轨道1。但写音频数据的时候仍往0号轨道写,就崩掉了报错:stop muxer failed

具体工程代码见 https://github.com/lujianyun06/VATask/tree/master/app/src/main/java/com/example/lll/va/task4

转载自 https://www.cnblogs.com/xqxacm/p/6673469.html
https://blog.csdn.net/superxukai88/article/details/78675686

并写了一些自己的总结
前言:
  自定义控件的三大方法:

  1. 测量: onMeasure(): 测量自己的大小,为正式布局提供建议
  2. 布局: onLayout(): 使用layout()函数对所有子控件布局
  3. 绘制: onDraw(): 根据布局的位置绘图

onDraw() 里面是绘制的操作,可以看下其他的文章,下面来了解 onMeasure()和onLayout()方法。

一、onMeasure()、测量

1
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

参数即父类传过来的两个宽高的”建议值”,即把当前view的高设置为:heightMeasureSpec ;宽设置为:widthMeasureSpec
这个参数不是简单的整数类型,而是2位整数(模式类型)和30位整数(实际数值) 的组合

其中模式分为三种:

①、UNSPECIFIED(未指定),父元素不对自元素施加任何束缚,子元素可以得到任意想要的大小;UNSPECIFIED=00000000000000000000000000000000

②、EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;EXACTLY =01000000000000000000000000000000
③、AT_MOST(至多),子元素至多达到指定大小的值。 他们对应的二进制值分别是: AT_MOST =10000000000000000000000000000000

最前面两位代表模式,分别对应十进制的0,1,2;

获取模式int值 和 获取数值int值的方法:

  1. int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
  2. int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
  3. int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
  4. int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
    模式的值有:
    MeasureSpec.AT_MOST = 2
    MeasureSpec.EXACTLY = 1
    MeasureSpec.UNSPECIFIED = 0

上面我们知道了 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 方法参数的意义
下面了解参数对应的三个模式分别对应的意义:
每一个模式都对应的xml布局中的一个值
wrap_content — MeasureSpec.AT_MOST
match_parent — MeasureSpec.EXACTLY
具体值 — MeasureSpec.UNSPECIFIED

注意:
—当模式是MeasureSpec.AT_MOST时,即wrap_content时,此时传入的int widthMeasureSpec, int heightMeasureSpec的数值部分(后30位)实际上就是父亲建议的尺寸数值,(如果父亲只有一个子view,数值就是父亲自己的尺寸,多个子view的LinearLayout传递的值是安置完之前的子view剩下的空间尺寸),因此在onMeasure里不做其他操作,view仍然会符合父亲的建议.

PS: 但需要注意的是,如果直接自定义一个View,宽高为wrap_content, 则最后绘制的时候view是占满整个父View的(假设父view只有这一个儿子),这是因为上面说的,传过来的是父亲的尺寸,但如果自定义View继承了像TextView之类的,最后还调用了super.onMeasure,则显示的时候大小是根据其内容显示的,这是因为这些TextView在自己的onMeasure里又做了处理,使得其根据内容的大小而变化。

—当模式是MeasureSpec.EXACTLY时,即match_parent时,此时传入的int widthMeasureSpec, int heightMeasureSpec的数值部分(后30位)实际上也是父亲建议的尺寸数值(如果父亲只有一个子view,数值就是父亲自己的尺寸,多个子view的LinearLayout传递的值是安置完之前的子view剩下的空间尺寸),因此在onMeasure里不做其他操作,view仍然会符合父亲的建议

PS:(AT_MOST与EXACTLY:如果在onMeasure中不做任何处理而调用setMeasureDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec)))来设置尺寸,,或者在基类的onMeasure中不做任何额外处理而调用super.onMeasure().那么这两个模式没有任何区别(都是用父亲建议的大小设置自己的大小,而且两个模式父亲建议的大小都一样)。因此若使用AT_MOST一般都要在onMeasure中根据mode不同而设置view尺寸为不同的大小(如同TextView做的那样)

—当模式是MeasureSpec.UNSPECIFIED时,即具体数值时,此时传入的int widthMeasureSpec, int heightMeasureSpec的数值部分(后30位)实际是具体指定的数字,因此在onMeasure里不做其他操作,子view会符合父亲的建议,即变成它自己设定的尺寸,尽管这里的数字大小是不做限制的,但当绘制的时候,父亲会clip掉自己尺寸之外的子view的部分。要避免的话,需设置子view的顶级祖先ViewGroup的clipChildren属性为false,而且它的父亲必须不是RelativeLayout(即不clip有两个条件,1.顶级祖先view设置clipChildren=false。2.父view必须不是RelativeLayout,PS:父view设置不设置clipChildren无所谓,顶级祖先必须设置)

二、onLayout() 、 布局

onLayout方法是ViewGroup中子View的布局方法,用于放置子View的位置。放置子View很简单,只需在重写onLayout方法,然后获取子View的实例,调用子View的layout方法实现布局。在实际开发中,一般要配合onMeasure测量方法一起使用。

onLayout方法:

1
2
3
@Override
protected
abstract void onLayout(boolean changed, int l, int t, int r, int b);

该方法在ViewGroup中定义是抽象函数,继承该类必须实现onLayout方法,而ViewGroup的onMeasure并非必须重写的。View的放置都是根据一个矩形空间放置的,onLayout传下来的l,t,r,b分别是放置父控件的矩形可用空间(除去margin和padding的空间)的左上角的left、top以及右下角right、bottom值。

layout方法:

1
2
public
void layout(int l, int t, int r, int b);

该方法是View的放置方法,在View类实现。调用该方法需要传入放置View的矩形空间左上角left、top值和右下角right、bottom值。这四个值是相对于父控件而言的。例如传入的是(10, 10, 100, 100),则该View在距离父控件的左上角位置(10, 10)处显示,显示的大小是宽高是90(参数r,b是相对左上角的),这有点像绝对布局。

平常开发所用到RelativeLayout、LinearLayout、FrameLayout…这些都是继承ViewGroup的布局。这些布局的实现都是通过都实现ViewGroup的onLayout方法,只是实现方法不一样而已。

:(两种错误刚好就是三种进入老年代的方法引起的)

一.并发模式失败(concurrent mode failure):产生的原因是老年代的可用空间不够了(因为正常晋升入老年代的对象太多太快,或者由于新生代不够而从创建就直接进入老年代的对象太多)

原因有两种:
1.年轻代提升太快,老年代的处理速度跟不上新生代的提升速度;或者新生代空间太小,放不下新产生的对象而直接转入老年代,但老年代也空间不够
解决办法:
①.调大新生代空间 -Xmn
②.加大新生代晋升的阈值 -XX:MaxTenuringThreshold
2.老年代碎片过多
解决办法:
①.调大老年代的比例 –XX:NewRatio
②.降低老年代进行垃圾回收的阈值,
-XX:CMSInitiatingOccupancyFraction=60(默认是 68)
-XX:+UseCMSInitiatingOccupancyOnly
当老年代碎片过多时,这个过程注意cms的性能会比较差,退化成只有一个线程来收集垃圾,耗时可能有几秒或十几秒。

二. 提升失败(promotion failed):新生代太小,放不下要复制的存活对象,转而要往老年代放,但这样老年代就有大量短命对象,而很快内存不够就报错(因为MinorGC时的survivor放不下eden和另一个survivor中没回收的对象,转而进入老年代)

一个Survivor 区不能容纳eden和另外一个survivor里面的存活对象,多余的对象进入老年代,这样就会导致老年代里面的存放大量的短暂存活的对象,
而我们知道,如果老年代里面没有可用空间就会发生full gc,这样就造成扫描整个堆,造成提升失败(promotion failed)。

解决办法:增加survivor
    ①.增加年轻代的大小 -Xmn
    ②.调整survivor和eden的比例  -XX:SurvivorRatio 默认是8 , 各占比 s0:s1 :eden =1:1:8 , 减小这个值也就加大了survivor。


IoC:


context是总管家。总领全局
context创建一个bean工厂,一个解析工具(如xmlReader)。先让解析工具去完成对xml或者注解中bean的解析,对于每个bean节点,提取出其中的名称,类型、属性来构建出一个beandefinition(bd),并存放到解析工具的map中,之后创建这个map中的bd全部移到bean工厂的map中,然后扫描出所有类型为bean初始化前置/后置处理器的bean,此时就要把它们创建好,并把它们都加到bean工厂的前置/后置处理列表中。
此时bean工厂中有了所有的bean的bd(但bean本身还没创建,除了前置/后置处理器bean)
(tiny中是全部bean都在这里创建,而在spring中,bean创建可以等到用户第一次getbean时,再触发下面的动作)
之后进行bean的创建,进行context的onrefresh,在bean工厂中遍历map,创建bean的实例,并且对每个bean实例做如下工作:
创建后把bean实例和它的bd绑定起来(把它赋给bd的一个属性),并且从bd中取出之前设定的一些属性,给bean赋上。然后对从bean工厂中取出之前加入到前置处理器列表中的bean,使用这些处理器bean对当前的这个bean做前置处理;然后进行这个bean的初始化方法(指定的init-method),然后取出之前加入到后置处理器列表中的bean,使用这些处理器bean对当前的这个bean做后置处理(比如aop就是在这一步发挥作用,检测这个bean的类型是不是符合要拦截处理的类型,如果是,则创建用一个该类代理类型的bean代替当前的这个bean返回); 做完前置/后置处理后都会返回bean(这个bean可能已经被修改了),再次把这个最终经过前后置处理的bean重新与bd绑定(之前绑定的就作废了)。(真正的spring中还有给每个bean设置关于容器感知,名称感知,context感知之类的功能)
(如果对于特定类型的bean要执行特定的前置/后置处理,只需在处理器中判断bean的类型,满足再处理,否则就把传入的bean原样返回即可)
此时,bean的初始化就完成了,获取bean的话,就通过context得到bean工厂,再从bean工厂中通过要获取的bean的类型或名称得到bd,再从bd得到与之绑定的bean。

AOP

有一个后置处理器叫AspectJAwareAdvisorAutoProxyCreator(简称apc),它的作用是在bean的后置处理时如果需要创建代理,则创建代理来代替原始bean返回
有一个类叫TargetSource,它可以封装一个其他类的实例对象,类型,以及它实现和继承的类型/接口集合,相当于是某个实例对象的信息组合。
一个类叫AdvisedSupport(具体实现是proxyFactory),它是一个aop的核心类型,它有targetsource的成员变量,有方法拦截器的成员变量,有方法匹配器的成员变量。
advisors实际上是包含所有需要代理的类的名单,需要拦截的方法的名单,以及所有的方法拦截器。
advisors由多个advisor组成,每个advisor包含一个pointcut成员,一个advice成员。
pointcut定义了一组规则,包括符合条件的类型名规则,符合拦截的方法名规则。(术语是切点)
advice就是一个方法拦截器.(其实就是执行额外的逻辑代码的工具) (术语是通知)
(这里advisor的术语就是aspect,即切面,实际上就是切点和通知的综合体)
每个advisor也是一个bean,它在spring中实际上就是注解了@AspectJ的类的实例,其pointcut和advice可能都是自己。(而且为了保证advisor必须在其他实例创建之前就创建好,每次有一个bean进入创建代理的后置处理器时,会从容器获得所有的advisor(利用getBeansForType),如果有某个advisor还没创建,则优先创建它。因此会保证如果一个bean需要被代理,则其advisor一定在它用之前就创建好)

当有一个bean要进行apc的后置处理的时候,遍历所有的advisor,如果不符合所有的advisor类型规则,则直接返回bean;如果发现符合其中一个advisor的类型规则,进入下一步:(真正的spring中使用拦截链,也就是说可以有多个advisor匹配当前的bean,调用方法的时候会一个挨一个调用)
创建一个advisedSupport
·根据这个bean去创建一个它的targetSource,并且把这个targetSource设置给advisedSupport的成员
·把当前的advisor的方法规则(方法匹配器)设置给advisedSupport
·把当前advisor的方法拦截规则(方法拦截器)设置给advisedSupport

使用advisedSupport生成一个代理工厂,该代理工厂拥有advisedSupport的成员变量,并且这个代理工厂是一个调用处理器(InvocationHandler)。
使用代理工厂去生成一个当前这个bean的代理对象。如果用jdk动态代理的话,使用
Proxy.newProxyInstance(当前类的类型加载器, bean的所有实现和继承的类型/接口集合, 调用处理器(也就是生成它的代理工厂))来得到一个代理对象,这个代理对象拥有所有bean的同名方法,当调用代理对象的一个方法时,会调用它的调研处理器的invoke方法,并且会把bean的原本该名称的方法、bean对象,参数都传入invoke
,在调用处理器(即代理工厂)的invoke方法中,使用它的成员advisedSupport的方法匹配规则来判断当前调用的这个方法是不是需要拦截的,如果不是则直接调用bean的原来的方法;如果是需要拦截的,则使用advisedSupport的方法拦截器,去执行具体的拦截器的额外逻辑代码(这里还涉及到一个MethodInvocation,但它实际上就是对一个方法、对象、参数的包装,只是为了方便打包传递,从调用处理器传递到拦截器,没有额外的用途),当然这个额外的逻辑代码中也可以调用bean原来的方法,也可以去做一些额外的操作。
生成代理后返回,则此时代理对象就取代了原本的bean成为了bean容器中针对原来的bean类型的合法代言人了,别的地方不论是通过原本的类型,还是设置的名称,获得的都是这个代理,而原来的bean就只存在于代理对象的调用处理器中的advisedSupport中的targetSource中(层层包裹,外部是无法得到这个bean的)。

另:
生成代理对象的方式有jdk动态代理,还有其他一些,如Cglib2等,但原理都是一样的。

注意,jdk动态代理生成的对象只能转成实际类实现的接口类型的对象,而无法转成实在的类的对象(而cglib能)
例如,A implements I
jdk动态代理创建好的代理proxy只能强转成I类型的,而不能强转成A类型的。

注意,InvocationHandler的invoke方法,传递过来的是proxy,而不是原始对象,所以如果要运行原本实例的方法(method.invoke),需要在InvocationHandler中保存原本的实例

首先看一下收集器的分布:

垃圾收集器

1.解答parallel scavenge收集器为什么不能CMS配合使用?
首先讲一下Hotspot,HotSpot VM里多个GC有部分共享的代码。有一个分代式GC框架,Serial/Serial Old/ParNew/CMS都在这个框架内;在该框架内的young collector和old collector可以任意搭配使用,所谓的“mix-and-match”。
而ParallelScavenge与G1则不在这个框架内,而是各自采用了自己特别的框架。这是因为新的GC实现时发现原本的分代式GC框架用起来不顺手。

ParallelScavenge(PS)的young collector就如其名字所示,是并行的拷贝式收集器。本来这个young collector就是“Parallel Scavenge”所指,但因为它不兼容原本的分代式GC框架,为了凸显出它是不同的,所以它的young collector带上了PS前缀,全名变成PS Scavenge。对应的,它的old collector的名字也带上了PS前缀,叫做PS MarkSweep。

这个PS MarkSweep默认的实现实际上是一层皮,它底下真正做mark-sweep-compact工作的代码是跟分代式GC框架里的serial old(这个collector名字叫做MarkSweepCompact)是共用同一份代码的。也就是说实际上PS MarkSweep与MarkSweepCompact在HotSpot VM里是同一个collector实现,包了两张不同的皮;这个collector是串行的。

最后:
重点就是Parallel Scavenge没有使用原本HotSpot其它GC通用的那个GC框架,所以不能跟使用了那个框架的CMS搭配使用。

新生代:使用复制算法进行GC。
老年代:使用标记-整理算法。

并发:(concurrent)用户线程与垃圾收集器同时执行(但不一定并行的,可能会交替执行)
并行:(parallel)多条垃圾手机线程并行,用户线程仍然等待。

1.新生代收集器
serial收集器:是新生代的一个单线程的GC,,进行GC时,停掉所有用户线程,直至回收结束,“stop-the-world”。但是其单线程的简单高效,没有线程交互的开销,常被JVM运行在client模式下的默认新生代收集器。

ParNew:并行收集器,是serial收集器的多线程的版本。是运行在server模式下的首先的新生代的收集器。

parallel scavenge :新生代收集器,多线程,并行收集。
此收集器与之前的收集器目的不同:(特点)达到一个可控制的吞吐量。吞吐量=运行用户代码时间/CPU总执行时间。
用于精确吞吐量的两个参数:1.控制最大垃圾收集停顿时间参数 2.直接设置吞吐量大小的参数。Parallel scavenge收集器与ParNew收集器重要区别是: 垃圾自适应调节策略。

2.老年代收集器
serial old:老年代收集器版本,单线程。
用途:1.在JDK1.5版本之前与parallel scavenge 收集器搭配使用。
2.作为CMS收集器的后备预案。

Parallel Old :使用多线程收集。吞吐量优先。

CMS:

  1. 目标是:尽量缩短垃圾回收时间和用户线程的停顿时间
  2. 严格意义上第一款并发垃圾回收器
  3. 主要场景在 互联网 B/S 架构上
  4. 使用标记清除算法
  5. 步骤
    5.1 初始标记:STW、快;GC Root 能直接关联的对象
    5.2 并发标记:并发;GC Root Tracing 的过程
    5.3 重新标记:STW、快;修复并发标记阶段 用户线程运行时变动的对象
    5.4 并发清除:并发
  6. 因为整个过程中耗时最长的 “并发标记”和“并发清除”是和用户线程并发执行的,所以可认为CMS回收器是和用户线程并发执行的

1.volatile
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

volatile两条实现原则:

  1. Lock前缀指令会引起处理器缓存回写到内存。
  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
    在JDK 7的并发包里新增一个队列集合类LinkedTransferQueue,它在使用volatile变量时,用一种追加字节的方式来优化队列出队和入队的性能。(补足64个字节,使得缓存中的头尾节点不会被读到一个缓存行中,也就不能相互锁定)
    不应该追加字节的场景:
  • 缓存行非64字节宽的处理器。
  • 共享变量不会被频繁地写。

1.synchronized
用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下3种形式。

  • ·对于普通同步方法,锁是当前实例对象。
  • ·对于静态同步方法,锁是当前类的Class对象。
  • ·对于同步方法块,锁是Synchonized括号里配置的对象。
    当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
    (synchronized本质上是去获取对象对应的monitor的所有权)
    synchronized用的锁是存在Java对象头里的。如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit,
    在Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。
    》偏向锁
    大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
简单说就算获得它的线程退出后先不释放锁,下一次如果还是这个线程进入的话就不用再获取锁了, 只是去比较一下是否是偏向锁且锁的markword中的线程ID是否是当前ID,
如果是的话,都不用CAS,就直接进入同步块了。 如果有别的线程来竞争的话,就撤销偏向锁标志了。

》轻量级锁

如果当前不是偏向锁,且线程通过CAS修改锁的markword成功了,则就将其标志为轻量级锁
如果CAS没成功,则一直自旋CAS,如果失败到一定程度,则认为膨胀为重量级锁,修改其锁标志,这个自旋的线程再进入阻塞状态。
任何去竞争对象锁的线程都有权利按照规则修改其对象头的锁标志(不管是否获取到锁),但对象头的markword中的指向的锁记录的栈帧所属的线程才是该线程获得锁的标志
如果有线程去尝试获得锁,发现是重量级锁,就不会自旋CAS了,而是直接进入阻塞

在Java中可以通过锁和循环CAS的方式来实现原子操作。
(只有AtomicXXX才能使用CAS)

CAS实现原子操作的三大问题

  • ABA问题,java中使用AtomicStampedReference解决,这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
  • 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
  • 只能保证一个共享变量的原子操作。这个时候就可以用锁

》使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。

新装的ubuntu16,自带的python2.7,使用pip升级后,报错,修改记录如下:

如何彻底卸载pip?

sudo python -m pip uninstall pip

如何安装pip

sudo apt install python-pip

如何安装指定版本的pip

python -m pip install pip==18.1(换成你想要的版本编号)

pip安装软件过程中报错:

Command “python setup.py egg_info” failed with error code 1 in /tmp/pip-build-BqMhb7/matplotlib/

  1. 安装并升级setuptools
    pip install –upgrade setuptools
  2. 升级pip
    pip install –upgrade pip

pip升级后报错

1
2
3
4
5
6
7
8
9
10
11
12
13
Traceback (most recent call last):
File "/usr/bin/pip", line 9, in <module>
from pip import main
ImportError: cannot import name 'main'
修改/usr/bin/pip如下:
//修改前
from pip import main
if __name__ == '__main__':
sys.exit(main())
修改后
from pip import __main__ //这行也要修改
if __name__ == '__main__':
sys.exit(__main__._main())//增加__main__._ (注意最后main()前面有个_)