Android相机开发——CameraView源码解析

前言

年底公司赶项目,忙得不亦乐乎,博客也很久没更新了。公司项目里用到了自定义摄像头的模块,也参考了Google开源项目CameraView来实现版本兼容的问题,这篇博客也是忙里偷闲来总结一下CameraView的源码。

相机开发需要注意的问题

随着Android版本的不断升级,Android对摄像头的调用方法也在不断更新,导致日趋严重的碎片化问题。

  • 版本兼容性问题:Androd提供了两套调用摄像头的API,Android5.0以下的Camera和Android5.0以上的Camera2。同时,Android又提供了两种常用的摄像头预览控件:Android 4.0以下的SurfaceView和Android 4.0以上的TextureView
  • 设备兼容性问题:各个厂家硬件设计不同和对摄像头的调校力度不同,对Camera/Camera2的支持程度也不同,甚至有些5.0以上的设备并不支持Camera2,需要降级使用Camera。要解决这种设备兼容性问题需要不断积累,一点一点踩坑。
  • 各个场景下的生命周期变化:如应用程序切换至后台运行、预览状态下锁屏、横竖屏切换等等,对应各个场景变化,我们需要随之执行摄像头的申请与释放、Surface的创建与销毁等一系列动作。

Android官方推出的开源项目CameraView,主要针对摄像头的版本兼容性问题提供了一种解决方案。Android5.0以下用Camera,Android5.0以上用Camera2;Android4.0以下用SurfaceView和Android4.0以上用TextureView。个别厂家设备Android5.0并不支持Camera2,这种情况不在我们的讨论范围内。

版本兼容策略,图片来源于参考资料

自定义相机的开发流程

自定义相机开发流程一般分为以下几个步骤:

  • 检查权限:在AndroidManifest.xml中添加Camera相关功能使用的权限,Android6.0以上需要申请动态权限。
  • 打开摄像头:检测并访问相机资源,打开指定的摄像头(前置或后置),这一动作比较耗时,一般在子线程中执行。打开成功后获取到摄像头对象,通过它可以获得该摄像头相关参数,也可以设置一些自定义参数(如闪光灯、对焦模式)。
  • 开启预览:创建预览控件和设置预览参数,摄像头预览数据同步显示在预览控件上。
  • 执行拍照:设置回调监听,获取拍照回传的图像数据。将拍摄获得的图像转换成位图文件,最终输出保存成各种常用格式的图片。
  • 关闭预览:对应各个场景生命周期执行关闭预览。
  • 关闭摄像头:当相机使用完毕后,应用程序必须正确地将其释放,以免消耗资源和发生异常冲突。
相机开发流程图,来源见水印

CameraView生命周期管理

我们在相机开发需要注意的问题一节中强调过对生命周期的管理。CameraView巧妙的将自身的生命周期封装在控件内部交由上层(Activity)进行维护,同时也简化了调用流程。我们可以看看CmaeraView项目首页上的用法介绍:

<com.google.android.cameraview.CameraView
    android:id="@+id/camera"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:keepScreenOn="true"
    android:adjustViewBounds="true"
    app:autoFocus="true"
    app:aspectRatio="4:3"
    app:facing="back"
    app:flash="auto"/>
    @Override
    protected void onResume() {
        super.onResume();
        mCameraView.start();
    }
    
    @Override
    protected void onPause() {
        mCameraView.stop();
        super.onPause();
    }

这里可以看到,只需要在Activity的onResume()执行mCameraView.start()和在onPause()执行mCameraView.stop()就可以简单维护整个CameraView的生命周期。

在CameraView内部,也在View的各个生命周期中做了相对应的操作。

CameraView内部生命周期方法

CameraView的版本兼容策略

CameraView为了解决版本兼容性问题,采用了策略模式,对应不同系统版本使用不同的实现方式。

CameraView的策略模式

