Android 播放视频并获取指定时间的帧画面
最近做的项目要求既能播放视频(类似于视频播放器),又能每隔1s左右获取一帧视频画面,然后对图片进行处理,调查了一周,也被折磨了一周,总算找到了大致符合要求的方法。首先对调查过程中涉及到的方法进行简单介绍,再重点介绍最终所采用的方法,话不多说,进入正题。
一.MediaMetadataRetriever
播放视频并取得画面的一帧,大家最先想到应该都是这个,我同样也最先对它进行了测试,这里使用MediaPlayer进行播放,视频播放界面使用SurfaceView来实现。
public class PlayerMainActivity extends Activity implements OnClickListener,
SurfaceHolder.Callback, Runnable { private static final String TAG = "Movie";
private MediaPlayer mediaPlayer;
private SurfaceView surfaceView;
private SurfaceHolder surfaceHolder;
private Button play_btn;private int currentPosition = 0;
private Bitmap bitmap = null;
private String dataPath = Environment.getExternalStorageDirectory() + "/Video/Test_movie.AVI"; @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); surfaceView = (SurfaceView) findViewById(R.id.surfaceView);
play_btn = (Button) findViewById(R.id.play_btn);
play_btn.setOnClickListener(this);
screen_cut_btn.setOnClickListener(this); surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(this);
} @Override
public void run() {
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setDisplay(surfaceHolder);
try {
mediaPlayer.setDataSource(dataPath);
mediaPlayer.prepare();
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
mediaMetadataRetriever.setDataSource(dataPath);
int millis = mediaPlayer.getDuration();
Log.i(TAG, "millis: " + millis/1000);
for (int i = 10000*1000; i < 20*1000*1000; i+=500*1000) { bitmap = mediaMetadataRetriever.getFrameAtTime(i, MediaMetadataRetriever.OPTION_CLOSEST); String path = Environment.getExternalStorageDirectory() + "/bitmap/" + i + ".png";
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(path);
bitmap.compress(CompressFormat.PNG, 100, fileOutputStream);
Log.i(TAG, "i: " + i/1000/1000);
} catch (Exception e) {
Log.i(TAG, "Error: " + i/1000/1000);
e.printStackTrace();
}
finally {
if (fileOutputStream != null) {
fileOutputStream.close();
}
}
bitmap.recycle();
}
} catch (Exception e) {
e.printStackTrace();
}
} @Override
public void surfaceCreated(SurfaceHolder holder) {
Thread t = new Thread(this);
t.start();
} @Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
} @Override
public void surfaceDestroyed(SurfaceHolder holder) {
} @Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.play_btn:
if (mediaPlayer.isPlaying()) {
mediaPlayer.pause();
play_btn.setText(getResources().getText(R.string.play));
} else {
mediaPlayer.start();
play_btn.setText(getResources().getText(R.string.pause));
}
break;default:
break;
}
} @Override
protected void onDestroy() {
super.onDestroy();
if (mediaPlayer.isPlaying()) {
mediaPlayer.stop();
}
mediaPlayer.release();
}
}
获取一帧的关键代码为:
Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(timeMs * 1000, MediaMetadataRetriever.OPTION_CLOSEST);
public Bitmap getFrameAtTime(long timeUs, int option)
第一个参数是传入时间,只能是us(微秒)
第二个参数:
- OPTION_CLOSEST 在给定的时间,检索最近一个帧,这个帧不一定是关键帧。
- OPTION_CLOSEST_SYNC 在给定的时间,检索最近一个同步与数据源相关联的的帧(关键帧)。
- OPTION_NEXT_SYNC 在给定时间之后检索一个同步与数据源相关联的关键帧。
- OPTION_PREVIOUS_SYNC 顾名思义,同上
这里为了提取我们想要的帧,不使用关键帧,所以用 OPTION_CLOSEST .
最终的测试结果并不理想,连续取20帧画面,其中真正有效的只有7张,其余都是重复的,原因为即使是使用参数OPTION_CLOSEST,程序仍然会去取指定时间临近的关键帧,如10s-15s总是取同一帧,因此这种方法不可用。
提高视频的质量或许有效,未尝试。
补充MediaMetadataRetriever的其他知识
// 取得视频的总长度(单位为毫秒)
String time = mediaMetadataRetriever. extractMetadata( MediaMetadataRetriever. METADATA_KEY_DURATION);
MediaMetadataRetriever主要用来取缩略图。
二.ThumbnailUtils
同样主要是用来获取视频的缩略图,不可靠,因此并未深入研究。
从以上两种方法可以看出,Android API 所提供的获取视频帧的方法大都只能获取视频的缩略图,没办法获取视频每一帧的图片。因此,调查方向应当转向对视频进行解码,然后获取任意一帧。
三.MediaCodec
硬件解码,尝试从inputBuffers、outputBuffers中获取帧画面,失败,bitmap中的数据大小始终为0 KB。
public class MoviePlayerActivity extends Activity implements OnTouchListener, OnClickListener, SurfaceHolder.Callback { private static final String TAG = "Image";
private String file_path;
private Button movie_play;
private boolean playButtonVisible;
private boolean playPause;
private SurfaceView surfaceView;
private SurfaceHolder surfaceHolder;
private PlayerThread playerThread = null;
private ByteBuffer mPixelBuf; @Override
protected void onCreate(Bundle savedInstanceState) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); super.onCreate(savedInstanceState);
setContentView(R.layout.movie_player_activity);
movie_play = (Button) findViewById(R.id.movie_videoview_play);
movie_play.setOnClickListener(this);
movie_play.setText("Play"); Intent intent = getIntent();
file_path = intent.getStringExtra("file_path"); //此Activity是从其他页面跳转来的,file_path为要播放的视频地址 surfaceView = (SurfaceView) findViewById(R.id.surfaceView);
surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(this);
mPixelBuf = ByteBuffer.allocateDirect(640*480*4);
mPixelBuf.order(ByteOrder.LITTLE_ENDIAN);
} @Override
public void surfaceCreated(SurfaceHolder holder) {
if (playerThread == null) {
playerThread = new PlayerThread(holder.getSurface());
playerThread.start();
}
} @Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
} @Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (playerThread != null) {
playerThread.interrupt();
}
} @Override
public boolean onTouch(View v, MotionEvent event) {
if (!playButtonVisible) {
movie_play.setVisibility(View.VISIBLE);
movie_play.setEnabled(true);
} else {
movie_play.setVisibility(View.INVISIBLE);
}
playButtonVisible = !playButtonVisible;
return false;
} @Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.movie_videoview_play:
if (!playPause) {
movie_play.setText("Pause");
} else {
movie_play.setText("Play");
}
playPause = !playPause;
break;
default:
break;
}
} private void writeFrameToSDCard(byte[] bytes, int i, int sampleSize) { i++;
if (i%10 == 0) {
try {
Bitmap bmp = BitmapFactory.decodeByteArray(bytes, 0, sampleSize);
mPixelBuf.rewind();
bmp.copyPixelsFromBuffer(mPixelBuf);
String path = Environment.getExternalStorageDirectory() + "/bitmap/" + i + ".png";
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(path);
bmp.compress(CompressFormat.PNG, 90, fileOutputStream);
bmp.recycle();
Log.i(TAG, "i: " + i);
} catch (Exception e) {
Log.i(TAG, "Error: " + i);
e.printStackTrace();
}
finally {
if (fileOutputStream != null) {
fileOutputStream.close();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
} private class PlayerThread extends Thread { private MediaExtractor extractor;
private MediaCodec mediaCodec;
private Surface surface; public PlayerThread(Surface surface) {
this.surface = surface;
} @Override
public void run() {
extractor = new MediaExtractor();
try {
extractor.setDataSource(file_path);
} catch (IOException e1) {
Log.i(TAG, "Error");
e1.printStackTrace();
} for (int i = 0; i < extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
extractor.selectTrack(i);
mediaCodec = MediaCodec.createDecoderByType(mime);
mediaCodec.configure(format, surface, null, 0);
break;
}
} if (mediaCodec == null) {
Log.e(TAG, "Can't find video info!");
return;
} mediaCodec.start(); ByteBuffer[] inputBuffers = mediaCodec.getInputBuffers();
ByteBuffer[] outputBuffers = mediaCodec.getOutputBuffers();
BufferInfo info = new BufferInfo();
boolean isEOS = false;
long startMs = System.currentTimeMillis(); int i = 0;
while (!Thread.interrupted()) {
if (!isEOS) {
int inIndex = mediaCodec.dequeueInputBuffer(10000);
if (inIndex >= 0) {
ByteBuffer buffer = inputBuffers[inIndex];
int sampleSize = extractor.readSampleData(buffer, 0);
if (sampleSize < 0) {
// We shouldn't stop the playback at this point, just pass the EOS
// flag to mediaCodec, we will get it again from the dequeueOutputBuffer
Log.d(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM");
mediaCodec.queueInputBuffer(inIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
isEOS = true;
} else {
mediaCodec.queueInputBuffer(inIndex, 0, sampleSize, extractor.getSampleTime(), 0);
extractor.advance();
}
}
} int outIndex = mediaCodec.dequeueOutputBuffer(info, 100000); switch (outIndex) {
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
Log.d(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
outputBuffers = mediaCodec.getOutputBuffers();
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
Log.d(TAG,"New format " + mediaCodec.getOutputFormat());
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
Log.d(TAG, "dequeueOutputBuffer timed out!");
break;
default:
ByteBuffer buffer = outputBuffers[outIndex];
Log.v(TAG,"We can't use this buffer but render it due to the API limit, " + buffer); // We use a very simple clock to keep the video FPS, or the video
// playback will be too fast
while (info.presentationTimeUs / 1000 > System.currentTimeMillis() - startMs) {
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
mediaCodec.releaseOutputBuffer(outIndex, true); /* saves frame to SDcard */
mPixelBuf.rewind();
GLES20.glReadPixels(0, 0, 640, 480, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, mPixelBuf);
try {
ByteBuffer outByteBuffer = outputBuffers[outIndex];
outByteBuffer.position(info.offset);
outByteBuffer.limit(info.offset + info.size); //info的两个参数值始终为0,所保存的.png也都是0KB。
outByteBuffer.limit(2);
byte[] dst = new byte[outByteBuffer.capacity()];
outByteBuffer.get(dst);
writeFrameToSDCard(dst, i, dst.length);
i++;
} catch (Exception e) {
Log.d(TAG, "Error while creating bitmap with: " + e.getMessage());
}
break;
} // All decoded frames have been rendered, we can stop playing now
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.d(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM");
break;
}
} mediaCodec.stop();
mediaCodec.release();
extractor.release();
}
}
}
所保存的图片大小始终为0的原因大概和下面的方法五类似。
想要深入研究此方法可以参考:
http://stackoverflow.com/questions/19754547/mediacodec-get-all-frames-from-video
http://stackoverflow.com/questions/23321880/how-to-get-bitmap-frames-from-video-using-mediacodec
四.JCodec
jcodec-samples-0.1.7.apk
解码视频文件耗时较长,性能较差,平均1.5s取一帧图片,达不到要求。
五.使用VideoView播放视频,使用getDrawingCache获取View视图
VideoView是Android提供的播放视频组件,上手很容易。这里使用getDrawingCache获取控件的View。
但是DrawingCache只能截取非视频部分的画面,播放视频的那个小窗口一直是黑色的。原因为Activity画面走的是framebuffer,视频是硬解码推送过来的,所有读取/dev/graphics/fb0 视频播放的那一块就是黑色的,硬件解码不会推送到buffer的,而是直接推送到硬件输出了。
public class MoviePlayerActivity extends Activity implements OnTouchListener, OnClickListener, Runnable { private static final String TAG = "Image";
private String file_path;
private VideoView videoView;
private Button movie_play;
private boolean playButtonVisible;
private boolean playPause; @Override
protected void onCreate(Bundle savedInstanceState) {
// full screen
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
// no title
requestWindowFeature(Window.FEATURE_NO_TITLE);
// landscape or horizontal screen
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); super.onCreate(savedInstanceState);
setContentView(R.layout.movie_player_activity);
movie_play = (Button) findViewById(R.id.movie_videoview_play);
movie_play.setOnClickListener(this);
movie_play.setText("Play"); Intent intent = getIntent();
file_path = intent.getStringExtra("file_path"); videoView = (VideoView) findViewById(R.id.movie_palyer_videoview);
videoView.setMediaController(null);
// videoView.setMediaController(new MediaController(this));
videoView.setVideoPath(file_path);
videoView.start();
videoView.requestFocus(); Thread screenShootThread = new Thread(this);
screenShootThread.start();
videoView.setOnTouchListener(this);
} @Override
public void run() { //播放视频时后台自动截图,注意参数为videoView
for (int i = 10000 * 1000; i < 20 * 1000 * 1000; i += 500 * 1000) {
int nowTime = videoView.getCurrentPosition();
try {
screenShot(videoView, i);
} catch (Exception e1) {
Log.i(TAG, "Error: screenShot. ");
e1.printStackTrace();
} try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} @Override
public boolean onTouch(View v, MotionEvent event) {
if (!playButtonVisible) {
movie_play.setVisibility(View.VISIBLE);
movie_play.setEnabled(true);
} else {
movie_play.setVisibility(View.INVISIBLE);
}
playButtonVisible = !playButtonVisible; return false;
} @Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.movie_videoview_play:
if (!playPause) {
movie_play.setText("Pause");
} else {
movie_play.setText("Play");
}
playPause = !playPause; int nowTime = videoView.getCurrentPosition();
Log.i(TAG, "nowTime: " + nowTime);
try {
screenShot(videoView, nowTime); //点击按钮截图,注意参数为videoView
} catch (Exception e) {
e.printStackTrace();
} break;
default:
break;
}
} public void screenShot(View view, int nowTime) throws Exception { view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
String path = Environment.getExternalStorageDirectory() + "/bitmap/" + nowTime + ".png";
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(path);
bitmap.compress(Bitmap.CompressFormat.PNG, 90, fileOutputStream);
Log.i(TAG, "i: " + nowTime);
} catch (Exception e) {
Log.i(TAG, "Error: " + nowTime);
e.printStackTrace();
} finally {
if (fileOutputStream != null) {
fileOutputStream.close();
}
}
bitmap.recycle();
view.setDrawingCacheEnabled(false);
}
}
DrawingCache 获取其他控件如Button等View时正常。
六.VideoView播放视频,MediaMetadataRetriever获取帧画面(就是你了!)
能够正常获取帧画面,并且画面之间不重复,即不是只取关键帧,而是去取相应时间点的帧画面。若不保存为图片(.png/.jpg),耗时最多为0.4s,基本达到要求。
参考:http://yashirocc.blog.sohu.com/175636801.html
public class MoviePlayerActivity extends Activity implements OnTouchListener, OnClickListener, Runnable { private static final String TAG = "ImageLight";
private String file_path;
private VideoView videoView;
private Button movie_play;
private boolean playButtonVisible;
private boolean playPause; @Override
protected void onCreate(Bundle savedInstanceState) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); // full screen
requestWindowFeature(Window.FEATURE_NO_TITLE); // no title
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); // landscape or horizontal screen super.onCreate(savedInstanceState);
setContentView(R.layout.movie_player_activity);
movie_play = (Button) findViewById(R.id.movie_videoview_play);
movie_play.setOnClickListener(this);
movie_play.setText("Play"); Intent intent = getIntent();
file_path = intent.getStringExtra("file_path"); videoView = (VideoView) findViewById(R.id.movie_palyer_videoview);
videoView.setMediaController(null);
// videoView.setMediaController(new MediaController(this));
videoView.setVideoPath(file_path);
videoView.start();
videoView.requestFocus(); Thread screenShootThread = new Thread(this);
screenShootThread.start();
videoView.setOnTouchListener(this);
} @Override
public void run() {
MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
metadataRetriever.setDataSource(file_path); for (int i = 40000 * 1000; i < 50 * 1000 * 1000; i += 500 * 1000) {
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
Bitmap bitmap = metadataRetriever.
getFrameAtTime(videoView.getCurrentPosition()*1000, MediaMetadataRetriever.OPTION_CLOSEST);
Log.i(TAG, "bitmap---i: " + i/1000); String path = Environment.getExternalStorageDirectory() + "/bitmap/" + i + ".png";
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(path);
bitmap.compress(Bitmap.CompressFormat.PNG, 90, fileOutputStream);
Log.i(TAG, "i: " + i/1000);
} catch (Exception e) {
Log.i(TAG, "Error: " + i/1000);
e.printStackTrace();
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
bitmap.recycle();
}
} @Override
public boolean onTouch(View v, MotionEvent event) {
if (!playButtonVisible) {
movie_play.setVisibility(View.VISIBLE);
movie_play.setEnabled(true);
} else {
movie_play.setVisibility(View.INVISIBLE);
}
playButtonVisible = !playButtonVisible;
return false;
} @Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.movie_videoview_play:
if (!playPause) {
movie_play.setText("Pause");
} else {
movie_play.setText("Play");
}
playPause = !playPause;
break;
default:
break;
}
}
}
Android 播放视频并获取指定时间的帧画面的更多相关文章
- Android 播放视频文件
package com.example.myvideo2; import java.io.File; import android.app.Activity; import android.net.U ...
- Java获取指定时间的毫秒值的方法
有以下两种方法获取指定时间的毫秒值: 1.Calendar类 先由getInstance获取Calendar对象,然后用clear方法将时间重置为(1970.1.1 00:00:00),接下来用set ...
- android: 播放视频
播放视频文件其实并不比播放音频文件复杂,主要是使用 VideoView 类来实现的.这个 类将视频的显示和控制集于一身,使得我们仅仅借助它就可以完成一个简易的视频播放器. VideoView 的用法和 ...
- js获取指定时间的前几秒
最近项目上有一个需求是:根据一张图片的拍摄时间获取到这个时间前二后三的一个五秒钟的视频信息,通过查找相关资料写了一个方法拿来记录分享一下. //指定时间减2秒function reduceTwoS(d ...
- Android播放视频
package cn.c; import java.io.File; import java.io.IOException; import android.app.Activity; ...
- Shell获取指定时间
时区基本知识 相差一个时区(经度15度)时间相差一小时.简单计算口诀:1.同一个时区的相差时间用减法,2.不同时区的相差时间用加法.3东加西减.例如一,东八区是8点,问东1区几点.根据上面口诀1,算: ...
- android播放器如何获取音乐文件信息
转http://blog.csdn.net/hellofeiya/article/details/8464356, android自带的音乐播放器中,在获取音乐文件信息的时候是通过扫描得到相关信息的. ...
- H5调用Android播放视频
webView.loadUrl("http://10.0.2.2:8080/assets/RealNetJSCallJavaActivity.htm"); js调用的Java文件中 ...
- android 播放视频时切换全屏隐藏状态栏
1. Demo: @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstance ...
随机推荐
- c++动态绑定与静态绑定
C++为了支持多态性,采用了动态绑定和静态绑定 相关概念: 对象的静态类型:对象在声明时采用的类型,编译时确定 对象的动态类型:目前所指对象的类型,在运行时确定 class B { } class C ...
- MYSQL命令cmd操作
今天我们就来看一下数据库的各种命令,以下命令全部是从CMD命令窗口下的命令行输入指令,首先如果如果输入mysql,系统提示“mysql不是内部命 令或外部命令.那么这其实是环境变量没有设置好的原因,例 ...
- problem 1 -- Two sum
很简单.没什么好说的.但是在阿里实习的第四面的时候居然问到了. 大意是给出一组无序数列和目标数Z,在无序数列中找到X和Y,使得X+Y=Z. 有两种方法: 一种是排序后,同时首尾搜索.时间复杂度为O(n ...
- 高性能、高并发TCP服务器(多线程调用libevent)
from:http://blog.csdn.net/i_am_jojo/article/details/7587838 本文讲述的TCP服务器是模仿memcache中的TCP网络处理框架,其中是基于l ...
- ODBC方式连接Informix数据库
公司某个报表系统使用Informix数据库,在谋划使用Perl语言写数据采集程序后,花费了很多时间建立Perl访问Informix连接.恰巧Windows下ActivePerl的CPAN中又没有DBD ...
- sql join 用法
SQL JOIN 的用法 关于sql语句中的连接(join)关键字,是较为常用而又不太容易理解的关键字,下面这个例子给出了一个简单的解释 --建表table1,table2:create tabl ...
- “父窗口拖动的时候Popup不随着父窗口移动”问题的解决方案
我们用WPF用的Popup时候会发现,当 StaysOpen=True 的时候,因为Popup不会消失,在父窗口移走的时候Popup仍旧在原地...作者在国外网站上无意间发现了这个解决方案,拿出来给大 ...
- 普及下Oracle hints语法
普及下Oracle hints的语法:{DELETE|INSERT|SELECT|UPDATE} /*+ hint [text] [hint[text]]... */ 1.hint只能出现在诸如sel ...
- MINA的session.close
现象:客户端session.close之后,并没有提出,客户端程序一直hold在那里: 解决:调用了session.getService().dispose(false)方法后,客户端程序完成了退出 ...
- 分享 - Social.framework
/** * 第三方分享 * * @param void 友盟分享 * @param shareSDK * @param 百度分享 */ #import "ViewController ...