浅谈Android版本更新

2023-10-29

关于本文DownloadManager版本更新的源码链接详见我的开源项目AppUpdate

前言

版本升级对于app来讲已经是非常常见的功能了,每次项目的版本迭代、新功能的开发都需要下载更新新版本,通过安装新版本来实现我们的迭代。当然除了这种方式,实际上也有热更新与热修复的存在,无需安装的情况下实现版本的迭代,而且很多大型的项目在有了大量用户的积累后也大都采取了灰度发布的功能,先小范围升级试用,在正式推向市场。今天我只想单纯来讲讲基于系统自带的DownloadManager来实现的下载更新。

万能流程图

画图不易,这张流程图几乎包含了app检查更新的所有涉及到的流程,像流程图中进度框、下载失败的弹框,MD5校验个人觉得可以不需要,一般像DownloadManager来实现下载更新只需要在后台下载,下载完成用系统的Notification进行通知即可,然后自动弹出安装界面,这是个标准的流程。

涉及知识归纳

  • DownloadManager系统下载服务的相关api及使用。
  • Android M 运行时权限的动态申请,主要涉及读写存储卡权限。
  • Android N 关于文件的访问权限,不能以file://xxx格式的Uri来访问文件,需要使用FileProvider,Uri格式为content://xxx
  • Android O 关于未知来源应用的权限申请。
  • Android Q 增加沙箱并改变了应用程序访问设备外部存储上文件的方式,而且不可以在内部存储肆意的构建自己的目录
  • 文件MD5校验,防止apk下载被拦截篡改及验证apk文件的完整性。

DownloadManager介绍及使用

介绍

