如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(三)播放能力

RK3572-Android16-记录一次OTA升级失败解决方案 这里记录一次解决RK3576 Android16 OTA 用Recovery 升级失败的一次解决案例,将部分经验作为总结。本次OTA升级耗时了几天,最终是RK给出的解决方案,再加上经验来解决了其它部分问题新平台,高一点的Android版本,可能本身有bug,多请教平台来,不要蒙着干,干不出来的。 阅读详情

前言


在这里插入图片描述

本章我们继续使用 Jetpack 来完善音乐播放器,今天我们来晚上 播放 能力,也就是 PlayerFragment;

PlayerFragment

播放器我们使用了一个开源的:

implementation 'com.kunminx.player:player:1.1.6'

PlayerManager

我们来定义一个 PlayerManager 来管理我们的播放能力;

class PlayerManager private constructor() : IPlayController<TestAlbum, TestMusic?> {

    private val mController: PlayerController<TestAlbum, TestMusic>

    private var mContext: Context? = null

    fun init(context: Context) {
        init(context, null)
    }

    override fun init(context: Context, iServiceNotifier: IServiceNotifier?) {
        mContext = context.applicationContext
        mController.init(mContext, null) { startOrStop: Boolean ->
            val intent = Intent(mContext, PlayerService::class.java)
            if (startOrStop) {
                // 后台播放
                mContext?.startService(intent) 
            } else {
                // 停止后台播放
                mContext?.stopService(intent) 
            }
        }
    }

    override fun loadAlbum(musicAlbum: TestAlbum) {
        mController.loadAlbum(mContext, musicAlbum)
    }

    override fun loadAlbum(musicAlbum: TestAlbum, playIndex: Int) {
        mController.loadAlbum(mContext, musicAlbum, playIndex)
    }

    override fun playAudio() {
        mController.playAudio(mContext)
    }

    // 下标的播放
    override fun playAudio(albumIndex: Int) {
        // 在这里只需要知道,调用此 playAudio函数,就能够播放音乐了
        mController.playAudio(mContext, albumIndex)
    }

    // 下一首播放
    override fun playNext() {
        mController.playNext(mContext)
    }

    // 上一首播放
    override fun playPrevious() {
        mController.playPrevious(mContext)
    }

    override fun playAgain() {
        mController.playAgain(mContext)
    }

    // 暂停播放
    override fun pauseAudio() {
        mController.pauseAudio()
    }

    override fun resumeAudio() {
        mController.resumeAudio()
    }

    // 清除歌曲下标的标记
    override fun clear() {
        mController.clear(mContext)
    }

    override fun changeMode() {
        mController.changeMode()
    }

    override fun isPlaying(): Boolean {
        return mController.isPlaying
    }

    override fun isPaused(): Boolean {
        return mController.isPaused
    }

    override fun isInited(): Boolean {
        return mController.isInited
    }

    override fun requestLastPlayingInfo() {
        mController.requestLastPlayingInfo()
    }

    override fun setSeek(progress: Int) {
        mController.setSeek(progress)
    }

    override fun getTrackTime(progress: Int): String {
        return mController.getTrackTime(progress)
    }

    override fun getAlbum(): TestAlbum? {
        return mController.album
    }

    override fun getAlbumMusics(): List<TestMusic?> {
        return mController.albumMusics
    }

    override fun setChangingPlayingMusic(changingPlayingMusic: Boolean) {
        mController.setChangingPlayingMusic(mContext, changingPlayingMusic)
    }

    override fun getAlbumIndex(): Int {
        return mController.albumIndex
    }

    // 返回音乐播放的 ld,上一首,下一首
    override fun getChangeMusicLiveData(): MutableLiveData<ChangeMusic<*, *, *>> {
        return mController.changeMusicLiveData
    }

    // 音乐数据,也需要观察
    override fun getPlayingMusicLiveData(): MutableLiveData<PlayingMusic<*, *>> {
        return mController.playingMusicLiveData
    }

    override fun getPauseLiveData(): MutableLiveData<Boolean> {
        return mController.pauseLiveData
    }

    // 播放模式:列表循环,单曲循环,随机播放
    override fun getPlayModeLiveData(): MutableLiveData<Enum<*>> {
        return mController.playModeLiveData
    }

    override fun getRepeatMode(): Enum<*> ? {
        return mController.repeatMode
    }

    override fun togglePlay() {
        mController.togglePlay(mContext)
    }

    override fun getCurrentPlayingMusic(): TestMusic {
        return mController.currentPlayingMusic
    }

    companion object {
        // 单例模式
        @SuppressLint("StaticFieldLeak")
        val instance = PlayerManager() // 单例相关的
    }

    init {
        mController = PlayerController()
    }
}

PlayerService

我们再来定一个 PlayerService 用来支持后台播放的能力;

class PlayerService : Service() {
    private var mPlayerCallHelper: PlayerCallHelper? = null

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {

        // 在播放的时候来电话了
        if (mPlayerCallHelper == null) {
            mPlayerCallHelper = PlayerCallHelper(object : PlayerCallHelperListener {
                override fun playAudio() {
                    PlayerManager.instance.playAudio()
                }

                override val isPlaying: Boolean
                    get() = PlayerManager.instance.isPlaying
                override val isPaused: Boolean
                    get() = PlayerManager.instance.isPaused

                override fun pauseAudio() {
                    PlayerManager.instance.pauseAudio()
                }
            })
        }
        val results = PlayerManager.instance.currentPlayingMusic
        if (results == null) {
            stopSelf()
            return START_NOT_STICKY
        }
        mPlayerCallHelper?.bindCallListener(applicationContext)
        createNotification(results)
        return START_NOT_STICKY
    }

