• Android——一个简单的音乐APP(二)


    效果视频

    一个简单的音乐APP

    前言

    一个简单的音乐APP(第一篇)
    第二版基于第一版新增了以下功能:

    1. 音乐下载
    2. 音乐离线播放
    3. mLog视频播放
    4. 个人信息
    5. 音乐信息
    6. 删除本地音乐

    基于第一版也稍稍有些变动,废弃了二维码登录功能(依旧能登录,但有少部分功能无法联动使用),部分代码结构有变动

    音乐下载

    音乐下载效果图

    实习步骤&思想

    1. 将所有选中的下载项与数据库中的子项进行匹配,如果存在相同的则不添加到下载队列中
    2. 下载队列采用先入先出,如果某一个音乐获取的音乐源为空,则代表需要会员或者需要购买才能获取下载链接(因为我的账号为非会员,故需要会员下载的歌曲无法进行下载,有会员的账户可以正常获取下载链接);如果为获取链接为空,则从下载队列之中删除此项,以及删除数据库对应项、本地项
    3. 正常获取的音乐下载链接,则通过FileDownloader开源库进行下载,然后通过EventBus跨模块进行下载进度实时更新
    4. 断点续传:如果有未下载完成的下载项,被强制退出APP,当下次重新进去app时,自动断电续传,完成下载

    添加到下载队列

    下载选中以及下载弹窗提醒就跳过,先讲解添加到下载队列;先取出数据库实例,如果存在相同项则不添加到下载队列中,反之添加;此数据库以歌曲id歌曲名称作为主键,防止重复

    private fun isExistIdentity(bean: SongBean): Boolean{
            val dao = PlayListDataBase.getDBInstance().downloadDao()
            val key = "${bean.songId}${bean.songName}"
            val isExist: DownloadBean = dao.findByKey(key)
            /**
             * 如果不为空,则代表存在,则不再进行重复下载*/
            if (isExist != null) return true
    
            val downloadBean = DownloadBean(key,bean.songName,bean.songId,bean.singerName,"",bean.albumCover,"","",0,0,false,false,"")
    
            /*update room*/
            dao.insert(downloadBean)
    
            /*update binder*/
            DownloadBinder.downloadList.add(downloadBean)
            return false
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    单任务下载
              val flag = isExistIdentity(bean)
                    if (flag){
                        ToastUtil.setFailToast(this@SongListActivity,"已存在相同下载项,请勿重复添加!")
                    }else{
                        DownloadBinder.download()
                        ToastUtil.setSuccessToast(this@SongListActivity,"已添加到下载队列中!")
                    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    多任务下载
              var count = 0
                for (i in songBeanList.indices){
                    if (songBeanList[i].isSelect){
                        var flag = isExistIdentity(songBeanList[i])
                        if (flag)count++
                    }
                }
                if (count> 0)ToastUtil.setFailToast(this@SongListActivity,"部分音乐可能已经存在下载队列之后,请勿重复添加!")
                DownloadBinder.download()
                downloadPop.dismiss()
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    音乐下载

    下载class为一个Service服务的单例Binder实现类

    获取音乐下载源

    通过音乐id获取其下载链接,然后回调给调用放,如上述表示所言,非会员账号,下载部分歌曲无法获取音乐下载源,故需要进行字符判断

     /**
         * 获取音乐下载源*/
        private fun startDownload(bean: DownloadBean,callback: IGenerallyInfo) {
            val url = HttpUtil.getDownloadURL(bean.songId.toString())
    
            HttpUtil.getGenerallyInfo(url, object : IGenerallyInfo {
                override fun onRespond(json: String?) {
                    val downloadUrl = JSONObject(json.toString()).getJSONObject("data").getString("url").toString()
                    if (!TextUtils.isEmpty(downloadUrl) && downloadUrl != "null") {
                        callback.onRespond(downloadUrl)
                    } else {
                        callback.onFailed("此音乐需要开通才能进行下载!")
                    }
                }
    
                override fun onFailed(e: String?) {
                    callback.onFailed(e.toString())
                }
            })
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    创建本地路径

    对下载队列进行大小判断,防止下标越界异常,然后通过创建本地文件,对正常下载源进行下载,非正常音乐源进行删除(下载队列删除、数据库删除)

    fun download() {
            if (downloadList.size == 0) return
    
            if (current >= downloadList.size) {
                ToastUtil.setSuccessToast(HomePageActivity.MA, "下载完成!")
                current = 0
                downloadList.clear()
                return
            }
    
            val name = downloadList[current].songId.toString() + downloadList[current].songName + ".mp3"
            val path = locationDir + File.separator + name
            downloadList[current].path = path
    
            startDownload(downloadList[current],object :IGenerallyInfo{
                override fun onRespond(json: String?) {
                    downloadList[current].url = json.toString()
                    bindDownload(path, downloadList[current].url)
                }
    
                override fun onFailed(e: String?) {
                    delete(downloadList[current])
                    EventBus.getDefault().postSticky(DownloadingBean(111, "", percentage, e.toString(), null))
                    download()
                }
            })
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    创建目录
    public String mainCatalogue() {
            String path = "EasyMusicDownload";
            File dir = new File(BaseApplication.getContext().getCacheDir(), path);
            File movie = BaseApplication.getContext().getExternalFilesDir(Environment.DIRECTORY_MUSIC);
            if (movie != null) {
                dir = new File(movie, path);
            }
            if (!dir.exists()) {
                dir.mkdirs();
            }
            return dir.toString();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    开始音乐下载

    在不同的回调中进行不同的处理,通过EventBus进行下载进度跨模块更新,其中需要关注的只有progresscompleted两个回调,前者需要不断进行下载进度回调,后面在下载完成之后需要改变下载项,并且更新数据库相对应实例,方便下载完成显示界面正常;而warn回调则无需进行太多关注,因为我们添加到下载队列之前已经判断过是否存在重复项啦,在代码正常情况下,无需担心

    private fun bindDownload(path:String,url:String){
            FileDownloader.getImpl()
                .create(url)
                .setPath(path, false)
                .setListener(object : FileDownloadListener() {
                    override fun pending(task: BaseDownloadTask, soFarBytes: Int, totalBytes: Int) {
                        //已经进入下载队列,正在等待下载
                        EventBus.getDefault().postSticky(DownloadingBean(task.status, "", 0, "", null))
                    }
    
                    override fun started(task: BaseDownloadTask?) {
                        //结束了pending,并且开始当前任务的Runnable
                        if (task != null) {
                            EventBus.getDefault().postSticky(DownloadingBean(task.status, "", 0, "", null))
                        }
                    }
    
                    override fun connected(task: BaseDownloadTask?, etag: String?, isContinue: Boolean, soFarBytes: Int, totalBytes: Int) {
                        //已经连接上
                    }
    
                    override fun progress(task: BaseDownloadTask, soFarBytes: Int, totalBytes: Int) {
                        //soFarBytes:已下载字节数
                        //totalBytes:文件总字节数
                        Log.d("downloadTAG", "running:${task.speed}")
                        percentage = ((soFarBytes * 1.0 / totalBytes) * 100).toInt()
                        EventBus.getDefault().postSticky(DownloadingBean(task.status, "${remainDigit(task.speed / 8.0)}KB/s", percentage, "", downloadList[current]))
                    }
    
                    override fun completed(task: BaseDownloadTask) {
                        //status = -3
    //                /**
    //                 * 除2个1024的到大小MB
    //                 * 记得最后保留一位小数*/
                        val primary = "${downloadList[current].songId}${downloadList[current].songName}"
    //
    //                /**
    //                 * 下载完成之后,更新数据库字段*/
                        PlayListDataBase.getDBInstance().downloadDao().updateComplete(primary, true)
                        PlayListDataBase.getDBInstance().downloadDao().updatePath(primary, task.path)
                        PlayListDataBase.getDBInstance().downloadDao().updateUrl(primary, task.url)
                        val size = remainDigit(task.smallFileTotalBytes * 1.0 / 1024 / 1024)
                        PlayListDataBase.getDBInstance().downloadDao().updateSize(primary, "${size}MB")
                        EventBus.getDefault().postSticky(DownloadingBean(task.status, "", percentage, "", downloadList[current]))
    
                        current++
                        download()
                    }
    
                    override fun paused(task: BaseDownloadTask, soFarBytes: Int, totalBytes: Int) {
                        //callback.pause(task)
                        Log.d("downloadTAG", "pause:${task.status}")
                        EventBus.getDefault().postSticky(DownloadingBean(task.status, "", percentage, "", downloadList[current]))
                    }
    
                    override fun error(task: BaseDownloadTask, e: Throwable) {
                        // callback.failed(task,e.message)
                        //error = -1
                        Log.d("downloadTAG", "failed:${task.status}")
                        Log.d("downloadTAG", "failed:${e.message}")
                        EventBus.getDefault().postSticky(DownloadingBean(task.status, "", percentage, e.message!!, downloadList[current]))
    
                        download()
                    }
    
                    /**
                     * 存在相同项目*/
                    override fun warn(task: BaseDownloadTask) {
                        // callback.exist(task)
                        /**
                         * 不会进入此处
                         * 因为外面已经判断过重复项*/
                        Log.d("downloadTAG", "exist:${task.status}")
                        EventBus.getDefault().postSticky(DownloadingBean(111, "", percentage, "", downloadList[current]))
    
                        //current++
                        download()
                    }
                }).start()
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    下载进度回调
     /**
         * 下载回调监听*/
        @Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
        fun onEvent(bean: DownloadingBean){
            update(bean)
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    下载进度更新

    下载进度总共有四种状态:默认状态(灰色)、绿色(当前下载项)、黄色(下载暂停)、红色(下载异常);为了提醒四种状态,就没有贴代码,而是放了图片,因为旁边有颜色提示,更加明了
    在这里插入图片描述
    对应上面的状态进行不同的处理

        /**
         * currentItem等于-1代表没有相同项
         * currentItem等于-2代表状态改变,比如下载完成,初始化-2*/
        private fun update(bean: DownloadingBean){
            when(bean.status){
                pending_start->{}
                running->{
                    updateItem(bean,1)
                }
                completed->{
                    if (currentItem != -1 && currentItem != -2){
                        adapter.deleteCompletedItem(currentItem)
                        currentItem = -2
                        EventBus.getDefault().postSticky(DownloadCompleteBean(true))
                    }
                }
                failed->{updateItem(bean,3)}
                error->{
                    ToastUtil.setFailToast(HomePageActivity.MA,bean.error)
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    寻找当前下载项

    -2(默认)下载完成,需要重新寻找下载项,-1代表下载队列已经全部下载完成,已经没有匹配项;反之,为当前下载项,进行进度更新

        private fun updateItem(bean: DownloadingBean,status: Int){
            when (currentItem) {
                -2 -> {
                    searchItem(bean)
                }
                -1 -> {
                    //没有相同项
                }
                else -> {
                    downloadList[currentItem].progress = bean.percentage
                    downloadList[currentItem].speed = bean.speed
                    adapter.notifyItemChanged(currentItem,status)
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    离线播放

    离线播放较为简单,因为之前在线播放使用的都是同一个组件,在线播放传入音乐url,而离线播放传入本地音乐的绝对地址即可,故而没有任何变化。

    mLog视频播放

    mLog视频播放效果图
    样式

    原理与MV视频播放一致,只是界面发生些许变化;因为重写了GSYVideoPlayer的样式,此处就介绍一下样式。

    进度、声音、亮度
    声音
    亮度、声音、进度类似,就以声音为例;下列代码只是显示了`Dialog`以及相关属性,具体通过滑动屏幕计算音量大小,然后改变系统音量大小由下列代码实现(部分代码)
               deltaY = -deltaY;
                int max = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
                int deltaV = (int) (max * deltaY * 3 / curHeight);
                mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mGestureDownVolume + deltaV, 0);
                int volumePercent = (int) (mGestureDownVolume * 100 / max + deltaY * 3 * 100 / curHeight);
                showVolumeDialog(-deltaY, volumePercent);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    @Override
        protected void showVolumeDialog(float deltaY, int volumePercent) {
            WindowManager.LayoutParams localLayoutParams = null;
            if (mVolumeDialog == null) {
                View localView = LayoutInflater.from(getActivityContext()).inflate(getVolumeLayoutId(), null);
                if (localView.findViewById(getVolumeProgressId()) instanceof ProgressBar) {
                    mDialogVolumeProgressBar = ((ProgressBar) localView.findViewById(getVolumeProgressId()));
                    if (mVolumeProgressDrawable != null && mDialogVolumeProgressBar != null) {
                        mDialogVolumeProgressBar.setProgressDrawable(mVolumeProgressDrawable);
                    }
                }
                mVolumeDialog = new Dialog(getActivityContext(), R.style.video_style_dialog_progress);
                //mVolumeDialog = new Dialog(getActivityContext());
                mVolumeDialog.setContentView(localView);
                mVolumeDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
                mVolumeDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
                mVolumeDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE);
                //mVolumeDialog.getWindow().setLayout(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                mVolumeDialog.getWindow().setLayout(380, 50);
                localLayoutParams = mVolumeDialog.getWindow().getAttributes();
                localLayoutParams.gravity = Gravity.CENTER;
                localLayoutParams.width = getWidth();
                localLayoutParams.height = getHeight();
            }
            if (!mVolumeDialog.isShowing()) {
                mVolumeDialog.show();
                mVolumeDialog.getWindow().setAttributes(localLayoutParams);
            }
            if (mDialogVolumeProgressBar != null) {
                mDialogVolumeProgressBar.setProgress(volumePercent);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    个人信息

    个人信息效果图
    行政代码解析

    个人信息界面就是普通的数据展示,此处讲解一下地区,因为接口返回的是省和市的行政代码,然后找了一个XML 全国省市区+区行政代码的文件,通过PULL方式进行解析,然后展示,此处讲解这个

    XML数据格式

    以安徽省-安庆市为例,此文件没有Text内容,所有信息全在标头上

    
        
          
          
          
          
          
          
          
          
          
          
          
          
        
        
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    代码

    这是为解析这个文件写的一个解析类,首先获取这个资源文件,这个文件可以存储在xml目录下、raw目录下、assets目录下都可以;前者与后两者有稍微不同,但解析步骤一样,只是获取资源文件方式不同。我这个采用的是一个嵌套类,

    • 省数据类中包括:省名称、省行政代码、城市集合
    • 城市数据类中包括:城市名称、城市行政代码、区县集合
    • 区县数据类中包括:区县名称、区县行政代码(区县在该文件中没有行政代码,所有代码均为该市行政代码)
    object XMLParserUtil {
        private val provinceTag = "province"
        private val cityTag = "city"
        private val countyTag = "district"
        private val NAME = "name"
        private val CODE = "zipcode"
        private lateinit var pullParser : XmlPullParser
        private lateinit var provinceList:MutableList
    
        init {
            provinceList = ArrayList()
            create()
            resolve()
        }
    
    
        private fun create(){
            val factory = XmlPullParserFactory.newInstance();
            pullParser = factory.newPullParser();
            val inputStream = BaseApplication.context.resources.openRawResource(R.raw.region);
            pullParser.setInput(InputStreamReader(inputStream));
        }
        
        private fun resolve(){
            var province: ProvinceBean = ProvinceBean()
            var currencyProvince = false
            var currentCityFlag = false
            var currentCity: Int = -1
            var currentCounty: Int = -1
            try {
            var type = pullParser.eventType
            while (type != XmlPullParser.END_DOCUMENT){
                when(type){
                    XmlPullParser.START_DOCUMENT-> provinceList = ArrayList()
    
                    XmlPullParser.START_TAG->{
                        when (pullParser.name) {
                            provinceTag -> {
                                /**
                                 * 省份*/
                                province = ProvinceBean()
                                currencyProvince = true
    
                                province.provinceName = pullParser.getAttributeValue(null, NAME)
                                province.provinceCode = pullParser.getAttributeValue(null, CODE).toInt()
                            }
                            cityTag -> {
                                /**
                                 * 城市*/
                                if (currencyProvince){
                                    currentCity = -1
                                    currencyProvince = false
                                    province.cityList = ArrayList()
                                }
                                currentCityFlag = true
                                currentCity++
                                val bean = CityBean()
                                bean.cityName =  pullParser.getAttributeValue(null, NAME)
                                bean.cityCode = pullParser.getAttributeValue(null, CODE).toInt()
                                province.cityList.add(bean)
                            }
                            countyTag -> {
                                /**
                                 * 区县*/
                                if (currentCityFlag){
                                    currentCounty = -1
                                    currentCityFlag = false
                                    province.cityList[currentCity].countyList = ArrayList()
                                }
                                currentCounty++
                                val bean = CountyBean()
                                bean.countyName = pullParser.getAttributeValue(null, NAME)
                                bean.countyCode = pullParser.getAttributeValue(null, CODE).toInt()
                                province.cityList[currentCity].countyList.add(bean)
                            }
                        }
                    }
    
                    XmlPullParser.END_TAG->{
                        val provinceName = pullParser.name
                        if (pullParser.name == provinceTag) provinceList.add(province)
                    }
                    XmlPullParser.END_DOCUMENT->{}
                }
                type = pullParser.next()
            }
            }catch (e: XmlPullParserException){
                e.printStackTrace()
            }catch (e:IOException){
                e.printStackTrace()
            }catch (e:NullPointerException){
                e.printStackTrace()
            }
        }
    
        /**
         * 根据省/城市名获取行政代码*/
        private fun getCodeByName(){
    
        }
    
        /**
         * 查询省级*/
        fun getProvinceNameByCode_B(code: Int):ProvinceBean?{
            val province = searchProvince(code)
            province?.let { return province }
            return null
        }
    
        fun getProvinceNameByCode_S(code: Int):String?{
            val province = searchProvince(code)
            province?.let { province.provinceName }
            return null
        }
    
        /**
         * 根据行政代码获取省/城市名
         * 模糊搜索:传入省或者城市编码,具体类型不明
         * 查找某个城市的名称,首先通过找到该省然后在进一步通过城市行政代码查询该市名称*/
    
        /**
         * 返回组合类型
         * 例如:湖南-长沙*/
        fun getCityNameByCode(provinceCode: Int,cityCode: Int,split: String): String{
            val builder = StringBuilder()
            val province = searchProvince(provinceCode)
            if (province != null){
                builder.append(province.provinceName).append(split)
                val city = searchCity(cityCode, province)
                city?.let {
                    builder.append(city.cityName)
                    return builder.toString()
                }
            }
            return ""
        }
    
        /**
         * 单独返回城市名称*/
        fun getCityNameByCode(province:ProvinceBean,cityCode: Int): String{
            val city = searchCity(cityCode, province)
            city?.let { return city.cityName }
            return ""
        }
    
        private fun searchProvince(code: Int): ProvinceBean?{
            if (provinceList.size == 0)return null
            for (i in 0 until provinceList.size){
                if (code == provinceList[i].provinceCode) return provinceList[i]
            }
            return null
        }
    
        private fun searchCity(code: Int,bean: ProvinceBean):CityBean?{
            if (bean.cityList.size == 0) return null
            for (i in 0 until bean.cityList.size){
                if (code == bean.cityList[i].cityCode) return bean.cityList[i]
            }
            return null
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
  • 相关阅读:
    MySQL数据库的回滚rollback是怎么做到的?
    【Android】DataBinding 最全使用解析
    有关java连接数据库报错的解决方案
    go编程中接口(interface)用法
    刷新AI作图速度,最快的开源Stable Diffusion出炉
    Apache Doris (四十五): Doris数据更新与删除 - Sequence 列
    退运险业务及系统架构演进史
    玩转数据库索引
    【算法篇】动态规划(二)
    问题排查利器:Linux 原生跟踪工具 Ftrace 必知必会
  • 原文地址:https://blog.csdn.net/News53231323/article/details/127435871