DownloadManager下载管理器是一种处理长时间运行的HTTP下载的系统服务。客户端可以请求将URI下载到特定目标文件。下载管理器将在后台进行下载,负责HTTP交互并在发生故障或跨连接更改和系统重新启动后重试下载。翻译过来的始终感觉不好,以下是官方的原话 (官方传送门

The download manager is a system service that handles long-running HTTP downloads. Clients may request that a URI be downloaded to a particular destination file. The download manager will conduct the download in the background, taking care of HTTP interactions and retrying downloads after failures or across connectivity changes and system reboots.

Apps that request downloads through this API should register a broadcast receiver for ACTION_NOTIFICATION_CLICKED to appropriately handle when the user clicks on a running download in a notification or from the downloads UI.

Note that the application must have the Manifest.permission.INTERNET permission to use this class.

从概念上都已经明确说明了DownloadManager系统下载服务的优越性:
1.可以长时间在后台运行下载
2.可以指定任意的下载路径,也可以支持Android Q
3.下载过程中遇见问题或者更改网络会重试下载,断点续传
4.原生系统下载服务,不依赖第三方,兼容性和稳定性无疑最好
5.默认已经帮你封装好了系统栏通知、wifi/移动网络/漫游等等下载限制

下载核心的API

类/常量/方法 介绍
DownloadManager.Query 主要用来在下载的过程中查询过滤,比如下载状态、进度等
DownloadManager.Request 下载服务一些配置、下载地址、下载路径、通知栏配置、网络限制、媒体类型等
ACTION_DOWNLOAD_COMPLETE 下载完成后,由下载管理器发送的广播意图操作
ACTION_NOTIFICATION_CLICKED 当用户从系统通知或下载UI单击正在运行的下载时,下载管理器发送广播意图操作
ACTION_VIEW_DOWNLOADS 启动活动以显示所有下载的意图操作,说白了手机系统的下载管理界面
COLUMN_BYTES_DOWNLOADED_SO_FAR 目前下载的字节数,需要下载进度条的用得到
COLUMN_TOTAL_SIZE_BYTES 下载文件的总大小,单位为字节,需要下载进度条的用得到
COLUMN_LOCAL_URI 下载的文件将存储在Uri中,注意:N之前是file://xxx,N之后是content://xxx
EXTRA_DOWNLOAD_ID 在广播ACTION_DOWNLOAD_COMPLETE中,可拿到download_id
COLUMN_REASON 提供有关下载状态的更多详细信息
COLUMN_STATUS 当前的下载状态,通过DownloadManager.Query来查询
STATUS_PENDING 下载开始
STATUS_RUNNING 下载进行中
STATUS_PAUSED 下载暂停,这里会等待重试,注意这是断点续传,暂停原因可以通过COLUMN_REASON去查
STATUS_SUCCESSFUL 下载成功
STATUS_FAILED 下载失败,这里的失败不会重试的,原因可以通过COLUMN_REASON去查
enqueue(DownloadManager.Request request) 开启一个下载服务
getMaxBytesOverMobile(Context context) 返回手机移动网络限定下载的最大值
getMimeTypeForDownloadedFile(long id) 通过download_id查询下载文件的媒体类型,也就是格式
getRecommendedMaxBytesOverMobile(Context context) 获取建议的移动网络下载的大小
getUriForDownloadedFile(long id) 如果文件下载成功,返回文件的Uri
openDownloadedFile(long id) 打开下载的文件,读文件
query(DownloadManager.Query query) 下载查询
remove(long… ids) 取消下载并从下载管理器中删除文件

以上便是DownloadManager下载使用到的核心api了,基本上满足一个正常的下载了,当然并没有全部罗列出来,像下载暂停和下载失败关于COLUMN_REASON的描述 还有很多,就不罗列出来了,下面看看下载更新的代码片段:

  • 下载核心代码
// 获取下载管理器
downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
clearCurrentTask();
// 下载地址如果为null,抛出异常
String downloadUrl = Objects.requireNonNull(appUpdate.getNewVersionUrl());
Uri uri = Uri.parse(downloadUrl);
DownloadManager.Request request = new DownloadManager.Request(uri);
// 下载中和下载完成显示通知栏
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
if (TextUtils.isEmpty(appUpdate.getSavePath())) {
//使用系统默认的下载路径 此处为应用内 /android/data/packages ,所以兼容7.0
request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, context.getPackageName() + ".apk");
deleteApkFile(Objects.requireNonNull(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS + File.separator + context.getPackageName() + ".apk")));
} else {
// 自定义的下载目录,注意这是涉及到android Q的存储权限,建议不要用getExternalStorageDirectory()
request.setDestinationInExternalFilesDir(context, appUpdate.getSavePath(), context.getPackageName() + ".apk");
deleteApkFile(Objects.requireNonNull(context.getExternalFilesDir(appUpdate.getSavePath() + File.separator + context.getPackageName() + ".apk")));
}
// 部分机型(暂时发现Nexus 6P)无法下载,猜测原因为默认下载通过计量网络连接造成的,通过动态判断一下
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if(connectivityManager !=null){
boolean activeNetworkMetered = connectivityManager.isActiveNetworkMetered();
request.setAllowedOverMetered(activeNetworkMetered);
}
// 设置通知栏的标题
request.setTitle(getAppName());
// 设置通知栏的描述
request.setDescription("正在下载中...");
// 设置媒体类型为apk文件
request.setMimeType("application/vnd.android.package-archive");
// 开启下载,返回下载id
lastDownloadId = downloadManager.enqueue(request);
// 如需要进度及下载状态,增加下载监听
if (!appUpdate.getIsSlentMode()) {
DownloadHandler downloadHandler = new DownloadHandler(this);
downloadObserver = new DownloadObserver(downloadHandler, downloadManager, lastDownloadId);
context.getContentResolver().registerContentObserver(Uri.parse("content://downloads/my_downloads"), true, downloadObserver);
}
  • 下载进度的监听
    默认采取的是系统的ContentObserver对于本地下载的文件变化监听进度,也可以通过开启定时器每隔一定的时间去查询当前的下载进度。
  /**
     * 检查下载的状态
     */
    private void queryDownloadStatus() {
        // Java 7 新的 try-with-resources ,凡是实现了AutoCloseable接口的可自动close(),所以此处不需要手动cursor.close()
        try (Cursor cursor = downloadManager.query(query)) {
            if (cursor != null && cursor.moveToNext()) {
                int status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS));
                long totalSize = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
                long currentSize = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
                // 当前进度
                int mProgress;
                if (totalSize != 0) {
                    mProgress = (int) ((currentSize * 100) / totalSize);
                } else {
                    mProgress = 0;
                }
                Log.d(TAG, String.valueOf(mProgress));
                switch (status) {
                    case DownloadManager.STATUS_PAUSED:
                        // 下载暂停
                        handler.sendEmptyMessage(DownloadManager.STATUS_PAUSED);
                        Log.d(TAG, "STATUS_PAUSED");
                        break;
                    case DownloadManager.STATUS_PENDING:
                        // 开始下载
                        handler.sendEmptyMessage(DownloadManager.STATUS_PENDING);
                        Log.d(TAG, "STATUS_PENDING");
                        break;
                    case DownloadManager.STATUS_RUNNING:
                        // 正在下载,不做任何事情
                        Message message = Message.obtain();
                        message.what = DownloadManager.STATUS_RUNNING;
                        message.arg1 = mProgress;
                        handler.sendMessage(message);
                        Log.d(TAG, "STATUS_RUNNING");
                        break;
                    case DownloadManager.STATUS_SUCCESSFUL:
                        if (!isEnd) {
                            // 完成
                            handler.sendEmptyMessage(DownloadManager.STATUS_SUCCESSFUL);
                            Log.d(TAG, "STATUS_SUCCESSFUL");
                        }
                        isEnd = true;
                        break;
                    case DownloadManager.STATUS_FAILED:
                        if (!isEnd) {
                            handler.sendEmptyMessage(DownloadManager.STATUS_FAILED);
                            Log.d(TAG, "STATUS_FAILED");
                        }
                        isEnd = true;
                        break;
                    default:
                        break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Android M 运行时权限

android 6.0 版本引入了一种新的权限模式,如今,用户可直接在运行时管理应用权限。这种模式让用户能够更好地了解和控制权限,同时为应用开发者精简了安装和自动更新过程。用户可为所安装的各个应用分别授予或撤销权限。

对于以 Android 6.0(API级别23)或更高版本为目标平台的应用,请务必在运行时检查和请求权限。要确定您的应用是否已被授予权限,请调用新增的checkSelfPermission()方法。要请求权限,请调用新增的requestPermissions()方法。即使您的应用并不以Android6.0(API级别23)为目标平台,您也应该在新权限模式下测试您的应用.官方传送门

由于下载需要读写文件,Android M 需要动态申请运行时权限,关于如何查看运行时权限,可以通过AndroidStudio的Terminal终端执行如下命令:

  • 按组列出权限和状态:

$ adb shell pm list permissions -d -g

  • 授予或撤销一项或多项权限:

$ adb shell pm [grant|revoke] …

  • 列出所有权限:

$ adb shell pm list permissions -s

M运行时权限请求代码片段:

  /**
     * 判断存储卡权限
     */
    private void requestPermission() {
        //权限判断是否有访问外部存储空间权限
        int flag = ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
        if (flag != PackageManager.PERMISSION_GRANTED) {
            if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                // 用户拒绝过这个权限了,应该提示用户,为什么需要这个权限。
                Toast.makeText(getActivity(), getResources().getString(R.string.update_permission), Toast.LENGTH_LONG).show();
            }
            // 申请授权
            requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
        } else {
            // 拥有权限,执行下载相关逻辑
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 1) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                 // 授予权限,执行下载相关逻辑
            } else {
                //拒绝权限,给出提示
                Toast.makeText(getActivity(), getResources().getString(R.string.update_permission), Toast.LENGTH_LONG).show();
                dismiss();
            }
        }
    }
}