    // 创建通知
    private fun createNotification(testMusic: TestMusic) {
        try {
            val title = testMusic.title
            val album = PlayerManager.instance.album
            val summary = album?.summary
            val simpleContentView = RemoteViews(
                applicationContext.packageName, R.layout.notify_player_small
            )
            val expandedView: RemoteViews
            expandedView = RemoteViews(
                applicationContext.packageName, R.layout.notify_player_big
            )
            val intent = Intent(applicationContext, MainActivity::class.java)
            intent.action = "showPlayer"
            val contentIntent = PendingIntent.getActivity(
                this, 0, intent, 0
            )
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                val notificationManager =
                    getSystemService(NOTIFICATION_SERVICE) as NotificationManager
                val playGroup = NotificationChannelGroup(GROUP_ID, getString(R.string.play))
                notificationManager.createNotificationChannelGroup(playGroup)
                val playChannel = NotificationChannel(
                    CHANNEL_ID,
                    getString(R.string.notify_of_play), NotificationManager.IMPORTANCE_DEFAULT
                )
                playChannel.group = GROUP_ID
                notificationManager.createNotificationChannel(playChannel)
            }
            val notification = NotificationCompat.Builder(
                applicationContext, CHANNEL_ID
            )
                .setSmallIcon(R.drawable.ic_player)
                .setContentIntent(contentIntent)
                .setOnlyAlertOnce(true)
                .setContentTitle(title).build()
            notification.contentView = simpleContentView
            notification.bigContentView = expandedView
            setListeners(simpleContentView)
            setListeners(expandedView)
            notification.contentView.setViewVisibility(R.id.player_progress_bar, View.GONE)
            notification.contentView.setViewVisibility(R.id.player_next, View.VISIBLE)
            notification.contentView.setViewVisibility(R.id.player_previous, View.VISIBLE)
            notification.bigContentView.setViewVisibility(R.id.player_next, View.VISIBLE)
            notification.bigContentView.setViewVisibility(R.id.player_previous, View.VISIBLE)
            notification.bigContentView.setViewVisibility(R.id.player_progress_bar, View.GONE)
            val isPaused = PlayerManager.instance.isPaused
            notification.contentView.setViewVisibility(
                R.id.player_pause,
                if (isPaused) View.GONE else View.VISIBLE
            )
            notification.contentView.setViewVisibility(
                R.id.player_play,
                if (isPaused) View.VISIBLE else View.GONE
            )
            notification.bigContentView.setViewVisibility(
                R.id.player_pause,
                if (isPaused) View.GONE else View.VISIBLE
            )
            notification.bigContentView.setViewVisibility(
                R.id.player_play,
                if (isPaused) View.VISIBLE else View.GONE
            )
            notification.contentView.setTextViewText(R.id.player_song_name, title)
            notification.contentView.setTextViewText(R.id.player_author_name, summary)
            notification.bigContentView.setTextViewText(R.id.player_song_name, title)
            notification.bigContentView.setTextViewText(R.id.player_author_name, summary)
            notification.flags = notification.flags or Notification.FLAG_ONGOING_EVENT
            val coverPath = Configs.COVER_PATH + File.separator + testMusic.musicId + ".jpg"
            val bitmap = ImageUtils.getBitmap(coverPath)
            if (bitmap != null) {
                notification.contentView.setImageViewBitmap(R.id.player_album_art, bitmap)
                notification.bigContentView.setImageViewBitmap(R.id.player_album_art, bitmap)
            } else {
                requestAlbumCover(testMusic.coverImg, testMusic.musicId)
                notification.contentView.setImageViewResource(
                    R.id.player_album_art,
                    R.drawable.bg_album_default
                )
                notification.bigContentView.setImageViewResource(
                    R.id.player_album_art,
                    R.drawable.bg_album_default
                )
            }
            startForeground(5, notification)
            mPlayerCallHelper?.bindRemoteController(applicationContext)
            mPlayerCallHelper?.requestAudioFocus(title, summary)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    // 设置监听器
    fun setListeners(view: RemoteViews) {
        try {
            var pendingIntent = PendingIntent.getBroadcast(
                applicationContext,
                0, Intent(NOTIFY_PREVIOUS).setPackage(packageName),
                PendingIntent.FLAG_UPDATE_CURRENT
            )
            view.setOnClickPendingIntent(R.id.player_previous, pendingIntent)
            pendingIntent = PendingIntent.getBroadcast(
                applicationContext,
                0, Intent(NOTIFY_CLOSE).setPackage(packageName),
                PendingIntent.FLAG_UPDATE_CURRENT
            )
            view.setOnClickPendingIntent(R.id.player_close, pendingIntent)
            pendingIntent = PendingIntent.getBroadcast(
                applicationContext,
                0, Intent(NOTIFY_PAUSE).setPackage(packageName),
                PendingIntent.FLAG_UPDATE_CURRENT
            )
            view.setOnClickPendingIntent(R.id.player_pause, pendingIntent)
            pendingIntent = PendingIntent.getBroadcast(
                applicationContext,
                0, Intent(NOTIFY_NEXT).setPackage(packageName),
                PendingIntent.FLAG_UPDATE_CURRENT
            )
            view.setOnClickPendingIntent(R.id.player_next, pendingIntent)
            pendingIntent = PendingIntent.getBroadcast(
                applicationContext,
                0, Intent(NOTIFY_PLAY).setPackage(packageName),
                PendingIntent.FLAG_UPDATE_CURRENT
            )
            view.setOnClickPendingIntent(R.id.player_play, pendingIntent)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    // 请求专辑封面
    private fun requestAlbumCover(coverUrl: String, musicId: String) {
        // todo 
    }

    // 解绑监听等操作
    override fun onDestroy() {
        super.onDestroy()
        mPlayerCallHelper?.unbindCallListener(applicationContext)
        mPlayerCallHelper?.unbindRemoteController()
    }

    override fun onBind(intent: Intent): IBinder? {
        return null
    }

    // 此标记 与 广播接受者 AndroidManifest.xml 里面的标记保持一致
    companion object {
        const val NOTIFY_PREVIOUS = "pure_music.llc_mars.previous"
        const val NOTIFY_CLOSE = "pure_music.llc_mars.close"
        const val NOTIFY_PAUSE = "pure_music.llc_mars.pause"
        const val NOTIFY_PLAY = "pure_music.llc_mars.play"
        const val NOTIFY_NEXT = "pure_music.llc_mars.next"
        private const val GROUP_ID = "group_001"
        private const val CHANNEL_ID = "channel_001"
    }
}

PlayerReceiver

接下来,我们来创建一个广播,用于接收某些改变(系统发出来的信息,断网了),对音乐做出对应操作;

class PlayerReceiver : BroadcastReceiver() {

    // 广播接受者
    override fun onReceive(context: Context, intent: Intent) {

        if (intent.action == Intent.ACTION_MEDIA_BUTTON) {
            if (intent.extras == null) {
                return
            }

            val keyEvent = intent.extras?[Intent.EXTRA_KEY_EVENT] as KeyEvent? ?: return

            if (keyEvent.action != KeyEvent.ACTION_DOWN) {
                return
            }

            when (keyEvent.keyCode) {
                KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> PlayerManager.instance
                    .togglePlay()
                KeyEvent.KEYCODE_MEDIA_PLAY -> PlayerManager.instance.playAudio()  // 播放音频
                KeyEvent.KEYCODE_MEDIA_PAUSE -> PlayerManager.instance.pauseAudio() // 暂停音频
                KeyEvent.KEYCODE_MEDIA_STOP -> PlayerManager.instance.clear() // 清除记录
                KeyEvent.KEYCODE_MEDIA_NEXT -> PlayerManager.instance.playNext() // 下一首音频播放
                KeyEvent.KEYCODE_MEDIA_PREVIOUS -> PlayerManager.instance.playPrevious() // 上一首音频播放
                else -> {
                }
            }
        } else {
            if (Objects.requireNonNull(intent.action) == PlayerService.NOTIFY_PLAY) {
                PlayerManager.instance.playAudio() // 播放音频
            } else if (intent.action == PlayerService.NOTIFY_PAUSE || intent.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
                PlayerManager.instance.pauseAudio() // 暂停音频
            } else if (intent.action == PlayerService.NOTIFY_NEXT) {
                PlayerManager.instance.playNext() // 下一首音频播放
            } else if (intent.action == PlayerService.NOTIFY_CLOSE) {
                PlayerManager.instance.clear() // 清除记录
            } else if (intent.action == PlayerService.NOTIFY_PREVIOUS) {
                PlayerManager.instance.playPrevious() // 上一首音频播放
            }
        }
    }
}

接下来,我们需要完善下我们的 MainFragment,用来调用 PlayerManager 来实现播放;

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    .... 省略上一章的部分代码
    
    adapter = object : SimpleBaseBindingAdapter<TestAlbum.TestMusic?, AdapterPlayItemBinding?>(context, R.layout.adapter_play_item) {
        override fun onSimpleBindItem(
            binding: AdapterPlayItemBinding?,
            item: TestAlbum.TestMusic?,
            holder: RecyclerView.ViewHolder?) {

            .... 省略上一章的部分代码

            // 歌曲下标记录
            val currentIndex = PlayerManager.instance.albumIndex // 歌曲下标记录

            // 播放的标记
            binding.ivPlayStatus.setColor(
                if (currentIndex == holder ?.adapterPosition) resources.getColor(R.color.colorAccent) else Color.TRANSPARENT
            ) // 播放的时候,右变状态图标就是红色, 如果对不上的时候,就是没有

            // 点击Item
            binding.root.setOnClickListener { v ->
                Toast.makeText(mContext, "播放音乐", Toast.LENGTH_SHORT).show()
                PlayerManager.instance.playAudio(holder?.adapterPosition)
            }
        }
    }
    
    .... 省略上一章的部分代码
    
    // 播放相关业务的数据(如果这个数据发生了改变,为了更好的体验)
    PlayerManager.instance.changeMusicLiveData.observe(viewLifecycleOwner, {
        // 刷新适配器,这里也可以改成局部刷新,使用 DiffUtil 来完成局部刷新
        adapter?.notifyDataSetChanged() 
    })

    // 请求数据
    // 保证我们列表没有数据(music list)
    if (PlayerManager.instance.album == null) {
        musicRequestViewModel?.requestFreeMusics()
    }

    // 音乐资源的 VM getFreeMusicsLiveData变化了,如果变化就执行
    musicRequestViewModel?.freeMusicesLiveData?.observe(viewLifecycleOwner, { musicAlbum: TestAlbum? ->
        if (musicAlbum != null && musicAlbum.musics != null) {
            .... 省略上一章的部分代码

            // 播放相关的业务需要这个数据
            if (PlayerManager.instance.album == null ||
                PlayerManager.instance.album?.albumId != musicAlbum.albumId) {
                PlayerManager.instance.loadAlbum(musicAlbum)
            }
        }
    })  
}

PlayerViewModel

我们接下来完善下 PlayerFramgent 对应的 PlayerViewModel,用来控制播放;

class PlayerViewModel : ViewModel() {

    // 为什么要使用 ObservableField(DataBinding) 为什么不用LiveData
    // 不需要 LiveData 可见才能观察的能力,反而会导致有bug,因为灭屏、可见都要播放;

    // 歌曲名称
    @JvmField // 剔除 getXXX 函数
    val title = ObservableField<String>()

    // 歌手
    @JvmField
    val artist = ObservableField<String>()

    // 歌曲图片的地址:htpp:xxxx/img0.jpg
    @JvmField
    val coverImg = ObservableField<String>()

    // 歌曲正方形图片
    @JvmField
    val placeHolder = ObservableField<Drawable>()

    // 歌曲的总时长,会显示在拖动条后面
    @JvmField
    val maxSeekDuration = ObservableInt()

    // 当前拖动条的进度值
    @JvmField
    val currentSeekPosition = ObservableInt()

    // 播放按钮,状态的改变(播放和暂停)
    @JvmField
    val isPlaying = ObservableBoolean()

    // 这个是播放图标的状态,也都是属于状态的改变
    @JvmField
    val playModeIcon = ObservableField<MaterialDrawableBuilder.IconValue>()

    // 构造代码块,默认初始化
    init {
        title.set(Utils.getApp().getString(R.string.app_name)) // 默认信息
        artist.set(Utils.getApp().getString(R.string.app_name)) // 默认信息
        placeHolder.set(Utils.getApp().resources.getDrawable(R.drawable.bg_album_default)) // 默认的播放图标

        if (PlayerManager.instance.repeatMode === PlayingInfoManager.RepeatMode.LIST_LOOP) { // 如果等于“列表循环”
            playModeIcon.set(MaterialDrawableBuilder.IconValue.REPEAT)
        } else if (PlayerManager.instance.repeatMode === PlayingInfoManager.RepeatMode.ONE_LOOP) { // 如果等于“单曲循环”
            playModeIcon.set(MaterialDrawableBuilder.IconValue.REPEAT_ONCE)
        } else {
            playModeIcon.set(MaterialDrawableBuilder.IconValue.SHUFFLE) // 随机播放
        }
    }

}

接下来,我们在 PlayerFragment 中初始化这个 ViewModel;

PlayerFramgent

接下来,我们来晚上 PlayerFragment 用来展示画面,控制播放;

class PlayerFragment  : BaseFragment(){

    private var binding: FragmentPlayerBinding? = null
    private var playerViewModel: PlayerViewModel? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 初始化 VM
        playerViewModel = getFragmentViewModelProvider(this).get<PlayerViewModel>(PlayerViewModel::class.java)
    }

    // DataBinding + ViewModel
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?): View {

        // 加载界面
        val view: View = inflater.inflate(R.layout.fragment_player, container, false)
        // 绑定 Binding
        binding = FragmentPlayerBinding.bind(view)
        binding?.click = ClickProxy() // 布局控制 点击事件的
        binding?.event = EventHandler() // 布局控制 拖动条的
        binding?.vm = playerViewModel // ViewModel与布局关联
        return view
    }