上图可以看到,CameraView中包含两个抽象类接口,CameraViewImpl对应对摄像头操作的具体实现,根据系统版本分别对应Camera和Camera2;PreviewImpl对应对预览控件的具体实现,根据系统版本分别对应SurfaceView与TextureView。下面看一下具体代码:

    CameraViewImpl mImpl;
    
    public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        if (isInEditMode()){
            mCallbacks = null;
            mDisplayOrientationDetector = null;
            return;
        }
        // Internal setup
        final PreviewImpl preview = createPreviewImpl(context);
        mCallbacks = new CallbackBridge();
        if (Build.VERSION.SDK_INT < 21) {
            mImpl = new Camera1(mCallbacks, preview);
        } else if (Build.VERSION.SDK_INT < 23) {
            mImpl = new Camera2(mCallbacks, preview, context);
        } else {
            mImpl = new Camera2Api23(mCallbacks, preview, context);
        }
        // Attributes
        ...
        // Display orientation detector
        ...
    }

我们可以看到,Android5.0以下用Camera,Android5.0以上用Camera2,Android6.0以上在Camera2的基础上尝试输出最高清的分辨率。下面再来看一下createPreviewImpl方法:

@NonNull
    private PreviewImpl createPreviewImpl(Context context) {
        PreviewImpl preview;
        if (Build.VERSION.SDK_INT < 14) {
            preview = new SurfaceViewPreview(context, this);
        } else {
            preview = new TextureViewPreview(context, this);
        }
        return preview;
    }

上面代码展示了CameraView对与预览控件的版本兼容策略,Android4.0以下用SurfaceView,Android4.0以上用TextureView。

PreviewImpl内部封装了一系列对预览控件的操作方法,SurfaceViewPreview和TextureViewPreview分别对应了SurfaceView和TextureView对PreviewImpl的具体实现。在其具体实现类内部又实现了控件生命周期的回调接口,一些复杂的生命周期(如横竖屏切换)也在其内部做了相应处理。

由于篇幅原因,这里不再对SurfaceView和TextureView做过多讲解,关于SurfaceView与TextureView的详情,请参考Android 5.0(Lollipop)中的SurfaceTexture,TextureView,SurfaceView和GLSurfaceView

Camera1的相机实现

这一小节将解析使用Camera API来实现相机开发,对应前文的相机开发流程我们进行逐步解析:

  • 打开摄像头:调用Camera.open(),打开相机,默认为后置,可以根据摄像头ID来指定打开前置还是后置。
  • 开启预览:调用Camera.getParameters()获取Camera.Parameters对象,在Camera.Parameters对象中设置自定义参数,调用Camera.setPreviewDispaly(SurfaceHolder holder),指定预览控件的SurfaceHolder,调用Camera.startPreview()方法开启预览。
  • 执行拍照:调用Camera.takePicture()方法进行拍照。
  • 关闭预览:调用Camera.stopPreview()关闭预览。
  • 关闭摄像头:调用Camera.release()关闭摄像头。

下面来看一下在Camera1类中的具体实现代码,首先看CameraViewImpl的入口start()方法:

@Override
    boolean start() {
        //选择指定摄像头
        chooseCamera();
        //打开摄像头
        openCamera();
        //设置预览控件
        if (mPreview.isReady()) {
            setUpPreview();
        }
        mShowingPreview = true;
        //开启预览
        mCamera.startPreview();
        return true;
    }

start()方法执行了打开摄像头、开启预览两个步骤,我们对这里调用的每一个方法进行分析:

private void chooseCamera() {
        //遍历所有摄像头设备,找到指定的摄像头
        for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) {
            Camera.getCameraInfo(i, mCameraInfo);
            if (mCameraInfo.facing == mFacing) {
                mCameraId = i;
                return;
            }
        }
        mCameraId = INVALID_CAMERA_ID;
    }