Android N 文件的访问权限

为了提高私有文件的安全性,面向Android7.0或更高版本的应用私有目录被限制访问(0700)。此设置可防止私有文件的元数据泄漏,如它们的大小或存在性。此权限更改有多重副作用:

  • 私有文件的文件权限不应再由所有者放宽,为使用 MODE_WORLD_READABLE 和/或 MODE_WORLD_WRITEABLE 而进行的此类尝试将触发 SecurityException
注:迄今为止,这种限制尚不能完全执行。应用仍可能使用原生 API 或 File API 来修改它们的私有目录权限。但是,我们强烈反对放宽私有目录的权限
  • 传递软件包网域外的file://URI可能给接收器留下无法访问的路径。因此,尝试传递 file:// URI 会触发FileUriExposedException。分享私有文件内容的推荐方法是使用 FileProvider。
  • DownloadManager 不再按文件名分享私人存储的文件。旧版应用在访问COLUMN_LOCAL_FILENAME 时可能出现无法访问的路径。面向Android7.0或更高版本的应用在尝试访问COLUMN_LOCAL_FILENAME时会触发SecurityException。通过使用DownloadManager.Request.setDestinationInExternalFilesDir()DownloadManager.Request.setDestinationInExternalPublicDir()将下载位置设置为公共位置的旧版应用仍可以访问COLUMN_LOCAL_FILENAME中的路径,但是我们强烈反对使用这种方法。对于由DownloadManager公开的文件,首选的访问方式是使用ContentResolver.openFileDescriptor()
    下面看一下代码片段:

清单文件

<provider
   android:name=".DownloadFileProvider"
   android:authorities="${applicationId}.fileProvider"
   android:exported="false"
   android:grantUriPermissions="true">
      <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/update_file_path" />
    </provider>

文件存储配置

    <paths>
    <external-path
        name="external_storage_root"
        path="." />
    <files-path
        name="files-path"
        path="." />
    <cache-path
        name="cache-path"
        path="." />
    <!--/storage/emulated/0/Android/data/...-->
    <external-files-path
        name="external_file_path"
        path="." />
    <!--代表app 外部存储区域根目录下的文件 Context.getExternalCacheDir目录下的目录-->
    <external-cache-path
        name="external_cache_path"
        path="." />
    <!--配置root-path。这样子可以读取到sd卡和一些应用分身的目录,据说应用分身有bug-->
    <root-path
        name="root-path"
        path="" />
/paths>

app安装

    File downloadFile = getDownloadFile();
    Intent intent = new Intent(Intent.ACTION_VIEW);
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
        intent.setDataAndType(Uri.fromFile(downloadFile), "application/vnd.android.package-archive");
    } else {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
            boolean allowInstall = context.getPackageManager().canRequestPackageInstalls();
            if (!allowInstall) {
                //不允许安装未知来源应用,请求安装未知应用来源的权限
                if (mainPageExtraListener != null) {
                    mainPageExtraListener.applyAndroidOInstall();
                }
                return;
            }
        }
        //Android7.0之后获取uri要用contentProvider
        Uri apkUri = FileProvider.getUriForFile(context.getApplicationContext(), context.getPackageName() + ".fileProvider", downloadFile);
        //Granting Temporary Permissions to a URI
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
    }
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);

Android O 关于未知来源应用

针对 8.0 的应用需要在 AndroidManifest.xml 中声明REQUEST_INSTALL_PACKAGES 权限,否则将无法进行应用内升级

清单文件

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

权限检测