    // 观察变化
    // 观察到数据的变化,就变化
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // 观察:此字段只要发生了改变,就会添加监听(可以弹上来的监听)
        sharedViewModel.timeToAddSlideListener.observe(viewLifecycleOwner, {

            if (view.parent.parent is SlidingUpPanelLayout) {
                val sliding = view.parent.parent as SlidingUpPanelLayout
                // 添加监听(可以弹上来的监听)
                bingding?.let {
                    sliding.addPanelSlideListener(PlayerSlideListener(it, sliding))
                }
            }
        })

        // 我是播放条,我要去变化,我成为观察者 ---> 播放相关的类 PlayerManager
        PlayerManager.instance.changeMusicLiveData.observe(viewLifecycleOwner,  { changeMusic ->
            // 例如 :理解切歌的时候,音乐的标题,作者,封面,状态等改变
            playerViewModel?.title.set(changeMusic.title)
            playerViewModel?.artist.set(changeMusic.summary)
            playerViewModel?.coverImg.set(changeMusic.img)
        })

        // 我是播放条,我要去变化,我成为观察者 -----> 播放相关的类PlayerManager
        PlayerManager.instance.playingMusicLiveData.observe(viewLifecycleOwner, { playingMusic ->
            // 例如 :理解 切歌的时候,  播放进度的改变  按钮图标的改变
            playerViewModel?.maxSeekDuration.set(playingMusic.duration) // 总时长
            playerViewModel?.currentSeekPosition.set(playingMusic.playerPosition) // 拖动条
        })