调用chooseCamera()遍历所有摄像头设备,找到指定的摄像头。

    private void openCamera() {
        if (mCamera != null) {
            //如果摄像头已经开启,先释放掉
            releaseCamera();
        }
        //打开摄像头
        mCamera = Camera.open(mCameraId);
        //获得Camera.Parameters对象
        mCameraParameters = mCamera.getParameters();
        //获取该摄像头支持的所有预览尺寸
        mPreviewSizes.clear();
        for (Camera.Size size : mCameraParameters.getSupportedPreviewSizes()) {
            mPreviewSizes.add(new Size(size.width, size.height));
        }
        //获取该摄像头支持的所有拍照尺寸
        mPictureSizes.clear();
        for (Camera.Size size : mCameraParameters.getSupportedPictureSizes()) {
            mPictureSizes.add(new Size(size.width, size.height));
        }
        // AspectRatio
        if (mAspectRatio == null) {
            mAspectRatio = Constants.DEFAULT_ASPECT_RATIO;
        }
        //根据需求设置自定义参数,这里主要是设置预览尺寸、拍照尺寸、对焦模式、闪光灯等。
        adjustCameraParameters();
        //设置水平垂直方向
        mCamera.setDisplayOrientation(calcDisplayOrientation(mDisplayOrientation));
        //执行开启摄像头成功的回调方法
        mCallback.onCameraOpened();
    }

