首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Android平台如何采集摄像头数据并实现低延迟RTMP推送

Android平台如何采集摄像头数据并实现低延迟RTMP推送

原创
作者头像
音视频牛哥
发布2024-11-20 16:30:27
发布2024-11-20 16:30:27
8060
举报
文章被收录于专栏:RTMP推送RTMP推送

​技术背景

2015年,我们发布了第一版的Android平台RTMP摄像头|屏幕直播推送模块,几经迭代,功能强大、性能优异,在前些年几乎已经是业内延迟体验和口碑最好的RTMP模块了(毫秒级延迟,低延迟模式下100多毫秒)。鉴于我们侧重于传统行业音视频直播方案,我们从以下几个维度,介绍下Android平台RTMP摄像头采集推送模块的使用场景。

远程监控

  • 家庭监控:将带有摄像头的 Android 设备放置在家中合适的位置,通过 RTMP 推流将摄像头采集到的视频数据传输到远程的服务器或手机端,用户可以随时随地通过网络查看家中的实时情况,保障家庭安全 。
  • 企业监控:在企业办公场所、生产车间等区域安装 Android 摄像头设备,利用 RTMP 推流技术将监控画面实时推送到管理中心或相关负责人的终端设备上,方便企业进行远程管理和监控,及时发现问题并采取措施。

教育领域

  • 在线课堂:教师可以使用 Android 设备的摄像头进行实时授课,将教学过程推流至在线教育平台,学生可以通过网络观看直播课程,实现远程教学。同时,还可以进行互动交流,提高教学效果4.
  • 校园活动直播:学校举办的各种活动,如运动会、文艺演出等,可以通过 Android 设备采集现场画面并进行 RTMP 推流直播,让无法到现场的师生、家长能够实时观看活动盛况,增强校园文化的传播和共享。

医疗健康

  • 远程医疗诊断:医护人员可以使用 Android 设备的摄像头采集患者的病情症状、伤口等画面,通过 RTMP 推流将视频数据传输给远程的医生,医生根据实时画面进行诊断和指导,为患者提供及时的医疗服务,尤其在偏远地区或医疗资源紧张的情况下,具有重要的意义。
  • 健康监测:对于一些需要长期监测健康状况的患者,可以使用配备摄像头的 Android 设备采集相关生理数据的视频信息,如伤口愈合情况、康复训练过程等,并推流至医疗机构的服务器,医护人员可以随时查看患者的恢复情况,调整治疗方案。

媒体与娱乐

  • 新闻报道:记者可以使用 Android 设备在新闻现场快速采集视频素材,并通过 RTMP 推流技术将现场画面实时传输回电视台或新闻网站,实现新闻的快速报道和传播,提高新闻的时效性和现场感。
  • 活动现场直播:在各类大型活动,如演唱会、体育赛事、颁奖典礼等现场,观众可以使用自己的 Android 设备进行拍摄并推流,将现场的精彩瞬间分享到社交媒体或其他直播平台上,让更多的人能够感受到活动的热烈氛围 。

Camera数据采集

权限申请

AndroidManifest.xml 文件中添加必要的权限:

代码语言:java
复制
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

摄像头采集

以Camera2采集为例,大概的实现如下:

代码语言:java
复制
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.Image;
import android.media.ImageReader;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Size;
import android.view.Surface;
import android.view.TextureView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public class Camera2Activity extends AppCompatActivity {

    private TextureView textureView;
    private CameraDevice cameraDevice;
    private CameraCaptureSession cameraCaptureSession;
    private ImageReader imageReader;
    private HandlerThread backgroundThread;
    private Handler backgroundHandler;
    private final String cameraId = "0";
    private Size previewSize;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera2);

        textureView = findViewById(R.id.texture_view);
        textureView.setSurfaceTextureListener(surfaceTextureListener);
    }

    private final TextureView.SurfaceTextureListener surfaceTextureListener = new TextureView.SurfaceTextureListener() {
        @Override
        public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
            openCamera();
        }

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

        }

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

        @Override
        public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {

        }
    };

    private void openCamera() {
        CameraManager cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        try {
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)!= PackageManager.PERMISSION_GRANTED) {
                return;
            }
            CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
            StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
            previewSize = map.getOutputSizes(SurfaceTexture.class)[0];
            cameraManager.openCamera(cameraId, stateCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            cameraDevice = camera;
            createCameraPreviewSession();
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            camera.close();
            cameraDevice = null;
        }

        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            camera.close();
            cameraDevice = null;
        }
    };

    @SuppressLint("MissingPermission")
    private void createCameraPreviewSession() {
        try {
            SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
            surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
            Surface surface = new Surface(surfaceTexture);

            CaptureRequest.Builder captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            captureRequestBuilder.addTarget(surface);

            cameraDevice.createCaptureSession(Arrays.asList(surface), new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                    if (cameraDevice == null) {
                        return;
                    }
                    Camera2Activity.this.cameraCaptureSession = cameraCaptureSession;
                    try {
                        CaptureRequest captureRequest = captureRequestBuilder.build();
                        cameraCaptureSession.setRepeatingRequest(captureRequest, null, backgroundHandler);
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                    }
                }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {

                }
            }, null);

            imageReader = ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2);
            imageReader.setOnImageAvailableListener(imageAvailableListener, backgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private final ImageReader.OnImageAvailableListener imageAvailableListener = new ImageReader.OnImageAvailableListener() {
        @Override
        public void onImageAvailable(ImageReader reader) {
            Image image = reader.acquireNextImage();
            if (image!= null) {
                ByteBuffer buffer = image.getPlanes()[0].getBuffer();
                byte[] data = new byte[buffer.remaining()];
                buffer.get(data);
                // 在这里可以将采集到的图像数据进行处理或直接推送RTMP
                image.close();
            }
        }
    };

    @Override
    protected void onResume() {
        super.onResume();
        startBackgroundThread();
        if (textureView.isAvailable()) {
            openCamera();
        } else {
            textureView.setSurfaceTextureListener(surfaceTextureListener);
        }
    }

    @Override
    protected void onPause() {
        stopCameraPreviewSession();
        closeCamera();
        stopBackgroundThread();
        super.onPause();
    }

    private void stopCameraPreviewSession() {
        if (cameraCaptureSession!= null) {
            try {
                cameraCaptureSession.stopRepeating();
                cameraCaptureSession.close();
                cameraCaptureSession = null;
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }
    }

    private void closeCamera() {
        if (cameraDevice!= null) {
            cameraDevice.close();
            cameraDevice = null;
        }
    }

    private void startBackgroundThread() {
        backgroundThread = new HandlerThread("CameraBackground");
        backgroundThread.start();
        backgroundHandler = new Handler(backgroundThread.getLooper());
    }

    private void stopBackgroundThread() {
        backgroundThread.quitSafely();
        try {
            backgroundThread.join();
            backgroundThread = null;
            backgroundHandler = null;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

如果是CameraX,采集代码实现如下:

代码语言:java
复制
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.ImageFormat;
import android.os.Bundle;
import android.util.Log;
import android.util.Size;
import android.view.Surface;
import android.view.TextureView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;

import com.google.common.util.concurrent.ListenableFuture;

import java.nio.ByteBuffer;
import java.util.concurrent.ExecutionException;

public class CameraXActivity extends AppCompatActivity {

    private TextureView textureView;
    private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camerax);

        textureView = findViewById(R.id.texture_view);

        cameraProviderFuture = ProcessCameraProvider.getInstance(this);
        cameraProviderFuture.addListener(() -> {
            try {
                ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
                startCameraX(cameraProvider);
            } catch (ExecutionException | InterruptedException e) {
                Log.e("CameraXActivity", "Error starting camera", e);
            }
        }, ContextCompat.getMainExecutor(this));
    }

    @SuppressLint("UnsafeExperimentalUsageError")
    private void startCameraX(ProcessCameraProvider cameraProvider) {
        if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA)!= PackageManager.PERMISSION_GRANTED) {
            return;
        }

        Preview preview = new Preview.Builder()
               .build();

        CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;

        ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
               .setTargetResolution(new Size(640, 480))
               .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
               .build();

        imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this), new ImageAnalysis.Analyzer() {
            @Override
            public void analyze(@NonNull ImageProxy image) {
                if (image.getFormat() == ImageFormat.YUV_420_888) {
                    ByteBuffer buffer = image.getPlanes()[0].getBuffer();
                    byte[] data = new byte[buffer.remaining()];
                    buffer.get(data);
                    // 在这里可以将采集到的图像数据进行处理或直接推送RTMP
                    image.close();
                }
            }
        });

        Camera camera = cameraProvider.bindToLifecycle((LifecycleOwner) this, cameraSelector, preview, imageAnalysis);

        preview.setSurfaceProvider(textureView.getSurfaceProvider());
    }
}