        // 播放/暂停是一个控件  图标的 true 和 false
        PlayerManager.instance.pauseLiveData.observe(viewLifecycleOwner, { aBoolean ->
            // 播放时显示暂停,暂停时显示播放
            aBoolean?.let {
                playerViewModel?.isPlaying.set(!it)
            }
        })

        // 列表循环,单曲循环,随机播放 模式
        PlayerManager.instance.playModeLiveData.observe(viewLifecycleOwner, { anEnum ->
            val resultID = if (anEnum === PlayingInfoManager.RepeatMode.LIST_LOOP) {              
                 // 列表循环
                 playerViewModel?.playModeIcon.set(MaterialDrawableBuilder.IconValue.REPEAT)
                 R.string.play_repeat 
            } else if (anEnum === PlayingInfoManager.RepeatMode.ONE_LOOP) { 
                // 单曲循环
                playerViewModel?.playModeIcon.set(MaterialDrawableBuilder.IconValue.REPEAT_ONCE)
                R.string.play_repeat_once
            } else { 
                // 随机循环
                playerViewModel?.playModeIcon.set(MaterialDrawableBuilder.IconValue.SHUFFLE)
                R.string.play_shuffle
            }

            // 提示改变
            if (view.parent.parent is SlidingUpPanelLayout) {
                val sliding = view.parent.parent as SlidingUpPanelLayout
                // 展开状态
                if (sliding.panelState == SlidingUpPanelLayout.PanelState.EXPANDED) { 
                    // 弹出:“列表循环” or “单曲循环” or “随机播放”,这里可以进行各种扩展
                    showShortToast(resultID)
                }
            }
        })

        // 可以控制 播放详情 点击 back 收起
        sharedViewModel.closeSlidePanelIfExpanded.observe(viewLifecycleOwner, {
            if (view.parent.parent is SlidingUpPanelLayout) {
                val sliding = view.parent.parent as SlidingUpPanelLayout
                // 如果是扩大,也就是,详情页面展示出来的
                if (sliding.panelState == SlidingUpPanelLayout.PanelState.EXPANDED) {
                    // 缩小了(掉下来了)
                    sliding.panelState = SlidingUpPanelLayout.PanelState.COLLAPSED 
                    // 活动关闭的一些记录(播放条 缩小一条 与 扩大展开)
                    sharedViewModel.activityCanBeClosedDirectly.setValue(true) 
                } else {
                    // 活动关闭的一些记录(播放条 缩小一条 与 扩大展开)
                    sharedViewModel.activityCanBeClosedDirectly.setValue(false) 
                }
            } else {
                // 活动关闭的一些记录(播放条 缩小一条 与 扩大展开)
                sharedViewModel.activityCanBeClosedDirectly.setValue(false) 
            }
        })
    }

    /**
     * 当我们点击的时候,触发
     */
    inner class ClickProxy {
        // 上一首
        fun previous() = PlayerManager.instance.playPrevious() 
        // 下一首
        operator fun next() = PlayerManager.instance.playNext() 
        // 左手边的滑落,点击缩小的,可以控制播放详情点击 back 掉下来
        fun slideDown() = sharedViewModel.closeSlidePanelIfExpanded.setValue(true)
        // 更多的
        fun more() = Toast.makeText(mActivity, "你能不能不要乱点", Toast.LENGTH_SHORT).show()
        // 切换播放状态
        fun togglePlay() = PlayerManager.instance.togglePlay()
        // 播放
        fun playMode() = PlayerManager.instance.changeMode() 
        // 提示
        fun showPlayList() = showShortToast("最近播放的细节,我没有搞...")
    }

    // 内部类的好处,方便获取当前 Fragment 的环境
    /**
     * 更新 拖动条进度相关
     */
    inner class EventHandler : SeekBar.OnSeekBarChangeListener {
        override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
        override fun onStartTrackingTouch(seekBar: SeekBar) {}

        // 一拖动松开手把当前进度值通知给 PlayerManager
        override fun onStopTrackingTouch(seekBar: SeekBar) = PlayerManager.instance.setSeek(seekBar.progress)
    }
}