打开摄像头,根据需求设置预览尺寸、拍照尺寸、对焦模式、闪光灯等参数,设置水平垂直方向,执行开启摄像头成功的回调方法。

    void setUpPreview() {
        try {
            if (mPreview.getOutputClass() == SurfaceHolder.class) {
                final boolean needsToStopPreview = mShowingPreview && Build.VERSION.SDK_INT < 14;
                if (needsToStopPreview) {
                    mCamera.stopPreview();
                }
                mCamera.setPreviewDisplay(mPreview.getSurfaceHolder());
                if (needsToStopPreview) {
                    mCamera.startPreview();
                }
            } else {
                mCamera.setPreviewTexture((SurfaceTexture) mPreview.getSurfaceTexture());
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

setUpPreview()方法主要是根据系统版本和预览状态来指定预览控件。

所有准备工作完成以后,调用Camera.startPreview()方法开启预览。预览开启以后就可以触发执行拍照动作,来看一下拍照的具体方法takePicture():

@Override
    void takePicture() {
        //如果摄像头未开启,抛出异常
        if (!isCameraOpened()) {
            throw new IllegalStateException(
                    "Camera is not ready. Call start() before takePicture().");
        }
        //判断有没有开启自动对焦,如果有自动对焦则监听对焦完成以后执行拍照
        if (getAutoFocus()) {
            mCamera.cancelAutoFocus();
            mCamera.autoFocus(new Camera.AutoFocusCallback() {
                @Override
                public void onAutoFocus(boolean success, Camera camera) {
                    takePictureInternal();
                }
            });
        } else {
            //没有自动对焦,直接拍照
            takePictureInternal();
        }
    }
    
    void takePictureInternal() {
        if (!isPictureCaptureInProgress.getAndSet(true)) {
            //拍照,并监听拍照完成的回调
            mCamera.takePicture(null, null, null, new Camera.PictureCallback() {
                @Override
                public void onPictureTaken(byte[] data, Camera camera) {
                    isPictureCaptureInProgress.set(false);
                   //拍照完成,byte[] data为拍照的图像数据
                   //这里是使用回调方法将数据传到外部去执行下一步动作。
                    mCallback.onPictureTaken(data);
                    camera.cancelAutoFocus();
                    camera.startPreview();
                }
            });
        }
    }
    

拍照完成以后,可以根据需求来关闭预览和关闭摄像头,这里主要解析一下stop()方法:

@Override
    void stop() {
        if (mCamera != null) {
            //关闭预览
            mCamera.stopPreview();
        }
        mShowingPreview = false;
        //关闭摄像头
        releaseCamera();
    }
    
    private void releaseCamera() {
        if (mCamera != null) {
            //释放摄像头
            mCamera.release();
            mCamera = null;
            //摄像头关闭以后执行回调方法
            mCallback.onCameraClosed();
        }
    }

到这里Camera1的拍照流程解析完成了,我们可以学习到相机开发流程各个步骤所对应的Camera API的常用实现方法。

Camera2的相机实现

Android 5.0以上版本将原来的camera API弃用转而推荐使用新增的camera2 API,camera2 API理解起来更为抽象,开发难度也更大,但是功能也更加强大。

camera2 流程示意图 图片来源见参考资料

这里引用了管道的概念将安卓设备和摄像头之间联通起来,系统向摄像头发送 Capture 请求,而摄像头会返回 CameraMetadata。这一切建立在一个叫作 CameraCaptureSession 的会话中。

camera2 拍照流程图 图片来源见参考资料

上图可以简单的了解Camera2的拍照流程,对应前文的相机开发流程我们进行逐步解析:

  • 打开摄像头:首先通过Context.getSystemService(Context.CAMERA_SERVICE)获得CameraManager对象,通过CameraManager找到指定的摄像头对象CameraDevice和对应的CameraId,再调用CameraManager.openCamera(currentCameraId, stateCallback, backgroundHandler)打开摄像头。
  • 开启预览:Camera2需要通过创建请求来实现调用。CameraDevice.createCaptureSession(...)方法创建CaptureSession,在createCaptureSession()方法参数中设置请求类型和预览参数,再在回调函数中执行cameraCaptureSession.setRepeatingRequest(...)方法请求预览。
  • 执行拍照:先设置好CaptureRequest请求的相关参数,在调用cameraCaptureSession.capture()完成拍照,拍照的图片数据在ImageReader对象的回调接口中获取。
  • 关闭预览:CaptureSession.close()关闭会话对象。
  • 关闭摄像头:CameraDevice.close()关闭摄像头。

下面来看一下在Camera2类中的具体实现代码,还是start()方法开始:

    private final CameraManager mCameraManager;
    
    Camera2(Callback callback, PreviewImpl preview, Context context) {
        super(callback, preview);
        //构造函数中获取CameraManager对象
        mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
        mPreview.setCallback(new PreviewImpl.Callback() {
            @Override
            public void onSurfaceChanged() {
                startCaptureSession();
            }
        });
    }
    
    @Override
    boolean start() {
        //找到指定的摄像头
        if (!chooseCameraIdByFacing()) {
            return false;
        }
        //设置相关配置,预览尺寸、拍照尺寸等
        collectCameraInfo();
        //设置ImageReader及其拍照数据的回调接口
        prepareImageReader();
        //打开摄像头
        startOpeningCamera();
        return true;
    }

首先在构造函数中获取CameraManager对象为摄像头管理类,用于检测摄像头,打开系统摄像头,获取摄像头特性等。

private boolean chooseCameraIdByFacing() {
        try {
            int internalFacing = INTERNAL_FACINGS.get(mFacing);
            //获得所有摄像头的cameraId
            final String[] ids = mCameraManager.getCameraIdList();
            if (ids.length == 0) { // No camera
                throw new RuntimeException("No camera available.");
            }
            //遍历查找指定的摄像头
            for (String id : ids) {
                //获取该摄像头的描述信息
                CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(id);
                
                ...
                
                if (internal == internalFacing) {
                    mCameraId = id;
                    mCameraCharacteristics = characteristics;
                    return true;
                }
            }
            
            ...
            
            return true;
        } catch (CameraAccessException e) {
            throw new RuntimeException("Failed to get a list of camera devices", e);
        }
    }

chooseCameraIdByFacing()方法就是遍历所有摄像头设备,根据摄像头描述类CameraCharacteristics获取指定到的摄像头。

    private void collectCameraInfo() {
        StreamConfigurationMap map = mCameraCharacteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
        if (map == null) {
            throw new IllegalStateException("Failed to get configuration map: " + mCameraId);
        }
        //遍历摄像头所支持的预览尺寸
        mPreviewSizes.clear();
        for (android.util.Size size : map.getOutputSizes(mPreview.getOutputClass())) {
            int width = size.getWidth();
            int height = size.getHeight();
            if (width <= MAX_PREVIEW_WIDTH && height <= MAX_PREVIEW_HEIGHT) {
                mPreviewSizes.add(new Size(width, height));
            }
        }
        //遍历摄像头所支持的拍照尺寸
        mPictureSizes.clear();
        collectPictureSizes(mPictureSizes, map);
        //通过比例进行筛选
        for (AspectRatio ratio : mPreviewSizes.ratios()) {
            if (!mPictureSizes.ratios().contains(ratio)) {
                mPreviewSizes.remove(ratio);
            }
        }

        if (!mPreviewSizes.ratios().contains(mAspectRatio)) {
            mAspectRatio = mPreviewSizes.ratios().iterator().next();
        }
    }

collectCameraInfo()就是通过mCameraCharacteristics来设置相关配置,预览尺寸、拍照尺寸等。

    private void prepareImageReader() {
        if (mImageReader != null) {
            mImageReader.close();
        }
        //这里直接选择适合比例的最大的拍照尺寸
        Size largest = mPictureSizes.sizes(mAspectRatio).last();
        //设置用于拍照数据的ImageReader对象,并设置回调函数
        mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),
                ImageFormat.JPEG, /* maxImages */ 2);
        mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, null);
    }
    
     private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
            = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            try (Image image = reader.acquireNextImage()) {
                Image.Plane[] planes = image.getPlanes();
                if (planes.length > 0) {
                    ByteBuffer buffer = planes[0].getBuffer();
                    byte[] data = new byte[buffer.remaining()];
                    buffer.get(data);
                    //同样,完成拍照后使用回调方法将数据传到外部去处理
                    mCallback.onPictureTaken(data);
                }
            }
        }
    };