RTMP推送模块对接

获取到数据,大牛直播SDK的RTMP推送模块,数据投递接口实现如下:

代码语言:java
复制
@Override
public void onCameraImageData(Image image) {
	Image.Plane[] planes = image.getPlanes();

	int w = image.getWidth(), h = image.getHeight();
	int y_offset = 0, u_offset = 0, v_offset = 0;

	int scale_w = 0, scale_h = 0, scale_filter_mode = 0;
	scale_filter_mode = 3;

	int rotation_degree = cameraImageRotationDegree_;
	if (rotation_degree < 0) {
		Log.i(TAG, "onCameraImageData rotation_degree < 0, may need to set orientation_ to 0, 90, 180 or 270");
		return;
	}

	for (LibPublisherWrapper i : publisher_array_)
		i.PostLayerImageYUV420888ByteBuffer(0, 0, 0,
			planes[0].getBuffer(), y_offset, planes[0].getRowStride(),
			planes[1].getBuffer(), u_offset, planes[1].getRowStride(),
			planes[2].getBuffer(), v_offset, planes[2].getRowStride(), planes[1].getPixelStride(),
			w, h, 0, 0,
			scale_w, scale_h, scale_filter_mode, rotation_degree);

}

封装实现如下:

代码语言:java
复制
/*
 * LibPublisherWrapper.java
 * Created by daniusdk.com
 * WeChat: xinsheng120
 */
public boolean PostLayerImageYUV420888ByteBuffer(int index, int left, int top,
												 ByteBuffer y_plane, int y_offset, int y_row_stride,
												 ByteBuffer u_plane, int u_offset, int u_row_stride,
												 ByteBuffer v_plane, int v_offset, int v_row_stride, int uv_pixel_stride,
												 int width, int height, int is_vertical_flip,  int is_horizontal_flip,
												 int scale_width,  int scale_height, int scale_filter_mode,
												 int rotation_degree) {
	if (!check_native_handle())
		return false;

	if (!read_lock_.tryLock())
		return false;

	try {
		if (!check_native_handle())
			return false;

		return OK == lib_publisher_.PostLayerImageYUV420888ByteBuffer(get(), index, left, top, y_plane, y_offset, y_row_stride,
		 u_plane, u_offset, u_row_stride, v_plane, v_offset, v_row_stride, uv_pixel_stride,
		 width, height, is_vertical_flip, is_horizontal_flip, scale_width, scale_height, scale_filter_mode, rotation_degree);

	} catch (Exception e) {
		Log.e(TAG, "PostLayerImageYUV420888ByteBuffer Exception:", e);
		return false;
	} finally {
		read_lock_.unlock();
	}
}

以Camera2采集对接为例,Camera2RtmpPusherActivity.java调用RTMP推送端参数设置如下:

代码语言:java
复制
/*
 * Camera2RtmpPusherActivity.java
 * Created by daniusdk.com
 * WeChat: xinsheng120
 */