fragment_player.xml

接下来,我们开始 PlayerFragment 的布局编写,依然使用 DataBinding 来完成编写;

<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:playpauseview="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <!-- 点击的时候触发 -->
        <variable
            name="click"
            type="com.llc.puremusic.ui.page.PlayerFragment.ClickProxy" />

        <!-- 更新拖动条进度相关 -->
        <variable
            name="event"
            type="com.llc.puremusic.ui.page.PlayerFragment.EventHandler" />

        <!-- 底部播放条的 ViewModel -->
        <variable
            name="vm"
            type="com.llc.puremusic.bridge.state.PlayerViewModel" />

    </data>

    <androidx.coordinatorlayout.widget.CoordinatorLayout
        android:id="@+id/topContainer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="top">

        <!-- 看起来是 “播放条” 实际上责任很大,非常重要,管控了整个 -->
        <com.llc.puremusic.ui.view.ForegroundImageView
            android:id="@+id/album_art"
            imageUrl="@{vm.coverImg}"
            placeHolder="@{vm.placeHolder}"
            android:layout_width="@dimen/sliding_up_header"
            android:layout_height="@dimen/sliding_up_header"
            android:scaleType="centerCrop"
            android:src="@drawable/bg_album_default"
            android:visibility="visible"/>

        <!-- 下面的都属于播放详情了 -->
        <RelativeLayout
            android:id="@+id/custom_toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:layout_gravity="top"
            android:layout_marginTop="37dp"
            android:orientation="horizontal"
            android:visibility="invisible">

            <!-- 左手边的滑落 -->
            <net.steamcrafted.materialiconlib.MaterialIconView
                android:id="@+id/btn_close"
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:layout_alignParentStart="true"
                android:layout_centerVertical="true"
                android:layout_marginStart="16dp"
                android:background="?attr/selectableItemBackgroundBorderless"
                android:onClick="@{()->click.slideDown()}"
                android:scaleType="center"
                app:materialIcon="chevron_down"
                app:materialIconColor="@color/white"
                app:materialIconSize="28dp"
                android:visibility="visible"/>

            <!-- 右手边的更多 -->
            <net.steamcrafted.materialiconlib.MaterialIconView
                android:id="@+id/popup_menu"
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:layout_alignParentEnd="true"
                android:layout_centerVertical="true"
                android:layout_marginEnd="16dp"
                android:background="?attr/selectableItemBackgroundBorderless"
                android:onClick="@{()->click.more()}"
                android:scaleType="center"
                app:materialIcon="dots_vertical"
                app:materialIconColor="@color/white"
                app:materialIconSize="28dp"
                android:visibility="visible"/>

        </RelativeLayout>

        <!-- 歌词控件 -->
        <com.llc.puremusic.ui.view.LyricView
            android:id="@+id/lyric_view"
            android:layout_width="match_parent"
            android:layout_height="32dp"
            android:visibility="gone" />

        <!-- 歌手,描述,暂无歌词 等信息 -->
        <LinearLayout
            android:id="@+id/summary"
            android:layout_width="match_parent"
            android:layout_height="@dimen/sliding_up_header"
            android:layout_gravity="top"
            android:layout_marginStart="@dimen/sliding_up_header"
            android:orientation="vertical">

            <ProgressBar
                android:id="@+id/song_progress_normal"
                style="@style/Widget.AppCompat.ProgressBar.Horizontal"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="top"
                android:max="@{vm.maxSeekDuration}"
                android:minHeight="4dp"
                android:progress="@{vm.currentSeekPosition}"
                android:progressDrawable="@drawable/progressbar_color"
                android:progressTint="?colorAccent" />

            <TextView
                android:id="@+id/title"
                style="@style/TextAppearance.AppCompat.Body1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="12dp"
                android:layout_marginEnd="42dp"
                android:ellipsize="end"
                android:maxLines="1"
                android:text="@{vm.title}"
                android:textSize="16sp"
                tools:text="title" />

            <TextView
                android:id="@+id/artist"
                style="@style/TextAppearance.AppCompat.Widget.ActionMode.Subtitle"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="12dp"
                android:ellipsize="end"
                android:maxLength="20"
                android:maxLines="1"
                android:text="@{vm.artist}"
                android:textColor="?android:textColorSecondary"
                android:textSize="14sp"
                tools:text="artist" />

        </LinearLayout>

        <!-- 五个控件 -->
        <RelativeLayout
            android:id="@+id/icon_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="top"
            android:layout_marginTop="10dp">

            <net.steamcrafted.materialiconlib.MaterialIconView
                android:id="@+id/next"
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:layout_alignParentEnd="true"
                android:layout_marginEnd="16dp"
                android:background="?attr/selectableItemBackgroundBorderless"
                android:onClick="@{()->click.next()}"
                android:scaleType="center"
                app:materialIcon="skip_next"
                app:materialIconColor="@android:color/black"
                app:materialIconSize="28dp" />

            <com.llc.puremusic.ui.view.PlayPauseView
                android:id="@+id/play_pause"
                isPlaying="@{vm.isPlaying}"
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:layout_marginEnd="16dp"
                android:layout_toStartOf="@id/next"
                android:clickable="true"
                android:focusable="true"
                android:foreground="?attr/selectableItemBackground"
                android:onClick="@{()->click.togglePlay()}"
                playpauseview:circleAlpha="0"
                playpauseview:isCircleDraw="true" />

            <net.steamcrafted.materialiconlib.MaterialIconView
                android:id="@+id/previous"
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:layout_alignParentTop="true"
                android:layout_marginEnd="16dp"
                android:layout_toStartOf="@+id/play_pause"
                android:alpha="0"
                android:background="?attr/selectableItemBackgroundBorderless"
                android:onClick="@{()->click.previous()}"
                android:scaleType="center"
                app:materialIcon="skip_previous"
                app:materialIconColor="@android:color/black"
                app:materialIconSize="28dp" />

            <net.steamcrafted.materialiconlib.MaterialIconView
                android:id="@+id/mark"
                mdIcon="@{vm.playModeIcon}"
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:layout_marginEnd="16dp"
                android:layout_toStartOf="@id/previous"
                android:alpha="0"
                android:background="?attr/selectableItemBackgroundBorderless"
                android:onClick="@{()->click.playMode()}"
                android:scaleType="center"
                app:materialIconColor="@android:color/black"
                app:materialIconSize="28dp" />

            <net.steamcrafted.materialiconlib.MaterialIconView
                android:id="@+id/ic_play_list"
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:layout_marginStart="16dp"
                android:layout_toEndOf="@id/next"
                android:background="?attr/selectableItemBackgroundBorderless"
                android:onClick="@{()->click.showPlayList()}"
                android:scaleType="center"
                app:materialIcon="playlist_play"
                app:materialIconColor="@android:color/black"
                app:materialIconSize="28dp" />

        </RelativeLayout>

        <!-- 拖动条 -->
        <SeekBar
            android:id="@+id/seek_bottom"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
            android:background="@color/transparent"
            android:clickable="true"
            android:focusable="true"
            android:max="@{vm.maxSeekDuration}"
            android:minHeight="6dp"
            android:paddingTop="24dp"
            android:progress="@{vm.currentSeekPosition}"
            android:progressDrawable="@drawable/progressbar_color"
            android:thumb="@null"
            android:visibility="visible"
            app:onSeekBarChangeListener="@{event}" />

    </androidx.coordinatorlayout.widget.CoordinatorLayout>