prepareImageReader()设置了ImageReader的拍照尺寸和拍照的监听回调接口。

    private void startOpeningCamera() {
        try {
            //打开相机
            mCameraManager.openCamera(mCameraId, mCameraDeviceCallback, null);
        } catch (CameraAccessException e) {
            throw new RuntimeException("Failed to open camera: " + mCameraId, e);
        }
    }
    
    private final CameraDevice.StateCallback mCameraDeviceCallback
            = new CameraDevice.StateCallback() {

        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            mCamera = camera;
            //执行打开相机完成的回调方法
            mCallback.onCameraOpened();
            //开始预览
            startCaptureSession();
        }

        @Override
        public void onClosed(@NonNull CameraDevice camera) {
            //执行关闭相机完成的回调方法
            mCallback.onCameraClosed();
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            mCamera = null;
        }

        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            Log.e(TAG, "onError: " + camera.getId() + " (" + error + ")");
            mCamera = null;
        }

    };

startOpeningCamera()就是打开摄像头并设置监听回调,当摄像头打开完成后开启预览。

    void startCaptureSession() {
        if (!isCameraOpened() || !mPreview.isReady() || mImageReader == null) {
            return;
        }
        //选择最合适的预览尺寸
        Size previewSize = chooseOptimalSize();
        mPreview.setBufferSize(previewSize.getWidth(), previewSize.getHeight());
        Surface surface = mPreview.getSurface();
        try {
            //创建摄像头预览的请求
            mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            //将请求关联预览控件的surface
            mPreviewRequestBuilder.addTarget(surface);
            //发送请求建立会话session的请求并监听回调
            mCamera.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
                    mSessionCallback, null);
        } catch (CameraAccessException e) {
            throw new RuntimeException("Failed to start camera session");
        }
    }
    
    //监听CameraCaptureSession状态的回调接口
    private final Session.StateCallback mSessionCallback
            = new CameraCaptureSession.StateCallback() {

        @Override
        public void onConfigured(@NonNull CameraCaptureSession session) {
            if (mCamera == null) {
                return;
            }
            //获得CameraCaptureSession对象,控制摄像头的预览或者拍照
            mCaptureSession = session;
            updateAutoFocus();
            updateFlash();
            try {
                开启预览并设置监听回调
                mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(),
                        mCaptureCallback, null);
            } catch (CameraAccessException e) {
                Log.e(TAG, "Failed to start camera preview because it couldn't access camera", e);
            } catch (IllegalStateException e) {
                Log.e(TAG, "Failed to start camera preview.", e);
            }
        }

        @Override
        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
            Log.e(TAG, "Failed to configure capture session.");
        }

        @Override
        public void onClosed(@NonNull CameraCaptureSession session) {
            if (mCaptureSession != null && mCaptureSession.equals(session)) {
                mCaptureSession = null;
            }
        }

    };