/**
     * 检测到无权限安装未知来源应用,回调接口中需要重新请求安装未知应用来源的权限
     */
    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    public void applyAndroidOInstall() {
        //请求安装未知应用来源的权限
        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.REQUEST_INSTALL_PACKAGES}, INSTALL_PACKAGES_REQUESTCODE);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        // 8.0的权限请求结果回调
        if (requestCode == INSTALL_PACKAGES_REQUESTCODE) {
            // 授权成功
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
               // 执行安装apk的逻辑...
            } else {
                // 授权失败,引导用户去未知应用安装的界面
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
                    //注意这个是8.0新API
                    Uri packageUri = Uri.parse("package:" + getPackageName());
                    Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageUri);
                    startActivityForResult(intent, GET_UNKNOWN_APP_SOURCES);
                }
            }
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        //8.0应用设置界面未知安装开源返回时候
        if (requestCode == GET_UNKNOWN_APP_SOURCES) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                boolean allowInstall = getPackageManager().canRequestPackageInstalls();
                if (allowInstall) {
                   // 执行安装app的逻辑...
                } else {
                   // 拒绝权限逻辑...
                   Toast.makeText(MainActivity.this,"您拒绝了安装未知来源应用,应用暂时无法更新!",Toast.LENGTH_SHORT).show();
                }
            }
        }
    }

Android Q 存储变更

目前Android Q官网上还是处于Beta版,Android Q最大的变化 无非是对用户隐 私权的进一步保护,为每个应用程序在外部存储设备提供了一个独立的存储沙箱,应用通过路径创建的文件都保存在应用的沙箱目录。
关于下载,文件肯定需要保存到本地了,但是由于AndroidQ采取分区存储,致使:getExternalStorageDirectory()与getExternalStoragePublicDirectory()读写权限变化,用户在拥有读写权限的同时,不可以在内部存储肆意的构建自己的目录,这样也更容易管理,卸载应用的时候也可以将这块数据与文件完全删除。

   if (TextUtils.isEmpty(appUpdate.getSavePath())) {
        //使用系统默认的下载路径 此处为应用内 /android/data/packages ,所以兼容7.0
        request.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, context.getPackageName() + ".apk");
    } else {
        // 自定义的下载目录,注意这是涉及到android Q的存储权限,建议不要用getExternalStorageDirectory()
        request.setDestinationInExternalFilesDir(context, appUpdate.getSavePath(), context.getPackageName() + ".apk");
        // 清除本地缓存的文件
        deleteApkFile(Objects.requireNonNull(context.getExternalFilesDir(appUpdate.getSavePath())));
    }
通过setDestinationInExternalFilesDir()存储文件与getExternalFilesDir()获取文件,完全可以避免Android Q对于存储做出的限制。

文件MD5校验

如果采取系统的DownloadManager来实现更新的话,个人觉得可以不用进行校验,当然如果害怕下载的文件被篡改或者不完整的话建议可以加上MD5校验。关于MD5作用有以下几点:

  • 用于校验apk文件签名是否一致,防止下载被拦截与篡改
  • 用于校验文件大小的完整性