private boolean initialize_publisher(SmartPublisherJniV2 lib_publisher, long handle, int width, int height, int fps, int gop) {
	if (null == lib_publisher) {
		Log.e(TAG, "initialize_publisher lib_publisher is null");
		return false;
	}

	if (0 == handle) {
		Log.e(TAG, "initialize_publisher handle is 0");
		return false;
	}

	if (videoEncodeType == 1) {
		int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, true);
		Log.i(TAG, "h264HWKbps: " + kbps);
		int isSupportH264HWEncoder = lib_publisher.SetSmartPublisherVideoHWEncoder(handle, kbps);
		if (isSupportH264HWEncoder == 0) {
			lib_publisher.SetNativeMediaNDK(handle, 0);
			lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBR
			lib_publisher.SetVideoHWEncoderQuality(handle, 39);
			lib_publisher.SetAVCHWEncoderProfile(handle, 0x08); // 0x01: Baseline, 0x02: Main, 0x08: High
			lib_publisher.SetAVCHWEncoderLevel(handle, 0x1000); // Level 4.1 多数情况下,这个够用了

			Log.i(TAG, "Great, it supports h.264 hardware encoder!");
		}
	} else if (videoEncodeType == 2) {
		int kbps = LibPublisherWrapper.estimate_video_hardware_kbps(width, height, fps, false);
		Log.i(TAG, "hevcHWKbps: " + kbps);
		int isSupportHevcHWEncoder = lib_publisher.SetSmartPublisherVideoHevcHWEncoder(handle, kbps);
		if (isSupportHevcHWEncoder == 0) {
			lib_publisher.SetNativeMediaNDK(handle, 0);
			lib_publisher.SetVideoHWEncoderBitrateMode(handle, 1); // 0:CQ, 1:VBR, 2:CBR
			lib_publisher.SetVideoHWEncoderQuality(handle, 39);
			
			Log.i(TAG, "Great, it supports hevc hardware encoder!");
		}
	}

	boolean is_sw_vbr_mode = true;
	//H.264 software encoder
	if (is_sw_vbr_mode) {
		int is_enable_vbr = 1;
		int video_quality = LibPublisherWrapper.estimate_video_software_quality(width, height, true);
		int vbr_max_kbps = LibPublisherWrapper.estimate_video_vbr_max_kbps(width, height, fps);
		lib_publisher.SmartPublisherSetSwVBRMode(handle, is_enable_vbr, video_quality, vbr_max_kbps);
	}

	if (is_pcma_) {
		lib_publisher.SmartPublisherSetAudioCodecType(handle, 3);
	} else {
		lib_publisher.SmartPublisherSetAudioCodecType(handle, 1);
	}

	lib_publisher.SetSmartPublisherEventCallbackV2(handle, new EventHandlerPublisherV2().set(handler_, record_executor_));

	lib_publisher.SmartPublisherSetSWVideoEncoderProfile(handle, 3);

	lib_publisher.SmartPublisherSetSWVideoEncoderSpeed(handle, 2);

	lib_publisher.SmartPublisherSetGopInterval(handle, gop);

	lib_publisher.SmartPublisherSetFPS(handle, fps);

	// lib_publisher.SmartPublisherSetSWVideoBitRate(handle, 600, 1200);

	boolean is_noise_suppression = true;
	lib_publisher.SmartPublisherSetNoiseSuppression(handle, is_noise_suppression ? 1 : 0);

	boolean is_agc = false;
	lib_publisher.SmartPublisherSetAGC(handle, is_agc ? 1 : 0);

	int echo_cancel_delay = 0;
	lib_publisher.SmartPublisherSetEchoCancellation(handle, 1, echo_cancel_delay);

	return true;
}