</layout>

这里又涉及到了几个 BindingAdapter,一个是 isPlaying:

在这里插入图片描述

一个是 mdIcon:

在这里插入图片描述

我们来完善下这两个 BindingAdapter

object IconBindingAdapter {

    // 调用播放暂停功能
    @JvmStatic
    @BindingAdapter(value = ["isPlaying"], requireAll = false)
    fun isPlaying(pauseView: PlayPauseView, isPlaying: Boolean) {
        if (isPlaying) {
            pauseView.play()
        } else {
            pauseView.pause()
        }
    }

    @JvmStatic
    @BindingAdapter(value = ["mdIcon"], requireAll = false)
    fun setIcon(view: MaterialIconView, iconValue: IconValue?) {
        view.setIcon(iconValue)
    }
}

还有一个是 imageUrl 和 placeHolder

在这里插入图片描述

object CommonBindingAdapter {

    @JvmStatic
    @BindingAdapter(value = ["imageUrl", "placeHolder"], requireAll = false)
    fun loadUrl(view: ImageView, url: String?, placeHolder: Drawable?) {
        Glide.with(view.context).load(url).placeholder(placeHolder).into(view)
    }
}

然后,我们在 Application 中初始化 PlayerManager

class App : Application(), ViewModelStoreOwner {

    override fun onCreate() {
        super.onCreate()

        // 这里必须初始化一下,是为了保证播放音乐管理类(PlayerManager.java) 不会为null,从而不引发空指针异常
        PlayerManager.instance.init(this)
    }
}

在这里插入图片描述

好了,播放能力就讲到这里吧~

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力~

西门子S7通信协议TSAP配置全攻略:从默认值到自定义修改(附各型号对照表) 本文深入解析西门子S7通信协议中TSAP配置的核心原理与实战方法。针对S7-200/300/400/1200/1500等主流PLC型号,提供了默认TSAP对照表,并详细阐述了如何根据机架号和插槽号计算或自定义Remote TSAP。文章结合常见连接失败案例,给出了系统化的排错流程,旨在帮助工程师快速打通S7通信链路,提升工业自动化现场调试效率。 阅读详情

相关推荐

5分钟搞定CST周期性结构仿真:共面波导色散曲线快速生成技巧

本文提供了一套在CST Studio Suite中快速仿真周期性结构并生成色散曲线的标准化流程。针对共面波导设计,重点解析了周期性边界条件的核心设置、本征模求解器(JDM算法)的优化技巧,以及通过参数扫描与后处理模板实现自动化,旨在帮助射频与微波工程师在5分钟内高效完成关键性能验证。

blue的专栏 1246

2024年安卓最新真香警告:即使不学 OS 进阶,也请务必收藏好该文!(1),2024年最新安卓开发面试题目

在这里小编整理了一份Android大厂常见面试题,和一些Android架构视频解析,都已整理成文档,全部都已打包好了,希望能够对大家有所帮助,在面试中能顺利通过。喜欢本文的话,不妨顺手给我点个小赞、评论区留言或者转发支持一下呗网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。需要这份系统化学习资料的朋友,可以戳这里获取一个人可以走的很快,但一群人才能走的更远!

2401_84563354的博客 783

Akamai移动端Sensor数据采集与BMP风控机制深度解析

本文深度解析了Akamai在移动端风控中的核心机制,重点阐述了Sensor数据如何通过采集陀螺仪、加速度计等硬件信息,结合设备指纹生成连续且符合物理规律的行为轨迹。详细剖析了数据从采集、加密校验到通过BMP风控三重验证(校验位、时间差、数据逻辑)的完整流程,并提供了从模拟优化到自建风控模型的实用应对思路。

quiet的专栏 284

如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(五)完结

