标签: android

  • Android 12的启动白屏简单适配方案

    TL;DR

    1. 导入implementation 'androidx.core:core-splashscreen:1.0.1'
    2. 继承主题 Theme.ScreenSplash,继承的主题可以设置前景和背景以及时间(最大500),类似icon
    3. 使用主题

    在国产大厂的APP上,每一个Feature,每一个模块,每一个按钮,甚至每一个不可交互的地方,它都有出生的意义:广告位。

    闪屏页的战略地位不言而喻😄

    在 Android 12(SDK 31)之前,应用的启动画面(Splash Screen)一直是个令人头疼的问题。开发者们不得不绞尽脑汁,通过自定义 Activity 主题、设置 windowBackground 或者创建一个独立的 Splash Activity 来模拟启动效果。

    不仅需要作为天然的广告位,这个一闪而过页面承载太多东西了。你的所有需要注册的内容,所有内容的完整性检查,服务器判断,DNS解析。当然还有广告的加载以及用户的手有没有抖动…

    虽然目前看来体验不尽人意,适配寥寥无几,不过当初Android12似乎真的想改变这些(当然不止这些,更多的可能是体验统一的问题),所以带来一个原生,更高效的解决方案。抛开这些开发决定不了的内容,下面我简单记录一下自己适配这个SplashScreen,当然这也是我第一次使用这个特性。

    核心概念与 Theme.SplashScreen 属性详解

    Android 12 的启动画面不再是应用自身绘制的 View,而是由系统根据主题配置生成并管理。你的启动 Activity 会首先应用一个继承自 Theme.SplashScreen 的主题,系统会根据该主题的属性来渲染启动画面,并在应用准备就绪后平滑过渡到应用真正的界面。下面是我在网上找到的VerseAPP的动画闪屏页(你也可以下载查看teelgram的动画效果):

    以下是 Theme.SplashScreen 主题及其关键属性的详细解析:

    1. 父主题:Theme.SplashScreen

    • 作用: 这是 Android 12+ 系统提供的启动画面基础主题。你的自定义启动主题必须继承这个主题,才能享受到系统级别的启动画面管理和动画效果。

    2. 背景属性:windowSplashScreenBackground

    类型: Drawable 引用(@drawable/@color/

    作用: 定义启动画面的背景。这是替换传统“白屏”的关键。

    自定义方案:

    • 纯色: 最简单也是最高效的方式,直接引用一个颜色资源,如 <item name="windowSplashScreenBackground">@color/your_brand_color</item>
    • 渐变色: 创建一个 shape Drawable,并在其中定义 gradient 标签,实现平滑的颜色过渡。
    • 图片作为背景(慎用,推荐 LayerList): 虽然可以直接引用一张图片 (@drawable/your_image),但通常不推荐直接将一张大图作为背景,因为它可能导致屏幕适配问题、文件尺寸增大和内存开销。
    • LayerList 组合背景

    3. 图标属性:windowSplashScreenAnimatedIcon

    • 类型: Drawable 引用(@drawable/@mipmap/
    • 作用: 定义在启动画面中心显示的图标或动画。
    • 可选动画类型:
      • Animated Vector Drawable (AVD)
        • 格式: XML (.xml),定义了矢量图的动画。
        • 优点: 官方推荐,文件小巧,可缩放不失真,性能优秀,可以实现复杂的矢量动画。
        • 创建: 复杂动画通常需要手动编写 XML 或借助工具。
      • Animation Drawable:
        • 格式: XML (.xml),引用一系列帧图片。
        • 优点: 简单易懂,适合简单的帧动画。
        • 缺点: 每一帧都是一张图片,文件体积大,内存占用高,缩放可能失真。适用于帧数非常少且简单的动画。
      • 自适应图标 (Adaptive Icon):
        • 格式: 通常是矢量图,由前景层 (@mipmap/ic_launcher_foreground) 和背景层 (@mipmap/ic_launcher_background) 组成。
        • 优点: 系统原生支持,自动适应不同形状的图标蒙版,无需额外动画即可平滑缩放。
        • 最常用方案: 如果没有自定义动画需求,直接使用你的自适应图标前景层即可,例如:<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_foreground</item>

    4. 动画持续时间:windowSplashScreenAnimationDuration

    • 类型: 整数(毫秒)
    • 作用: 定义 windowSplashScreenAnimatedIcon 中图标动画的持续时间。系统会在这个时间结束后开始淡出启动画面。
    • 注意: 你的动画设计应尽量在这个持续时间内完成,以避免动画被截断或过早结束。建议值为 200ms 到 1000ms 之间,保持快速且流畅。

    5. 品牌 Logo:windowSplashScreenBrandDrawable (可选)

    • 类型: Drawable 引用
    • 作用: 在启动画面底部显示一个可选的品牌 Logo。
    • 位置: 这个 Logo 会固定显示在启动画面的底部,不会随图标一起动画。
    • 用处: 适合展示公司或产品的额外品牌标识。

    6. 核心属性:postSplashScreenTheme

    • 类型: Style 引用(@style/
    • 作用: 这是最重要的属性!它指定了启动画面结束后,Activity 应该切换到哪个主题来渲染你的应用界面。
    • 重要性: 如果没有正确设置这个属性,你的 Activity 在启动画面消失后,可能会显示错误的样式,甚至出现界面空白或闪烁。务必将其指向你应用正常运行时所使用的主要主题。

    二、应用开发实践:从零开始配置

    让我们通过一个完整的示例,一步步将 Android 12 的启动画面集成到你的应用中。

    1: 确保你的项目兼容 Android 12 (SDK 31+)

    首先,在你的 build.gradle (Module: app) 文件中,确保 compileSdktargetSdk 至少是 31:

    android {
        compileSdk 31 // 或更高版本
        defaultConfig {
            targetSdk 31 // 或更高版本
            // ...
        }
        // ...
    }

    2. 添加 Splash Screen 库依赖

    dependencies {
        // ... 其他依赖
        implementation 'androidx.core:core-splashscreen:1.0.1' 
    }

    3. 定义你的应用主题

    res/values/themes.xml (和 res/values-night/themes.xml,用于深色模式) 中定义应用主主题。这将是 postSplashScreenTheme 指向的主题。

    <resources>
        <style name="Theme.MyAwesomeApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
            <item name="colorPrimary">#6200EE</item>
            <item name="colorPrimaryVariant">#3700B3</item>
            <item name="colorOnPrimary">#FFFFFF</item>
            <item name="colorSecondary">#03DAC6</item>
            <item name="colorSecondaryVariant">#018786</item>
            <item name="colorOnSecondary">#000000</item>
            <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
            <item name="android:navigationBarColor">@color/black</item>
            <item name="android:windowBackground">@color/white</item>
            </style>
    </resources>
    
    <resources>
        <style name="Theme.MyAwesomeApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
            <item name="colorPrimary">#BB86FC</item>
            <item name="colorPrimaryVariant">#3700B3</item>
            <item name="colorOnPrimary">#000000</item>
            <item name="colorSecondary">#03DAC6</item>
            <item name="colorSecondaryVariant">#03DAC6</item>
            <item name="colorOnSecondary">#000000</item>
            <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
            <item name="android:navigationBarColor">@color/black</item>
            <item name="android:windowBackground">@color/dark_gray</item>
        </style>
    </resources>
    

    4. 准备启动画面背景(渐变色和Logo)

    我们创建一个 LayerList 来实现一个包含渐变背景和底部品牌Logo的启动画面。(实际上不推荐 用渐变色,我更推荐telegram或者X的方案,纯色背景+AVD/静态LOGO)

    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)">
        <item>
            <shape android:shape="rectangle">
                <gradient
                    android:angle="270"
                    android:startColor="#6200EE"
                    android:endColor="#03DAC6"
                    android:type="linear" />
            </shape>
        </item>
    
        <item android:bottom="32dp"> <bitmap
                android:src="@drawable/ic_my_company_logo"
                android:gravity="bottom|center_horizontal" />
        </item>
    </layer-list>
    

    注意:

    • @drawable/ic_my_company_logo 应该是你的 Logo 图片(Vector Drawable 或 Bitmap)。
    • android:bottom 可以调整 Logo 的位置。
    • android:gravity="bottom|center_horizontal" 将 Logo 放置在底部中央。

    5. 准备启动画面动画图标(AVD 示例)

    假设你已经有一个名为 ic_animated_logo.xml 的 Animated Vector Drawable。

    <?xml version="1.0" encoding="utf-8"?>
    <animated-vector xmlns:android="[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"
        android:drawable="@drawable/ic_static_logo"> <target android:name="group_name_in_static_logo"> <propertyValuesHolder
                android:propertyName="rotation"
                android:valueFrom="0"
                android:valueTo="360"
                android:valueType="floatType"
                android:duration="1000"
                android:interpolator="@android:interpolator/fast_out_slow_in" />
        </target>
    </animated-vector>
    

    如果不想使用自定义动画,直接使用自适应图标前景部分:@mipmap/ic_launcher_foreground

    6. 定义启动画面主题

    现在,在 res/values/themes.xml (和 res/values-night/themes.xml) 中定义你的启动主题,并引用我们准备好的 Drawable。

    <resources>
        <style name="Theme.MyAwesomeApp.SplashScreen" parent="Theme.SplashScreen">
            <item name="windowSplashScreenBackground">@drawable/splash_layer_bg</item>
    
            <item name="windowSplashScreenAnimatedIcon">@drawable/ic_animated_logo</item>
            <item name="windowSplashScreenAnimationDuration">1000</item> <item name="postSplashScreenTheme">@style/Theme.MyAwesomeApp</item>
        </style>
    </resources>
    
    <resources>
        <style name="Theme.MyAwesomeApp.SplashScreen" parent="Theme.SplashScreen">
            <item name="windowSplashScreenBackground">@color/dark_splash_bg_color</item>
            <item name="windowSplashScreenAnimatedIcon">@drawable/ic_animated_logo_dark</item>
            <item name="windowSplashScreenAnimationDuration">1000</item>
            <item name="postSplashScreenTheme">@style/Theme.MyAwesomeApp</item>
        </style>
    </resources>
    

    7. 在 AndroidManifest.xml 中应用主题

    AndroidManifest.xml 中,将 Theme.MyAwesomeApp.SplashScreen 主题应用到启动 Activity(通常是 MainActivity,也有可能是SplashActivity之类的)。

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="[http://schemas.android.com/apk/res/android](http://schemas.android.com/apk/res/android)"
        xmlns:tools="[http://schemas.android.com/tools](http://schemas.android.com/tools)">
    
        <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/Theme.MyAwesomeApp"> <activity
                android:name=".MainActivity"
                android:exported="true"
                android:theme="@style/Theme.MyAwesomeApp.SplashScreen"> <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    
            <activity android:name=".OtherActivity"
                android:theme="@style/Theme.MyAwesomeApp"/>
    
        </application>
    </manifest>
    

    8. 在 MainActivity 中安装启动画面

    最后,在启动 Activity 的 onCreate() 方法中调用 installSplashScreen()这是启用系统启动画面 API 的核心步骤。

    // MainActivity.kt
    import android.os.Bundle
    import androidx.appcompat.app.AppCompatActivity
    import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
    import kotlinx.coroutines.Dispatchers
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.withContext
    import androidx.lifecycle.lifecycleScope
    import kotlinx.coroutines.launch
    
    class MainActivity : AppCompatActivity() {
    
        private var isContentReady = false // 用于控制启动画面何时消失
    
        override fun onCreate(savedInstanceState: Bundle?) {
            // 1. 调用 installSplashScreen() 必须在 super.onCreate() 之前
            val splashScreen = installSplashScreen()
    
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            // 2. 延迟启动画面消失,直到内容准备就绪
            splashScreen.setKeepOnScreenCondition {
                !isContentReady // 当 isContentReady 为 false 时,启动画面会一直显示
            }
    
            // 3. 模拟数据加载或应用初始化
            loadAppContent()
        }
    
        private fun loadAppContent() {
            lifecycleScope.launch(Dispatchers.IO) {
                // 模拟耗时的初始化操作,例如网络请求、数据库加载等
                delay(3000) // 模拟 3 秒的加载时间
    
                // 数据加载完成
                isContentReady = true
    
                withContext(Dispatchers.Main) {
                    // 在这里可以执行数据加载完成后的 UI 更新或跳转操作
                    // 例如:navigateToHome()
                }
            }
        }
    }
    

    Java 示例(AI转换的代码,应该咩问题):

    // MainActivity.java
    import android.os.Bundle;
    import androidx.appcompat.app.AppCompatActivity;
    import androidx.core.splashscreen.SplashScreen;
    import android.os.Handler;
    import android.os.Looper;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    public class MainActivity extends AppCompatActivity {
    
        private boolean isContentReady = false; // 用于控制启动画面何时消失
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            // 1. 调用 installSplashScreen() 必须在 super.onCreate() 之前
            SplashScreen splashScreen = SplashScreen.Companion.installSplashScreen(this);
    
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            // 2. 延迟启动画面消失,直到内容准备就绪
            splashScreen.setKeepOnScreenCondition(() -> !isContentReady);
    
            // 3. 模拟数据加载或应用初始化
            loadAppContent();
        }
    
        private void loadAppContent() {
            ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
            executor.schedule(() -> {
                // 模拟耗时的初始化操作
                isContentReady = true; // 数据加载完成
    
                new Handler(Looper.getMainLooper()).post(() -> {
                    // 在主线程执行数据加载完成后的 UI 更新或跳转操作
                });
            }, 3, TimeUnit.SECONDS); // 模拟 3 秒的加载时间
        }
    }
  • Android实时通话技术解析

    TL;DR

    实时语音传输的核心在于持续流式处理,我们通过一个完整的代码示例来揭示其工作原理:

    1. 音频分片机制:

    // 音频采集线程
    class AudioCaptureThread extends Thread {
        private static final int SAMPLE_RATE = 48000; // 48kHz采样率
        private static final int FRAME_DURATION = 20; // 20ms帧间隔
        private static final int FRAME_SIZE = (SAMPLE_RATE * FRAME_DURATION) / 1000; // 960采样点
    
        @Override
        public void run() {
            AudioRecord recorder = createAudioRecord();
            ByteBuffer buffer = ByteBuffer.allocateDirect(FRAME_SIZE * 2); // 16bit采样
            
            recorder.startRecording();
            while (isRunning) {
                // 读取20ms的PCM数据
                int readBytes = recorder.read(buffer, FRAME_SIZE * 2);
                
                // 添加RTP头部(时间戳+序号)
                RtpPacket packet = new RtpPacket();
                packet.timestamp = SystemClock.elapsedRealtimeNanos() / 1000;
                packet.sequence = nextSequenceNumber();
                packet.payload = buffer.array();
                
                // 立即发送数据包
                networkSender.send(packet);
                
                buffer.rewind(); // 重用缓冲区
            }
        }
        
        private AudioRecord createAudioRecord() {
            return new AudioRecord.Builder()
                .setAudioFormat(new AudioFormat.Builder()
                    .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                    .setSampleRate(SAMPLE_RATE)
                    .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
                    .build())
                .setBufferSizeInBytes(FRAME_SIZE * 4) // 双缓冲
                .build();
        }
    }

    关键原理说明:

    • 时间戳精度:采用微秒级时间戳(elapsedRealtimeNanos()/1000)确保时序精度
    • 环形缓冲区:DirectByteBuffer 重用避免内存抖动
    • 实时发送:每个 20ms 数据包立即发送,无需等待前序确认

    2. 实时播放机制

    class AudioPlaybackThread extends Thread {
        private static final int JITTER_BUFFER_DEPTH = 5; // 100ms缓冲深度
        private final PriorityBlockingQueue<RtpPacket> buffer = 
            new PriorityBlockingQueue<>(50, Comparator.comparingLong(p -> p.timestamp));
        
        private AudioTrack audioTrack;
        private long lastPlayedTimestamp = 0;
    
        @Override
        public void run() {
            audioTrack = createAudioTrack();
            audioTrack.play();
            
            while (isRunning) {
                RtpPacket packet = waitForNextPacket();
                writeToAudioTrack(packet);
                updateTimeline(packet);
            }
        }
        
        private RtpPacket waitForNextPacket() {
            if (buffer.size() < JITTER_BUFFER_DEPTH) {
                // 缓冲不足时插入静音包
                return generateSilencePacket();
            }
            
            return buffer.poll(20, TimeUnit.MILLISECONDS); // 阻塞等待
        }
        
        private void writeToAudioTrack(RtpPacket packet) {
            // 抖动补偿计算
            long expectedTimestamp = lastPlayedTimestamp + 20000; // 20ms间隔
            long timestampDelta = packet.timestamp - expectedTimestamp;
            
            if (timestampDelta > 50000) { // 超过50ms延迟
                resetPlayback(); // 重置时间线
            }
            
            audioTrack.write(packet.payload, 0, packet.payload.length);
            lastPlayedTimestamp = packet.timestamp;
        }
        
        private AudioTrack createAudioTrack() {
            return new AudioTrack.Builder()
                .setAudioFormat(new AudioFormat.Builder()
                    .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                    .setSampleRate(SAMPLE_RATE)
                    .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
                    .build())
                .setBufferSizeInBytes(FRAME_SIZE * 4)
                .setTransferMode(AudioTrack.MODE_STREAM)
                .build();
        }
    }

    核心算法解析:

    • 自适应抖动缓冲:根据网络状况动态调整缓冲深度
    • 时间线同步:通过时间戳差值检测网络延迟突变
    • 静音补偿:在丢包时生成舒适噪声保持播放连续性

    开始

    在做之前我完全没考虑过网络通话是如何实现。就是如何做到你在说话的同时,对方一直可以听到的。我的意思是,你说了一句很长的话,对方不是你说完才听到的,是你一直在说,对方那边一直播放。

    上面的TL;DR部分几乎可以解答我所有的疑惑了。不过要实现类似微信语音通话的实时对话功能,需要深入理解音视频流式处理的完整技术链。本文将重点从Android客户端的视角,剖析实时语音通话的核心技术实现。本文结束后,有包括上面的代码在内的完整的示例代码,有需要都可以自取。


    一、实时通话核心技术原理

    1.1 流式处理 vs 文件传输

    graph LR
        A[麦克风持续采集] --> B[20ms数据分块]
        B --> C[即时编码]
        C --> D[网络实时发送]
        D --> E[接收端缓冲]
        E --> F[持续解码播放]

    与传统文件传输不同,实时通话采用流水线式处理:

    • 时间切片:音频按20ms为单位切割处理
    • 无等待传输:每个数据包独立发送,无需等待整段语音
    • 并行处理:采集、编码、传输、解码、播放同时进行

    1.2 关键性能指标

    指标 目标值 实现要点
    端到端延迟 <400ms 编解码优化/Jitter Buffer控制
    音频采样率 48kHz 硬件加速支持
    抗丢包能力 5%-20% FEC/Opus冗余
    CPU占用率 <15% MediaCodec硬件编码

    二、Android客户端实现详解

    2.1 音频采集模块

    // 低延迟音频采集配置
    private void setupAudioRecorder() {
        int sampleRate = 48000; // 优先选择硬件支持的采样率
        int channelConfig = AudioFormat.CHANNEL_IN_MONO;
        int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
        
        int bufferSize = AudioRecord.getMinBufferSize(sampleRate, 
                            channelConfig, audioFormat);
        
        AudioRecord recorder = new AudioRecord(
            MediaRecorder.AudioSource.VOICE_COMMUNICATION, // 专用通信模式
            sampleRate,
            channelConfig,
            audioFormat,
            bufferSize * 2); // 双缓冲避免溢出
            
        // 环形缓冲区实现
        audioThread = new Thread(() -> {
            byte[] buffer = new byte[960]; // 20ms数据量:48000Hz * 20ms * 16bit / 8 = 1920字节
            while (isRecording) {
                int readBytes = recorder.read(buffer, 0, buffer.length);
                if (readBytes > 0) {
                    encoderQueue.offer(buffer.clone()); // 提交编码队列
                }
            }
        });
    }

    关键参数选择依据:

    • VOICE_COMMUNICATION :启用回声消除硬件加速
    • 48kHz采样率:平衡音质与延迟
    • 20ms帧长:Opus编码标准推荐值

    2.2 音频编码与传输

    // 硬件编码器初始化
    MediaFormat format = MediaFormat.createAudioFormat(
            MediaFormat.MIMETYPE_AUDIO_OPUS, 48000, 1);
    format.setInteger(MediaFormat.KEY_BIT_RATE, 24000);
    format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 960);
    
    MediaCodec encoder = MediaCodec.createEncoderByType("audio/opus");
    encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
    encoder.start();
    
    // 编码循环
    while (!encoderQueue.isEmpty()) {
        int inputIndex = encoder.dequeueInputBuffer(10000);
        if (inputIndex >= 0) {
            ByteBuffer inputBuffer = encoder.getInputBuffer(inputIndex);
            byte[] rawData = encoderQueue.poll();
            inputBuffer.put(rawData);
            encoder.queueInputBuffer(inputIndex, 0, rawData.length, 
                                    System.nanoTime()/1000, 0);
        }
        
        MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        int outputIndex = encoder.dequeueOutputBuffer(bufferInfo, 10000);
        if (outputIndex >= 0) {
            ByteBuffer encodedData = encoder.getOutputBuffer(outputIndex);
            sendToNetwork(encodedData); // 网络发送
            encoder.releaseOutputBuffer(outputIndex, false);
        }
    }

    编码优化技巧:

    • 使用MediaCodec.CONFIGURE_FLAG_ENCODE开启硬件编码
    • 设置KEY_MAX_INPUT_SIZE防止缓冲区溢出
    • 时间戳使用微秒单位(System.nanoTime()/1000)

    2.3 网络传输层

    UDP封包结构示例

    +--------+--------+--------+-------------------+
    | RTP头  | 时间戳 | 序列号 | Opus载荷(20ms数据)|
    +--------+--------+--------+-------------------+
    | 12字节 | 4字节  | 2字节  | 可变长度           |
    +--------+--------+--------+-------------------+

    NAT穿透实现

    // STUN协议实现示例
    public InetSocketAddress discoverNAT() throws IOException {
        DatagramSocket socket = new DatagramSocket();
        byte[] stunRequest = createStunBindingRequest();
        
        // 发送到公共STUN服务器
        socket.send(new DatagramPacket(stunRequest, stunRequest.length, 
                     InetAddress.getByName("stun.l.google.com"), 19302));
                     
        // 接收响应
        byte[] buffer = new byte[1024];
        DatagramPacket response = new DatagramPacket(buffer, buffer.length);
        socket.receive(response);
        
        // 解析XOR-MAPPED-ADDRESS
        return parseStunResponse(response.getData());
    }

    2.4 接收端播放实现

    Jitter Buffer设计

    class JitterBuffer {
        private static final int MAX_BUFFER_SIZE = 10; // 存储200ms数据
        private PriorityQueue<AudioPacket> buffer = 
            new PriorityQueue<>(Comparator.comparingInt(p -> p.sequence));
        private int lastPlayedSeq = -1;
    
        public void addPacket(AudioPacket packet) {
            if (packet.sequence > lastPlayedSeq) {
                buffer.offer(packet);
                // 缓冲区溢出处理
                if (buffer.size() > MAX_BUFFER_SIZE) {
                    buffer.poll(); // 丢弃最旧数据包
                }
            }
        }
    
        public AudioPacket getNextPacket() {
            if (!buffer.isEmpty() && 
                buffer.peek().sequence == lastPlayedSeq + 1) {
                lastPlayedSeq++;
                return buffer.poll();
            }
            return null;
        }
    }

    低延迟播放配置

    AudioTrack audioTrack = new AudioTrack.Builder()
        .setAudioAttributes(new AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
            .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
            .build())
        .setAudioFormat(new AudioFormat.Builder()
            .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
            .setSampleRate(48000)
            .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
            .build())
        .setBufferSizeInBytes(960 * 2) // 双缓冲
        .setTransferMode(AudioTrack.MODE_STREAM)
        .build();
    
    audioTrack.play();
    
    // 播放线程
    while (isPlaying) {
        AudioPacket packet = jitterBuffer.getNextPacket();
        if (packet != null) {
            audioTrack.write(packet.data, 0, packet.data.length);
        } else {
            generateComfortNoise(); // 生成舒适噪声
        }
    }

    三、服务端关键技术方案

    3.1 信令服务器设计

    // Protobuf消息定义
    message SignalMessage {
        enum Type {
            OFFER = 0;
            ANSWER = 1;
            ICE_CANDIDATE = 2;
        }
        
        Type type = 1;
        string sdp = 2;
        repeated string iceCandidates = 3;
    }

    核心功能:

    • WebSocket长连接管理
    • SDP交换协调
    • ICE候选收集与转发

    3.2 TURN中继服务器

    客户端A ↔ TURN Server ↔ 客户端B
               ↓
               当P2P不通时启用中继

    四、性能优化实践

    4.1 延迟优化矩阵

    优化方向 具体措施 效果预估
    采集延迟 使用AudioRecord的READ_NON_BLOCKING模式 减少2-5ms
    编码延迟 启用MediaCodec异步模式 减少3-8ms
    网络传输 开启UDP QoS标记(DSCP 46) 减少10-50ms
    播放缓冲 动态调整Jitter Buffer深度 减少20-100ms

    4.2 功耗控制策略

    // 通话中唤醒锁管理
    PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
    WakeLock wakeLock = pm.newWakeLock(
        PowerManager.PARTIAL_WAKE_LOCK, "MyApp:VoiceCall");
    wakeLock.acquire(10*60*1000L /*10分钟*/);
    
    // 根据网络状态调整编码参数
    if (isNetworkPoor) {
        encoder.setVideoBitrate(1000000); // 降低码率
        adjustFrameRate(15); 
    }

    五、调试与监控

    5.1 WebRTC统计接口

    peerConnection.getStats(new StatsObserver() {
        @Override
        public void onComplete(StatsReport[] reports) {
            for (StatsReport report : reports) {
                if (report.type.equals("ssrc")) {
                    // 获取音频流统计信息
                    Log.d("Stats", "丢包率: " + report.values.get("packetsLost"));
                }
            }
        }
    });

    5.2 关键日志标记

    # 采集延迟
    D/AudioRecorder: Frame captured (seq=325, ts=158746532)
    
    # 网络事件
    W/Network: Packet lost detected, seq=1234, enabling FEC
    
    # 播放状态
    I/AudioTrack: Buffer underrun, inserting 20ms comfort noise

    六、总结

    实现高质量的实时语音通话需要Android开发者深入掌握以下核心技术:

    • 低延迟音频流水线:从采集到播放的端到端优化
    • 自适应网络传输:UDP+前向纠错的平衡艺术
    • 时钟同步机制:RTP时间戳与本地播放的精准对齐

    未来演进方向:

    • 基于AI的网络预测(BWE 2.0)
    • 端侧神经网络降噪(RNNoise)
    • 5G网络下的超低延迟优化(<100ms)

    建议进一步研究:

    掌握这些核心技术后,开发者可以构建出媲美商业级应用的实时通信系统。希望本文能为各位Android开发者在实时音视频领域提供有价值的参考。


    剖析部分至此结束,下面是实例部分

    一、音频流式传输原理剖析

    1. 音频分片机制

    // 音频采集线程
    class AudioCaptureThread extends Thread {
        private static final int SAMPLE_RATE = 48000; // 48kHz采样率
        private static final int FRAME_DURATION = 20; // 20ms帧间隔
        private static final int FRAME_SIZE = (SAMPLE_RATE * FRAME_DURATION) / 1000; // 960采样点
    
        @Override
        public void run() {
            AudioRecord recorder = createAudioRecord();
            ByteBuffer buffer = ByteBuffer.allocateDirect(FRAME_SIZE * 2); // 16bit采样
            
            recorder.startRecording();
            while (isRunning) {
                // 读取20ms的PCM数据
                int readBytes = recorder.read(buffer, FRAME_SIZE * 2);
                
                // 添加RTP头部(时间戳+序号)
                RtpPacket packet = new RtpPacket();
                packet.timestamp = SystemClock.elapsedRealtimeNanos() / 1000;
                packet.sequence = nextSequenceNumber();
                packet.payload = buffer.array();
                
                // 立即发送数据包
                networkSender.send(packet);
                
                buffer.rewind(); // 重用缓冲区
            }
        }
        
        private AudioRecord createAudioRecord() {
            return new AudioRecord.Builder()
                .setAudioFormat(new AudioFormat.Builder()
                    .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                    .setSampleRate(SAMPLE_RATE)
                    .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
                    .build())
                .setBufferSizeInBytes(FRAME_SIZE * 4) // 双缓冲
                .build();
        }
    }

    关键原理说明:

    • 时间戳精度:采用微秒级时间戳(elapsedRealtimeNanos()/1000)确保时序精度
    • 环形缓冲区:DirectByteBuffer 重用避免内存抖动
    • 实时发送:每个 20ms 数据包立即发送,无需等待前序确认

    2. 实时播放机制

    class AudioPlaybackThread extends Thread {
        private static final int JITTER_BUFFER_DEPTH = 5; // 100ms缓冲深度
        private final PriorityBlockingQueue<RtpPacket> buffer = 
            new PriorityBlockingQueue<>(50, Comparator.comparingLong(p -> p.timestamp));
        
        private AudioTrack audioTrack;
        private long lastPlayedTimestamp = 0;
    
        @Override
        public void run() {
            audioTrack = createAudioTrack();
            audioTrack.play();
            
            while (isRunning) {
                RtpPacket packet = waitForNextPacket();
                writeToAudioTrack(packet);
                updateTimeline(packet);
            }
        }
        
        private RtpPacket waitForNextPacket() {
            if (buffer.size() < JITTER_BUFFER_DEPTH) {
                // 缓冲不足时插入静音包
                return generateSilencePacket();
            }
            
            return buffer.poll(20, TimeUnit.MILLISECONDS); // 阻塞等待
        }
        
        private void writeToAudioTrack(RtpPacket packet) {
            // 抖动补偿计算
            long expectedTimestamp = lastPlayedTimestamp + 20000; // 20ms间隔
            long timestampDelta = packet.timestamp - expectedTimestamp;
            
            if (timestampDelta > 50000) { // 超过50ms延迟
                resetPlayback(); // 重置时间线
            }
            
            audioTrack.write(packet.payload, 0, packet.payload.length);
            lastPlayedTimestamp = packet.timestamp;
        }
        
        private AudioTrack createAudioTrack() {
            return new AudioTrack.Builder()
                .setAudioFormat(new AudioFormat.Builder()
                    .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                    .setSampleRate(SAMPLE_RATE)
                    .setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
                    .build())
                .setBufferSizeInBytes(FRAME_SIZE * 4)
                .setTransferMode(AudioTrack.MODE_STREAM)
                .build();
        }
    }

    核心算法解析:

    • 自适应抖动缓冲:根据网络状况动态调整缓冲深度
    • 时间线同步:通过时间戳差值检测网络延迟突变
    • 静音补偿:在丢包时生成舒适噪声保持播放连续性

    二、网络传输层深度实现

    1. UDP 封装优化

    class UdpSender {
        private static final int MAX_RETRIES = 2;
        private static final int MTU = 1400; // 典型移动网络MTU
        
        private final DatagramChannel channel;
        private final InetSocketAddress remoteAddress;
    
        void send(RtpPacket packet) {
            ByteBuffer buffer = ByteBuffer.wrap(packet.serialize());
            
            // 分片发送(应对MTU限制)
            while (buffer.hasRemaining()) {
                int bytesToSend = Math.min(buffer.remaining(), MTU);
                ByteBuffer slice = buffer.slice();
                slice.limit(bytesToSend);
                
                sendWithRetry(slice);
                buffer.position(buffer.position() + bytesToSend);
            }
        }
        
        private void sendWithRetry(ByteBuffer data) {
            int attempt = 0;
            while (attempt <= MAX_RETRIES) {
                try {
                    channel.send(data, remoteAddress);
                    return;
                } catch (IOException e) {
                    if (++attempt > MAX_RETRIES) {
                        reportNetworkError(e);
                    }
                }
            }
        }
    }

    关键技术点:

    • MTU 适配:自动分片避免 IP 层分片
    • 有限重试:防止过度重传增加延迟
    • 非阻塞 IO:使用 NIO DatagramChannel 提升性能

    2. 前向纠错实现

    import com.googlecode.javaewah.EWAHCompressedBitmap;
    import com.googlecode.javaewah.IntIterator;
    
    class FecEncoder {
        // Reed - Solomon(5,3)编码配置
        private static final int DATA_SHARDS = 3;
        private static final int PARITY_SHARDS = 2;
        private static final int TOTAL_SHARDS = DATA_SHARDS + PARITY_SHARDS;
        private final RSCodec codec = new RSCodec(DATA_SHARDS, PARITY_SHARDS);
    
        public List<byte[]> encode(byte[] input) {
            byte[][] shards = splitIntoShards(input);
            codec.encodeParity(shards, 0, DATA_SHARDS);
            List<byte[]> result = new ArrayList<>();
            for (byte[] shard : shards) {
                result.add(shard);
            }
            return result;
        }
    
        private byte[][] splitIntoShards(byte[] data) {
            int shardSize = (data.length + DATA_SHARDS - 1) / DATA_SHARDS;
            byte[][] shards = new byte[TOTAL_SHARDS][shardSize];
    
            for (int i = 0; i < DATA_SHARDS; i++) {
                int start = i * shardSize;
                int end = Math.min(start + shardSize, data.length);
                System.arraycopy(data, start, shards[i], 0, end - start);
            }
            return shards;
        }
    }

    数学原理:

    • 使用 Reed-Solomon 纠错码,可恢复任意 2 个分片的丢失
    • 编码效率:3 个数据分片 + 2 个校验分片,可容忍 40% 的随机丢包

    三、音频处理核心技术

    1. 回声消除实现,下面是CPP代码,有ai加持

    // 使用WebRTC AEC模块的JNI接口
    extern "C" JNIEXPORT void JNICALL
    Java_com_example_voice_AecProcessor_processFrame(
        JNIEnv* env,
        jobject thiz,
        jshortArray micData,
        jshortArray speakerData) {
        
        webrtc::EchoCancellation* aec = GetAecInstance();
        
        jshort* mic = env->GetShortArrayElements(micData, 0);
        jshort* speaker = env->GetShortArrayElements(speakerData, 0);
        
        // 执行AEC处理
        aec->ProcessRenderAudio(speaker, FRAME_SIZE);
        aec->ProcessCaptureAudio(mic, FRAME_SIZE, 0);
        
        env->ReleaseShortArrayElements(micData, mic, 0);
        env->ReleaseShortArrayElements(speakerData, speaker, 0);
    }

    算法流程:

    • 记录扬声器输出信号(参考信号)
    • 使用自适应滤波器建模声学路径
    • 从麦克风信号中减去估计的回声成分

    2. 动态码率调整,包含网络评估方法

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    class BitrateController {
        private int currentBitrate = 1000000; // 初始1Mbps
        private final NetworkMonitor networkMonitor;
        private final List<BitrateListener> listeners = new ArrayList<>();
        private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
    
        public BitrateController(NetworkMonitor networkMonitor) {
            this.networkMonitor = networkMonitor;
            executorService.scheduleAtFixedRate(() -> {
                int quality = calculateNetworkQuality();
                adjustBitrate(quality);
            }, 0, 2, TimeUnit.SECONDS);
        }
    
        private int calculateNetworkQuality() {
            NetworkStatus status = networkMonitor.getLatestStatus();
            if (status instanceof NetworkGood) {
                return 90;
            } else if (status instanceof NetworkFair) {
                return 70;
            } else if (status instanceof NetworkPoor) {
                return 40;
            }
            return 50;
        }
    
        private void adjustBitrate(int quality) {
            int newBitrate;
            if (quality > 80) {
                newBitrate = (int) (currentBitrate * 1.2);
            } else if (quality < 40) {
                newBitrate = (int) (currentBitrate * 0.7);
            } else {
                newBitrate = currentBitrate;
            }
            newBitrate = Math.max(100000, Math.min(2000000, newBitrate));
    
            if (newBitrate != currentBitrate) {
                currentBitrate = newBitrate;
                for (BitrateListener listener : listeners) {
                    listener.onBitrateChanged(newBitrate);
                }
            }
        }
    
        public void addBitrateListener(BitrateListener listener) {
            listeners.add(listener);
        }
    
        public void removeBitrateListener(BitrateListener listener) {
            listeners.remove(listener);
        }
    }
    
    interface BitrateListener {
        void onBitrateChanged(int newBitrate);
    }
    
    class NetworkMonitor {
        private int rtt = 100; // 毫秒
        private float lossRate = 0f; // 丢包率
        private int jitter = 50; // 抖动
    
        public NetworkStatus getLatestStatus() {
            if (lossRate > 0.2f) {
                return new NetworkPoor();
            } else if (rtt > 300) {
                return new NetworkFair();
            }
            return new NetworkGood();
        }
    
        public void setRtt(int rtt) {
            this.rtt = rtt;
        }
    
        public void setLossRate(float lossRate) {
            this.lossRate = lossRate;
        }
    
        public void setJitter(int jitter) {
            this.jitter = jitter;
        }
    }
    
    class NetworkGood implements NetworkStatus {}
    class NetworkFair implements NetworkStatus {}
    class NetworkPoor implements NetworkStatus {}
    interface NetworkStatus {}

    四、完整系统时序分析

    sequenceDiagram
        participant A as 发送端
        participant B as 网络
        participant C as 接收端
        
        A->>B: 发送数据包1(seq=1, ts=100)
        A->>B: 发送数据包2(seq=2, ts=120)
        B--xC: 包2丢失
        A->>B: 发送数据包3(seq=3, ts=140)
        C->>C: 检测到seq=2缺失
        C->>B: 发送NACK重传请求
        B->>C: 重传数据包2
        C->>C: 按时间戳排序[1,3,2]
        C->>C: 调整播放顺序为[1,2,3]

    我的主题貌似解析不了,请看下图

    关键时序说明:

    • 接收端通过时间戳检测乱序包
    • 选择性重传机制(NACK)保证关键数据
    • 播放线程按时间戳重新排序

    五、性能优化实战

    1. 内存优化技巧

    // 使用对象池减少GC
    public class RtpPacketPool {
        private static final int MAX_POOL_SIZE = 50;
        private static final Queue<RtpPacket> pool = new ConcurrentLinkedQueue<>();
        
        public static RtpPacket obtain() {
            RtpPacket packet = pool.poll();
            return packet != null ? packet : new RtpPacket();
        }
        
        public static void recycle(RtpPacket packet) {
            if (pool.size() < MAX_POOL_SIZE) {
                packet.reset();
                pool.offer(packet);
            }
        }
    }
    
    // 使用示例
    void processPacket(byte[] data) {
        RtpPacket packet = RtpPacketPool.obtain();
        packet.parse(data);
        // ...处理逻辑...
        RtpPacketPool.recycle(packet);
    }

    2. 线程优先级调整

    // 设置实时音频线程优先级
    private void setThreadPriority() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO);
        
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            PerformanceHintManager perfHintManager = 
                (PerformanceHintManager) context.getSystemService(Context.PERFORMANCE_HINT_SERVICE);
            
            SessionParams params = new SessionParams(
                PerformanceHintManager.SESSION_TYPE_CPU_LOAD,
                new CpuRange(70, 100));
            
            PerformanceHintManager.Session session = 
                perfHintManager.createSession(params);
            
            session.reportActualWorkDuration(500_000); // 500ms周期
        }
    }

    优化效果:

    • 音频线程调度延迟降低 30-50%
    • GC 暂停时间减少 80%
    • CPU 利用率提升 20%

    需要更深入某个技术点的实现细节可以随时告知!

  • Android第三方APP拍照开发指南

    现状分析

    在 Android 第三方 APP 拍照功能的实现现状中,我们不难发现存在不少问题,其中图像不清晰是较为突出的一个。经过深入研究发现,这主要归因于大部分 APP 采用 preview(预览)截图的方式来充当拍照结果。这种做法存在很大的局限性,preview 截图只是对相机预览画面的简单截取,并没有经过相机真正拍照时完整且精细的图像生成和处理流程。相机拍照时,会利用复杂的光学和电子元件精确地捕捉光线信息,然后经过一系列诸如自动对焦、自动曝光、色彩校正、降噪等处理环节,而 preview 截图往往缺失这些关键步骤,所以导致拍摄出的图像质量远低于预期。

    所以,这种方法不仅导致图片质量差,而且用户体验也不佳。由于没有充分利用Android系统的相机功能,很多开发者在实现拍照时面临诸多困难。大概归为下面几个比较突出的问题:

    • 图片质量低:预览截图无法获取高质量的照片。
    • 用户体验差:拍照过程不够流畅,用户可能需要多次尝试才能获得满意的照片。
    • 功能限制:无法使用相机的高级功能,如闪光灯、焦距调整等。

    正确的拍照方式

    一、调用系统相机拍照

    当第三方 APP 在 Android 系统中调用相机拍照时,本质上是通过与系统相机服务交互来实现的。应用会发送一个特定的 Intent 来唤起系统相机应用。这个 Intent 可以携带一些参数,比如指定输出的图像格式、存储位置等。系统相机应用启动后,它会初始化相机硬件,包括启动相机传感器、配置镜头参数等。当用户触发拍照操作时,相机传感器开始工作,光线通过镜头在传感器上成像。传感器将光信号转换为电信号,再经过模数转换为数字信号。这些数字信号随后在相机内部的图像处理器中经过一系列复杂算法的处理,如根据光线条件调整曝光值、通过自动对焦算法使图像清晰对焦、对色彩进行平衡处理以保证色彩的准确性。处理完成后,图像数据会根据设定的格式(如 JPEG)存储在指定位置,并通过 Intent 将拍摄结果回传给第三方应用,这个过程需要确保应用有相应的存储权限和读取权限。

    那么,在Android中,调用系统相机拍照的逻辑相对简单,主要通过Intent来实现。以下是基本的实现步骤:

    1. 创建Intent:使用MediaStore.ACTION_IMAGE_CAPTURE创建一个拍照的Intent。
    2. 传递文件URI:指定照片存储的位置,确保拍摄的照片能正确保存。
    3. 启动Activity:通过startActivityForResult启动相机界面。
    4. 处理结果:在onActivityResult中处理拍照后的结果。

    简单的实现步骤如下:

    private static final int REQUEST_IMAGE_CAPTURE = 1;
    private Uri photoURI;
    
    private void dispatchTakePictureIntent() {
        Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
            // 创建一个文件来存储照片
            File photoFile = createImageFile();
            if (photoFile != null) {
                photoURI = FileProvider.getUriForFile(this,
                        "com.example.android.fileprovider",
                        photoFile);
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
                startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
            }
        }
    }
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
            // 处理拍照结果
            // photoURI指向的文件就是拍摄的照片
        }
    }

    二、通过Camera2软件包进行拍照

    android.hardware.camera2

    首先一张图来说明一下构建相机APP的正确逻辑:

    2018 I/O大会

    Camera2 API提供了对相机硬件的深入控制,支持更复杂的功能,如手动对焦、曝光控制等。使用Camera2 API的基本步骤如下:

    1. 获取CameraManager:使用CameraManager获取相机服务。
    2. 打开相机:通过openCamera方法打开相机。
    3. 创建CaptureSession:配置并创建用于捕获图像的会话。
    4. 拍照:使用CaptureRequest配置拍照参数并执行。

    具体实现代码(上述步骤为简要步骤,具体请参考代码):

    申请权限

    <uses-permission android:name="android.permission.CAMERA" />

    获取可用相机设备列表

    CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
    try {
        String[] cameraIds = manager.getCameraIdList();
        for (String cameraId : cameraIds) {
            CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
            // 这里可以进一步检查相机特性,如是否支持特定功能等
        }
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }

    配置相机参数

    private CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(CameraDevice camera) {
            // 相机打开成功,可以进行参数配置和开始预览、拍照等操作
            try {
                CameraCaptureSession.StateCallback sessionCallback = new CameraCaptureSession.StateCallback() {
                    @Override
                    public void onConfigured(CameraCaptureSession session) {
                        // 会话配置成功,可以设置拍照请求等
                        try {
                            CaptureRequest.Builder builder = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
                            // 设置图像格式、自动对焦模式等参数
                            builder.addTarget(imageReader.getSurface());
                            CaptureRequest request = builder.build();
                            session.capture(request, null, null);
                        } catch (CameraAccessException e) {
                            e.printStackTrace();
                        }
                    }
    
                    @Override
                    public void onConfigureFailed(CameraCaptureSession session) {
                        // 会话配置失败处理
                    }
                };
                camera.createCaptureSession(Arrays.asList(imageReader.getSurface()), sessionCallback, null);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void onDisconnected(CameraDevice camera) {
            // 相机断开连接处理
            camera.close();
        }
    
        @Override
        public void onError(CameraDevice camera, int error) {
            // 相机出现错误处理
            camera.close();
        }
    };

    进行拍照

    CaptureRequest.Builder builder = camera.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
    builder.addTarget(imageReader.getSurface());
    // 设置自动对焦模式为自动
    builder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO);
    // 设置曝光模式为自动
    builder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
    CaptureRequest request = builder.build();
    session.capture(request, null, null);

    总结简单的实现方法

    private void openCamera() {
        CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        try {
            String cameraId = manager.getCameraIdList()[0]; // 获取后置相机ID
            manager.openCamera(cameraId, stateCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }
    
    private CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            // 相机打开成功,开始拍照
            // 此处可以配置CaptureSession
        }
    
        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            camera.close();
        }
    
        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            camera.close();
        }
    };

    通过CameraX软件包进行拍照

    CameraX 是一个 Jetpack 库。同样更方便地提供了包括但不限于相机的预览,分析,视频和图片的拍摄的api。下面是使用代码:

    添加依赖

    def camerax_version = "1.2.0"
    implementation "androidx.camera:camera-core:$camerax_version"
    implementation "androidx.camera:camera-camera2:$camerax_version"
    def camerax_version = "1.1.0-alpha07"
    implementation "androidx.camera:camera-core:$camerax_version"
    implementation "androidx.camera:camera-camera2:$camerax_version"
    implementation "androidx.camera:camera-lifecycle:$camerax_version"
    implementation "androidx.camera:camera-view:$camerax_version"
    

    初始化CameraX

    ProcessCameraProvider.getInstance(this).addListener(() -> {
        try {
            cameraProvider = ProcessCameraProvider.getInstance(context).get();
            bindCameraUseCases();
        } catch (ExecutionException | InterruptedException e) {
            // 异常处理
        }
    }, ContextCompat.getMainExecutor(this));

    配置和使用相机示例

    // 创建 ImageCapture 和 Preview 等相机用例,并将它们绑定到相机设备上
    private void bindCameraUseCases() {
        Preview preview = new Preview.Builder()
              .build();
        preview.setSurfaceProvider(viewFinder.getSurfaceProvider());
    
        ImageCapture imageCapture = new ImageCapture.Builder()
              .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
              .build();
    
        CameraSelector cameraSelector = new CameraSelector.Builder()
              .requireLensFacing(CameraSelector.LENS_FACING_BACK)
              .build();
    
        try {
            cameraProvider.unbindAll();
            camera = cameraProvider.bindToLifecycle((LifecycleOwner) this, cameraSelector, preview, imageCapture);
        } catch (Exception e) {
            // 绑定失败处理
        }
    }

    拍照操作

    imageCapture.takePicture(ContextCompat.getMainExecutor(this), new ImageCapture.OnImageCapturedCallback() {
        @Override
        public void onCaptureSuccess(@NonNull ImageProxy image) {
            // 拍照成功处理,这里可以获取图像数据并进一步处理
            super.onCaptureSuccess(image);
            image.close();
        }
    
        @Override
        public void onError(@NonNull ImageCaptureException exception) {
            // 拍照错误处理
            super.onError(exception);
        }
    });

    依然,提供一个简单的示例

    private void startCamera() {
        CameraX.bindToLifecycle(this, preview, imageCapture);
    }
    
    // 配置预览
    Preview preview = new Preview.Builder().build();
    
    // 配置拍照
    ImageCapture imageCapture = new ImageCapture.Builder().build();

    小结

    在Android应用开发中,实现高质量的拍照功能至关重要。通过合理选择相机API(如Camera2和CameraX),开发者可以提供更高质量的照片和更流畅的用户体验。还有很多内容本文没有提到,例如:HDR,多相机(Multi-Camera)调用,屏幕闪烁设置,视频流(相机帧)的优化,CameraX的Extensions API 。

    目前来看,android的相机和iOS一对比,真是一坨越堆越高的大屎山!