下面查看一下代码片段:

    /**
     * 检查文件的MD5的合法性,若不一致,则无法安装
     *
     * @param md5  服务器返回的文件md5值
     * @param file 下载的apk文件
     * @return true 则md5校验通过 false 则失败
     */
    public static boolean checkFileMd5(String md5, File file) {
        if (TextUtils.isEmpty(md5)) {
            return false;
        }
        String md5OfFile = getFileMd5ToString(file);
        if (TextUtils.isEmpty(md5OfFile)) {
            return false;
        }
        return md5.equalsIgnoreCase(md5OfFile);
    }

    /**
     * Return the MD5 of file.
     *
     * @param file The file.
     * @return the md5 of file
     */
    private static String getFileMd5ToString(final File file) {
        return bytes2HexString(getFileMd5(file));
    }

    private static final char[] HEX_DIGITS =
            {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};

    private static String bytes2HexString(final byte[] bytes) {
        if (bytes == null) {
            return "";
        }
        int len = bytes.length;
        if (len <= 0) {
            return "";
        }
        char[] ret = new char[len << 1];
        for (int i = 0, j = 0; i < len; i++) {
            ret[j++] = HEX_DIGITS[bytes[i] >> 4 & 0x0f];
            ret[j++] = HEX_DIGITS[bytes[i] & 0x0f];
        }
        return new String(ret);
    }

    /**
     * Return the MD5 of file.
     *
     * @param file The file.
     * @return the md5 of file
     */
    private static byte[] getFileMd5(final File file) {
        if (file == null) {
            return null;
        }
        DigestInputStream dis = null;
        try {
            FileInputStream fis = new FileInputStream(file);
            MessageDigest md = MessageDigest.getInstance("MD5");
            dis = new DigestInputStream(fis, md);
            byte[] buffer = new byte[1024 * 256];
            while (true) {
                if (dis.read(buffer) <= 0) {
                    break;
                }
            }
            md = dis.getMessageDigest();
            return md.digest();
        } catch (NoSuchAlgorithmException | IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (dis != null) {
                    dis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

最后

关于版本更新大概就这么多知识点了,比较简单,但是很零碎,如果想要了解详细的内容,卿可以下载源码进行查看哦,源码详见我的开源地址AppUpdate,本库经过长期的验证,稳定性很OK的啦,如果有好的想法,直接提issues。

本库目前的功能

  • 适配Android M,处理关于存储文件的运行时权限
  • 适配Android N,安卓增强了文件访问的安全性,利用FileProvider来访问文件
  • 适配Android O,增加未知来源应用的安装提示
  • 适配Android Q,关于Q增加沙箱,改变了应用程序访问设备外部存储上文件的方式如SD卡
  • 默认采取DownloadManager+系统通知实现后台下载,安装完毕自动弹出安装界面,也可以自由配置增加下载进度框与下载失败的提示框
  • 支持强制更新,未更新无法使用应用
  • 支持MD5文件防篡改及完整性校验
  • 支持自定义更新提示界面,定制适合自己的更新框
  • 下载失败支持通过系统浏览器直接下载
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

浅谈Android版本更新 的相关文章

随机推荐

  • HashMap源码

    数组 数组存储区间是连续的 占用内存严重 故空间复杂度很大 但数组的二分查找时间复杂度很小 为 o 1 数组的特点 查找速度快 插入和删除效率低 链表 链表存储区间离散 占用内存比较宽松 故空间复杂度很小 但时间复杂度很大 为 o n 链表
  • MTCNN+CRNN解决车牌识别问题-2

    这次到CRNN部分了 CRNN网络很简单 就是CNN RNN 因为RNN适用于时间序列类型的数据 车牌呢 其实也是有规律的 比如第一位是汉字 后面是字母 汉字 前一部分通过MTCNN将车牌区域已经定位了 那这部分就需要拿CRNN来对其进行训
  • Unable to cast object of type in System.DirectoryServices.AccountManagement.GroupPrincipal

    在使用UserPrincipal Current ToString 获取域登录用户信息时 本地调试没有问题 上传到服务器报错 Unable to cast object of type System DirectoryServices Ac
  • CSwin-PNet: CNN-Swin-Vit 组合金字塔网络用于超声图像中乳腺病变分割

    ATTransUNet 期刊分析 摘要 贡献 方法 整体框架 1 Residual Swin Transformer block 2 Interactive channel attention module 3 Supplementary
  • java数据类型

    整数类型 byte short int long 浮点数类型 float double 字符类型 String 布尔类型 boolean 1 整数类型 byte 128 127 short 32768 32767 int 214748364
  • CTFHub-时间盲注-wp #(自制脚本做法)

    时间盲注脚本 coding utf 8 Time 2021 5 16 19 29 Author z1moq File ctfhub时间盲注 py Software PyCharm import requests import string
  • MATLAB实现控制系统模型(传递函数)的建立与转化,传递函数模型与零极点增益模型的转化,连续系统与离散系统的转化,对比不同采样周期对系统性能的影响

    最近使用MATLAB做了很多控制工程方面的仿真 测试不同系统的响应和特性 不得不说使用MATLAB做控制仿真还是十分简洁方便的 尤其是其中的simulink模块可以提供更加直观的模型 方便分析与测试 今天就分享在matlab中构造传递函数模
  • 【微信小程序】微信支付接入全流程

    一 前置条件 接入支付首先得需要有企业资质 并开通企业对公户 注册微信支付并进行对公户打款认证 二 开始接入 1 下载微信支付的AP证书 2 服务端接入微信支付 2 1 引入相关maven配置
  • Java基础之集合

    Java基础 集合 1 Collection接口 Collection 是 List 和 Set 的父接口 常用方法如下 package com java day16 import java util ArrayList import ja
  • 简单实现继承一个抽象类的同时实现接口

    定义一个抽象类animal author ljf 定义一个抽象类animal 关键字abstract public abstract class Animal 将动物共有属性进行封装 名字 年龄 颜色 性别 说话 private Strin
  • np.dot(a, b)用法

    In short np dot a b 就是一个乘法函数 数和数相乘 若a和b都是数 np dot 1 2 2 一维数组的内积 np dot 1 2 3 4 5 6 1 2 3 4 5 6 1x4 2x5 3x6 32 矩阵的乘积 x np
  • Unity学习笔记——TextMeshPro使用详解

    https blog csdn net elineSea article details 88799896 TextMesh Pro是Unity默认文本组件的替代品 TextMesh Pro和默认组件一样拥有高性能 它使用了完全不同的Sig
  • 为什么大部分人认为测试用例不重要?如何正确编写软件测试用例?

    如何编写测试用例似乎不是开发的重要部分 但是为了让一个软件测试人员最好地完成他们的工如如何编写测试用例似乎不是开发的重要部分 但是为了让一个软件测试人员最好地完成他们的作 他们需要一套清晰的步骤和一个被测试的东西的清晰定义 编写优秀的测试用
  • 代码审查常见代码质量问题

    配套的Bug解释模式 为了有针对性的使用这个工具 减少bug的误报 提高使用效率 我们选择了10个左右的bug模式 下面就是对这10个模式的解释 这些bug可能会引起程序的性能或逻辑问题 需要说明的是 findbugs能检测的bug pat
  • 有哪些好用的设计图工具?

    设计图纸制作软件是高级学习数字设计的最佳选择 无论你是想通过设计图纸制作软件创建一个明亮的设计 还是与其他设计师分享和交流 本文将介绍十个易于使用的设计图纸制作软件 其中大多数是初学者和高级艺术家 具有完整的绘图 照片编辑和小图形设计项目功
  • jmeter 安装_JMeter安装教程

    一 安装JMeter之前我们需要下载Java的jdk Java软件开发工具包 这是因为JMeter软件是由Java代码100 开发的 Java代码要运行必须依托于JVM Java虚拟机 因此JMeter如果要运行也必须要在有JVM环境的系统
  • STM32学习记录 中断

    STM32 中断非常强大 每个外设都可以产生中断 中断类型有 系统异常 外部中断 NVIC 嵌套向量中断控制器 属于内核外设 管理着包括内核和片上所有外设的中断相关的功能 两个重要的库文件 core cm3 h和misc h 中断编程的顺序
  • C语言文件操作详解

    目录 前言 一 文本数据和二进制数据 文本数据 二进制数据 文本文件和二进制文件 二 文件的打开和关闭 文件指针 打开文件 小细节 关闭文件 举个例子 注意事项 三 文本文件的读写 向文件中写入数据 举个例子 运行效果 从文件中读取数据 举
  • 什么是黑盒测试,和白盒测试的区别有哪些?

    软件测试是软件工程中的一个非常重要的环节 是开发项目整体的一部分 是伴随软件工程的诞生而诞生的 但软件测试不是万能的 不可能发现全部缺陷 其中 黑盒测试和白盒测试是两种不同类型的软件测试策略 它们具有同样强大的功能 白盒测试和黑盒测试往往不
  • 浅谈Android版本更新

    关于本文DownloadManager版本更新的源码链接详见我的开源项目AppUpdate 前言 版本升级对于app来讲已经是非常常见的功能了 每次项目的版本迭代 新功能的开发都需要下载更新新版本 通过安装新版本来实现我们的迭代 当然除了这