这篇文章介绍了如何用Jetpack组件开发音乐播放器,重点讲解了协程替代RxJava实现网络请求的方法。主要内容包括: 协程实现:通过suspend关键字改造Retrofit API接口,简化线程切换 ViewModel中使用viewModelScope启动协程,实现主线程与异步线程的切换 展示了登录功能的协程实现示例 开始构建搜索模块,提供了fragment_search.xml布局代码片段 文章还列出了前期关于音乐播放器基础搭建、列表展示、播放功能、登录注册等系列文章链接。

IT_Android的博客 657

cuda安装时出现错误 Unable to locate package cuda-toolkit-12-4

cuda安装时出现错误 Unable to locate package cuda-toolkit-12-4

4865

LVGL v9的lv_scale控件实战:5分钟打造动态仪表盘(附完整代码)

本文详细介绍了LVGL v9中lv_scale控件的实战应用,通过5个关键步骤快速打造动态仪表盘。文章涵盖核心特性解析、代码实现、样式定制及性能优化,适用于工业HMI、智能家居等场景,并附完整代码示例。

gin88的博客 261

如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(四)登录注册

本文介绍了如何使用Jetpack组件和RxJava完善音乐播放器的用户注册功能。通过DataBinding实现布局与ViewModel的双向绑定,展示了注册页面的XML布局设计,包含用户名、密码输入框和注册按钮。重点介绍了RegisterViewModel的构建,使用MutableLiveData实现对用户输入数据的响应式处理,为后续登录功能开发奠定基础。这是音乐播放器系列教程的第四部分,前文已介绍基础搭建、音乐列表和播放功能实现。

IT_Android的博客 761

C#实战:如何用winmm.dll打造毫秒级高精度定时器(附完整代码)

本文详细介绍了在C#中利用winmm.dll实现毫秒级高精度定时器的完整方案。针对.NET内置定时器精度不足的问题,通过调用timeSetEvent等原生API,提供了超越系统时钟中断周期的精准定时能力。文章包含核心原理剖析、完整的封装类代码以及音视频处理、游戏开发等场景下的实战应用与性能调优指南。

ujm5678901的博客 690

2021年字节跳动+京东+美团面试总结!大牛最佳总结

大厂offer是每个技术人的追求? 我觉得每一个程序员的梦想大概就是能够找一份大厂的 Offer,我觉得这很正常,这并不是我们的饭后谈资而是每个技术人的追求。像阿里、腾讯、美团、字节跳动、京东等等的技术氛围与技术规范度还是要明显优于一些创业型公司/小公司,如果说能够在这样的公司锻炼几年,相信对自己能力的提升还是非常大的。但不是每个人都能够进入大厂的,这往往取决于我们的能力、学历、面试表现等因素。 现在面试大公司你说你没有 Android Jetpack、协程、 Jetpack Compose 、Flutte

m0_56228922的博客 219

【系统分析师】2026年上半年真题:论文及解题思路

四篇论文围绕系统与开发实践展开: 安全架构设计强调数据加密(国密算法)、零信任访问控制及容灾架构,以某医疗云平台为例,实现RTO<5分钟的高可用保障。 架构风格应用解析微服务/事件驱动等风格选型,结合电商平台案例,通过服务拆分与异步通信提升扩展性。 需求评审机制提出正式评审与走查结合的方法,在金融系统中减少30%返工,解决业务规则二义性问题。 LLM测试生成利用大模型从需求/代码生成用例,辅以人工校验,使测试效率提升50%,覆盖边缘场景。 各篇均遵循“背景-理论-实践”结构,突出

数据知道的博客 779

【考试大纲】中级多媒体应用设计师考试大纲

中级多媒体应用设计师考试大纲

邓邓子的博客 382

安卓开发快速学习!跟我一起手写EventBus吧,已拿offer

大厂offer是每个技术人的追求? 我觉得每一个程序员的梦想大概就是能够找一份大厂的 Offer,我觉得这很正常,这并不是我们的饭后谈资而是每个技术人的追求。像阿里、腾讯、美团、字节跳动、京东等等的技术氛围与技术规范度还是要明显优于一些创业型公司/小公司,如果说能够在这样的公司锻炼几年,相信对自己能力的提升还是非常大的。但不是每个人都能够进入大厂的,这往往取决于我们的能力、学历、面试表现等因素。 现在面试大公司你说你没有 Android Jetpack、协程、 Jetpack Compose 、Flutte

171

代谢组数据分析(二十一):通过MetaboAnalystR标准化构建sPLSDA预测模型

本文介绍了如何使用R语言中的MetaboAnalystR包和mixOmics包进行代谢组学数据的标准化和sPLSDA分析。MetaboAnalystR包用于数据预处理,包括数据初始化、清洗、补足、过滤和标准化等步骤,以确保数据质量。mixOmics包则用于sPLSDA分析,通过Lasso惩罚进行变量选择,并建立分类模型。文章详细说明了安装和加载相关R包的步骤,并提供了数据导入和预处理的代码示例。最后,强调了数据预处理的重要性,以确保后续分析的准确性和可靠性。

专注生信领域 2426

Android 14+ 模拟器抓包实录:把 Burp CA 装成系统 CA,让 targetSdk≥34 的 App 乖乖解密

本文记录了在 Android 14+ 环境下,通过将 Burp CA 安装为系统信任锚,成功抓取 targetSdk=35 App 的 HTTPS 明文流量的完整实操过程。针对“浏览器可用、App 不可用”的典型现象,精准识别出根本原因是系统级证书信任策略变更(用户安装的 CA 被屏蔽),而非 SSL Pinning。仅通过 adb 命令将 CA 推送至系统证书存储并重启生效,无需 Frida、Objection 或修改 APK,实现零侵入解密。全文命令均基于 Windows + MuMu 模拟器环境实测通

JIANqiao19931029的博客 341

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

具身智能 2026:启元人形机器人开发者生态的“安卓时刻”来了吗

一个贯穿全文的问题:人形机器人开发者生态,是否正从「封闭垂直」走向「开源平台化」?本文以开源底座的公开数据为标尺,盘点 SDK、URDF、仿真与技能分发的开放程度,并落回启元机器人的开源路径。所有关键数据均标注来源,文末附社区与学堂链接。

在北风吹起的时候加入我们的队伍 在秋雨到来的时候成为彼此的温暖 743

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