private void InitAndSetConfig() {
	if (null == libPublisher)
		return;

	if (!stream_publisher_.empty())
		return;

	Log.i(TAG, "InitAndSetConfig video width: " + video_width_ + ", height" + video_height_ + " imageRotationDegree:" + cameraImageRotationDegree_);

	int audio_opt = 1;
	long handle = libPublisher.SmartPublisherOpen(context_, audio_opt, 3,  video_width_, video_height_);
	if (0==handle) {
		Log.e(TAG, "sdk open failed!");
		return;
	}

	Log.i(TAG, "publisherHandle=" + handle);

	int fps = 25;
	int gop = fps * 3;

	initialize_publisher(libPublisher, handle, video_width_, video_height_, fps, gop);

	stream_publisher_.set(libPublisher, handle);
}

总结

从上文得知,目前我们做摄像头采集,已经主推Camera2,为什么要用Camera2呢?Camera2的采集优势在哪里呢?

功能更强大

  • 更精细的控制:Camera2 提供了对摄像头更底层、更精细的控制能力。它允许开发者直接控制曝光时间、感光度、对焦距离等参数,从而能够根据不同的拍摄场景和需求,实现更专业、更个性化的拍摄效果。例如,在拍摄夜景时,可以通过手动设置较长的曝光时间来获取更明亮清晰的画面;在拍摄运动物体时,可以调整对焦模式和参数,以确保快速准确地对焦。
  • 支持更多高级功能:支持 RAW 格式图像捕捉,这对于专业摄影和需要后期处理的场景非常有用,因为 RAW 格式保留了更多的图像细节和原始数据,能够提供更高的图像质量和更大的后期处理空间。此外,还支持多摄像头同时使用、3D 拍摄等高级功能,为开发者提供了更多的创作可能性。

性能更优

  • 更高的帧率和更低的延迟:Camera2 API 在处理图像数据时具有更高的效率,能够支持更高的帧率采集,从而可以实现更流畅的视频录制和实时预览。同时,它的延迟也相对较低,使得拍摄的画面能够更及时地显示在屏幕上,对于需要实时反馈的场景,如视频通话、直播等非常关键,可以提供更好的用户体验。
  • 更好的资源管理:能够更有效地管理摄像头资源,避免资源的浪费和冲突。它可以根据设备的硬件性能和当前的使用情况,自动调整资源分配,确保摄像头的稳定运行,并且在多个应用同时使用摄像头时,能够更好地协调资源,避免出现卡顿或崩溃等问题。

兼容性更好

  • 统一的接口:Camera2 为不同厂商的摄像头硬件提供了一个统一的编程接口,使得开发者可以使用相同的代码来操作不同设备上的摄像头,大大降低了开发的难度和工作量。无论设备的摄像头硬件是何种型号或品牌,只要其支持 Camera2 API,开发者就可以按照标准的接口进行开发,无需为每个设备单独编写适配代码。
  • 向后兼容性:虽然 Camera2 是在 Android 5.0(API 21)及以上版本引入的,但它在设计上考虑了向后兼容性。在较新的 Android 版本中,Camera2 不断得到优化和完善,同时也能够在一定程度上兼容旧版本的特性和功能,使得开发者可以在不同版本的 Android 设备上使用相对统一的开发方式,提高了应用的兼容性和可维护性。

灵活性更高

  • 可定制的处理流程:Camera2 允许开发者自定义图像数据的处理流程,开发者可以在图像数据从摄像头采集到最终显示或存储的过程中,插入自己的处理逻辑,如添加滤镜、进行图像识别、实时分析等。这种灵活性使得开发者能够根据具体的应用需求,对图像数据进行各种定制化的处理,为用户提供更丰富多样的功能和体验。
  • 与其他 Android 技术的集成:与 Android 系统的其他技术和框架,如 OpenGL ES、MediaCodec 等具有更好的集成性。开发者可以方便地将摄像头采集到的图像数据与图形渲染、视频编码等功能相结合,实现更复杂的应用场景,如实时视频特效、视频直播推流等。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ​技术背景
    • 远程监控
    • 教育领域
    • 医疗健康
    • 媒体与娱乐
  • Camera数据采集
    • 权限申请
    • 摄像头采集
    • RTMP推送模块对接
  • 总结
    • 功能更强大
    • 性能更优
    • 兼容性更好
    • 灵活性更高
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档