首先请求建立会话,获得CameraCaptureSession对象后请求开启预览,并设置CaptureCallback回调接口进行监听。

    @Override
    void takePicture() {
        //如果有设置自动对焦,则对焦完成后拍照
        if (mAutoFocus) {
            lockFocus();
        } else {
            //直接拍照
            captureStillPicture();
        }
    }
    
    void captureStillPicture() {
        try {
            //创建捕捉静态图片的请求
            CaptureRequest.Builder captureRequestBuilder = mCamera.createCaptureRequest(
                    CameraDevice.TEMPLATE_STILL_CAPTURE);
            //关联ImageReader的Surface对象
            captureRequestBuilder.addTarget(mImageReader.getSurface());
            //设置预览模式
            captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                    mPreviewRequestBuilder.get(CaptureRequest.CONTROL_AF_MODE));
            
            ...
            
            //设置拍照的格式和方向
            // Calculate JPEG orientation.
            @SuppressWarnings("ConstantConditions")
            int sensorOrientation = mCameraCharacteristics.get(
                    CameraCharacteristics.SENSOR_ORIENTATION);
            captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION,
                    (sensorOrientation +
                            mDisplayOrientation * (mFacing == Constants.FACING_FRONT ? 1 : -1) +
                            360) % 360);
            //停止连续取景
            // Stop preview and capture a still picture.
            mCaptureSession.stopRepeating();
            //拍照
            mCaptureSession.capture(captureRequestBuilder.build(),
                    new CameraCaptureSession.CaptureCallback() {
                        @Override
                        public void onCaptureCompleted(@NonNull CameraCaptureSession session,
                                @NonNull CaptureRequest request,
                                @NonNull TotalCaptureResult result) {
                            //取消对焦
                            unlockFocus();
                        }
                    }, null);
        } catch (CameraAccessException e) {
            Log.e(TAG, "Cannot capture a still picture.", e);
        }
    }

takePicture()方法执行拍照动作,先建立拍照请求,设置相关参数,再发送请求完成拍照,照片数据在前文的ImageReader的监听接口中获得。再看一下stop()方法,这个比较简单:

    @Override
    void stop() {
        if (mCaptureSession != null) {
            //关闭会话
            mCaptureSession.close();
            mCaptureSession = null;
        }
        if (mCamera != null) {
            //关闭摄像头
            mCamera.close();
            mCamera = null;
        }
        if (mImageReader != null) {
            //关闭ImageReader对象
            mImageReader.close();
            mImageReader = null;
        }
    }

至此Camera2的拍照流程解析完成,Camera2采用客户端/服务端式的请求/回调的设计模式,也将上层API和底层硬件层彻底分离与解耦。

总结

这篇文章讲解了Android相机开发中需要注意的问题和它们的解决方案,同时通过分析Google官方的CameraView项目源码,学习了如何管理相机的生命周期和解决版本兼容问题,最后我们又解析了Android的相机开发流程,通过camera和camera2两种API分别实现相机功能。

本文在很大程度上参考了以下资料,特此感谢!

Android相机开发那些坑

Android Camera2 拍照入门学习

Android平台Camera开发实践指南

android camera2 详解说明(一)

android.hardware.camera2 使用指南

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,800评论 25 707
  • 上一篇介绍了如何使用系统相机简单、快速的进行拍照,本篇将介绍如何使用框架提供的API直接控制摄像机硬件。 你还在为...
    Xiao_Mai阅读 7,161评论 4 18
  • Android中开发相机的两种方式Android系统提供了两种使用手机相机资源实现拍摄功能的方法,一种是直接通过I...
    TensorFlow开发者阅读 3,021评论 0 14
  • 一.Android中开发相机应用的两种方式 Android系统提供了两种使用手机相机资源实现拍摄功能的方法,一种是...
    GB_speak阅读 5,797评论 2 25
  • 薛之谦说 他的心愿是世界和平。 一个段子手一个带给别人笑声的人,悲伤起来会让你不知所措。我们做人都希望能带给别人快...
    Aries晴阅读 350评论 0 1