android-ad-cleaner:android免 Root 批量关闭系统开屏与营销推送

橘子,计算机视觉方向工程师,深耕 AIGC 与多媒体处理领域,专注技术实用化落地。本文为原创内容,转载请注明来源与作者。优快云 首发 | 版权所有 © 2026。

下载资源或查看教程前,请务必阅读置顶的《免责声明》 498

Android 14 SELinux策略深度解析

init 的全局公开约束(如 neverallow init execute_no_trans,放 public 才能约束所有分区的规则)这就是"哪些可见"的强制机制。alps_s0(MTK 工程)的 device/mediatek/sepolicy/basic/ 提供。所有分区(system/system_ext/product/vendor)都可引用。init的公开约束、跨分区共用的 type/attribute/neverallow。——vendor 的 .cil 只能引用 plat 的。

新时代农民工 102

Android 开发入门实践(二):Kotlin 空安全,从可空类型到安全访问

延续 NewsApp 项目中的新闻标题练习,记录 Kotlin 安全调用。

Easyhood的博客 276

Android 模块化收口:能力组合框架、手写 LifecycleOwner 与热修复共存

二十多个模块业务高度相似却各有差异,核心矛盾在于“统一与灵活”的平衡。通过角色接口组合(如IProjectAware、IVoiceAware)实现能力按需注入,新增功能零侵入;用模板方法锁定生命周期流程,确保顺序一致;结合@CallSuper与工厂方法,既约束关键逻辑,又保留子类自由。最终实现:骨架统一、填空自由,管得住业务,又不侵入业务。

mmsx990的博客 395

Android Message源码阅读

本文梳理了Android Handler机制中Message的核心设计:链表结构、消息复用池(sPool)及回收机制,重点解析了异步消息与同步屏障(SyncBarrier)的协同机制。当主线程需优先处理UI绘制等高优先级任务时,系统通过postSyncBarrier插入屏障,使Looper跳过普通同步消息,优先执行标记为异步的UI渲染任务,确保帧率流畅。该机制是保障Android界面响应性的关键技术之一。

u012116089的博客 183

安卓部署Linux性能实测:一颗移动处理器,能否胜任日常 Linux 开发?

本文通过在一加Ace5上部署NimoteCode的ARM64 Linux环境,测试了其在CoreMark、SQLite编译及并行构建中的表现。结果显示,尽管单核性能约2.34GHz,多核扩展比达6.15×,但优化编译耗时仍高达35秒,反映移动处理器虽具备一定开发能力,却受限于文件系统开销与运行环境(如PRoot)。实际开发中,非优化构建效率较高,而并行编译可显著提速。结论:安卓手机可作为轻量级开发辅助工具,但无法替代桌面环境,真正价值在于便携性与即时响应,而非全功能替代。

一个不知名码农的专栏 282

Android中Gradle网络下载超时、更改存放位置(解决方案)

在国内开发Android应用,需要使用到一些外网才能正常访问的环境依赖。除了挂梯子,这里再提供几个常见问题的配置方案,可以已定程度减少下载超时问题。但是要注意,不同的开发环境、网络环境、时间段,出现的问题都是不可控的,遇到配置问题,建议保持耐心,多做尝试。

indeedes的博客 122

云手机私有化部署完整方案:基于ARM服务器搭建私有安卓云集群

ARM 服务器是私有化安卓云集群最优硬件底座,原生架构匹配 Android,高密度、低损耗,非常适合企业内网场景。整套私有化方案的核心,从来不是 “跑起来安卓”,而是隔离可控、可审计、可运维、可扩容。两条路线各有取舍:开源自建灵活、门槛高;商用私有化交付开箱即用、安全能力完备,是当前企业生产环境主流方案。六边云手机私有化方案,补齐了自建方案在驱动适配、管控平台、审计合规、信创适配上的短板,适合对数据主权、内网隔离有强需求的政企、自研 App、垂直行业客户。

a598652的博客 308

Android 开发入门实践(九):把新闻数据与页面状态交给 ViewModel

本文记录第110—117课学习成果:将页面标题、新闻列表及选中编号逐步迁移至NewsViewModel,通过ViewModelProvider获取实例并读取数据。页面调用ViewModel方法处理选择与返回,实现状态管理解耦。过程中纠正了对Activity重建机制的误解,并厘清mutableStateOf的可观察性与ViewModel对象生命周期的区别,深化了对Compose状态更新机制的理解。

Easyhood的博客 203

Android 17 全栈性能分析关键字与源码手册

FrameTimeline 找异常帧,ATrace 找阶段,thread_state 找等待类型,ftrace 找内核原因,Simpleperf 找 CPU 热点,驱动源码找最终触发条件;任何根因结论都必须回到同一对象、同一时间窗口和同一条依赖链。

weixin_43228946的博客 362

CTF 命令执行题学习笔记:终端特殊字符 $IFS 与 %09 的用法(HelloCTF RCE-labs)

空格被过滤时:用%09(TAB)或$IFS代替空格关键词被过滤时:用通配符?或字符类[f]拼出被禁的词,让 WAF 在原始字符串里看不到它,shell 在执行时再展开这类技巧在后续的命令执行题里会反复用到,建议亲手在靶机上敲一遍感受一下。

YY888888_的博客 538

Android 顶点上传:Heap 到 Direct 的零拷贝,省的是 JNI 次数

一帧数十万次putFloat的性能瓶颈源于频繁跨JNI边界,每次开销远超传输字节数。通过将写入与上传解耦:用堆内ByteBuffer高速写入,最后一次性put(writeBuf)批量传至堆外DirectByteBuffer,将数百万次边界调用压缩为一次,实现近10倍性能提升。关键在于复用堆外缓冲区、统一端序,并结合“更新-绘制”分离,仅在数据变更时上传顶点,平移缩放时零开销重绘,彻底释放CPU瓶颈。

mmsx990的博客 474
上一篇: 如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(二)音乐列表
下一篇: 如何应对 Android 面试官 -> 运用 Jetpack 写一个音乐播放器(四)登录注册
老A说
博客等级 码龄14年 1181粉丝 85原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值