转载 :https://www.jianshu.com/p/a6cad97ea54f

相信很多应用都是采用内部下载的方式,这样的体验肯定比跳转到浏览器好得多!而应用商店审核周期长,无法实时更新最新应用!所以内部下载更新就显得尤为重要!

1.要美观好看,给用户实时的反馈下载情况:

界面体现为下载百分比%,下载速度 kb/s,圆环进度

2.下载完成后要自动安装:

Android6.0,需要动态申请权限,读取写入。
Android7.0,需要通过fileprovider的方式创建Uri
Android8.0,需要申请【安装未知来源应用权限】

针对第一个问题,我们采用自定义View来完成,可定制化高,样式想怎样改怎样改。而第二个问题就需要我们队权限的申请和对路径创建方式的注意了。

先来一个效果图:

下载安装apk.gif

这个其实就是简单的一个Dialog了,中间的狮子图片是应用LOGO,下面的正在下载就是一个文字描述,难点主要是中间的进度圆圈和圆圈点上的行星进度。

1.1新建一个Dialog弹出框:DownloadCircleDialog

public class DownloadCircleDialog extends Dialog {public DownloadCircleDialog(Context context) {super(context, R.style.Theme_Ios_Dialog);}DownloadCircleView circleView;TextView tvMsg;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.download_circle_dialog_layout);this.setCancelable(false);//设置点击弹出框外部,无法取消对话框circleView = findViewById(R.id.circle_view);tvMsg = findViewById(R.id.tv_msg);}public void setProgress(int progress) {circleView.setProgress(progress);circleView.postInvalidate();}public void setMsg(String msg){tvMsg.setText(msg);}
}

在style.xml中写入样式:Theme.Ios.Dialog

<!-- IOSDialog --><style name="Theme.Ios.Dialog" parent="@android:style/Theme.Dialog"><!-- Dialog的windowFrame框为无 --><!-- <item name="android:windowFrame">@null</item> --><!-- 边框 --><item name="android:windowIsFloating">true</item><!-- 是否浮现在activity之上 --><item name="android:windowIsTranslucent">true</item><!-- 半透明 --><item name="android:windowNoTitle">true</item><!-- 设置dialog的背景 --><item name="android:windowBackground">@android:color/transparent</item><!-- 背景是否模糊显示 --><item name="android:backgroundDimEnabled">true</item><!-- 模糊 --><item name="android:textColorPrimaryInverse">@android:color/black</item></style>

layout中的布局文件:download_circle_dialog_layout

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/layout_notice"android:background="@color/transparent"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:orientation="vertical"><com.yy.trade.customview.DownloadCircleViewandroid:id="@+id/circle_view"android:layout_centerInParent="true"android:layout_width="180dp"android:layout_height="180dp"/><ImageViewandroid:layout_centerInParent="true"android:src="@mipmap/icon_logo"android:layout_width="64dp"android:layout_height="64dp"/><TextViewandroid:id="@+id/tv_msg"android:layout_centerHorizontal="true"android:layout_below="@+id/circle_view"android:text="正在下载..."android:layout_width="wrap_content"android:layout_height="wrap_content"/>
</RelativeLayout>

1.2中间下载进度圆圈:DownloadCircleView

public class DownloadCircleView extends View {Paint mBgPaint;Paint mStepPaint;Paint mTxtCirclePaint;Paint mTxtPaint;int outsideRadius=160;int progressWidth =8;float progresTtextSize  = 24;Context context;public DownloadCircleView(Context context) {super(context);}public DownloadCircleView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);init(context);}public DownloadCircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {int width;int height;int size = MeasureSpec.getSize(widthMeasureSpec);int mode = MeasureSpec.getMode(widthMeasureSpec);if (mode == MeasureSpec.EXACTLY) {width = size;} else {width = (int) ((2 * outsideRadius) + progressWidth);}size = MeasureSpec.getSize(heightMeasureSpec);mode = MeasureSpec.getMode(heightMeasureSpec);if (mode == MeasureSpec.EXACTLY) {height = size;} else {height = (int) ((2 * outsideRadius) + progressWidth);}setMeasuredDimension(width, height);}private void init(Context context) {this.context = context;mBgPaint = new Paint();mBgPaint.setStrokeWidth(8);mBgPaint.setColor(Color.GRAY);this.mBgPaint.setAntiAlias(true); //消除锯齿this.mBgPaint.setStyle(Paint.Style.STROKE); //绘制空心圆mStepPaint = new Paint();mStepPaint.setStrokeWidth(8);mStepPaint.setColor(Color.parseColor("#3B4463"));this.mStepPaint.setAntiAlias(true); //消除锯齿this.mStepPaint.setStyle(Paint.Style.STROKE); //绘制空心圆mTxtCirclePaint = new Paint();mTxtCirclePaint.setColor(Color.parseColor("#3B4463"));this.mTxtCirclePaint.setAntiAlias(true); //消除锯齿this.mTxtCirclePaint.setStyle(Paint.Style.FILL); //绘制实心圆mTxtPaint = new Paint();mTxtPaint.setTextSize(progresTtextSize);mTxtPaint.setColor(Color.WHITE);this.mTxtPaint.setAntiAlias(true); //消除锯齿this.mTxtPaint.setStyle(Paint.Style.FILL); //绘制实心圆}float maxProgress=100f;float progress  =0f;public void setProgress(float progress) {this.progress = progress;}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);//灰色圆圈int circlePoint = getWidth() / 2;canvas.drawCircle(circlePoint, circlePoint, outsideRadius, mBgPaint); //画出圆//进度RectF oval = new RectF();oval.left=circlePoint - outsideRadius;oval.top=circlePoint - outsideRadius;oval.right=circlePoint + outsideRadius;oval.bottom=circlePoint + outsideRadius;float range = 360 * (progress / maxProgress);canvas.drawArc(oval, -90,  range, false, mStepPaint);  //根据进度画圆弧//轨道圆和文字double x1 = circlePoint + outsideRadius * Math.cos((range-90) * 3.14 / 180);double y1 = circlePoint + outsideRadius * Math.sin((range-90) * 3.14 / 180);canvas.drawCircle((float) x1, (float) y1, outsideRadius/4, mTxtCirclePaint);String txt = (int) progress + "%";float strwid  = mTxtPaint.measureText(txt);//直接返回参数字符串所占用的宽度canvas.drawText(txt,(float) x1-strwid/2, (float) y1+progresTtextSize/2,mTxtPaint);}
}

这样,下载样式基本就完成了,每次通过方法setProgress和setMsg就可以去设置下载的进度和速度了!

下面说说下载:采用 okhttp来下载apk文件,通过 ProgressManager来监听进度,通过 easypermissions简化动态申请权限

2.1首先我们写个下载工具类:DownloadUtils

public class DownloadUtils {private static DownloadUtils instance;private OkHttpClient okHttpClient;private Handler mHandler; //所有监听器在 Handler 中被执行,所以可以保证所有监听器在主线程中被执行public static DownloadUtils getInstance() {if (instance == null) instance = new DownloadUtils();return instance;}private DownloadUtils() {this.mHandler = new Handler(Looper.getMainLooper());OkHttpClient.Builder builder = new OkHttpClient.Builder();okHttpClient = ProgressManager.getInstance().with(builder).build();}public interface OnDownloadListener{/*** 下载成功*/void onDownloadSuccess();/*** @param progress 下载进度*/void onDownloading(ProgressInfo progress);/*** 下载失败*/void onDownloadFailed();}/*** @param url      下载连接* @param saveDir  储存下载文件的SDCard目录* @param listener 下载监听*/public void download(final String url, final String saveDir, final String saveName, final OnDownloadListener listener) {Request request = new Request.Builder().url(url).build();okHttpClient.newCall(request).enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) {// 下载失败listener.onDownloadFailed();}@Overridepublic void onResponse(Call call, Response response) throws IOException {// Okhttp/Retofit 下载监听InputStream is = null;byte[] buf = new byte[2048];int len = 0;FileOutputStream fos = null;// 储存下载文件的目录try {is = response.body().byteStream();File file = new File(saveDir, saveName);if (!file.getParentFile().exists()) file.getParentFile().mkdirs();fos = new FileOutputStream(file);while ((len = is.read(buf)) != -1) {fos.write(buf, 0, len);}fos.flush();mHandler.post(new Runnable() {@Overridepublic void run() {// 下载完成listener.onDownloadSuccess();}});} catch (Exception e) {Log.e("下载异常", e.getMessage());listener.onDownloadFailed();} finally {try {if (is != null) is.close();} catch (IOException e) {}try {if (fos != null) fos.close();} catch (IOException e) {}}}});ProgressManager.getInstance().addResponseListener(url, new ProgressListener() {@Overridepublic void onProgress(ProgressInfo progressInfo) {listener.onDownloading(progressInfo);}@Overridepublic void onError(long l, Exception e) {listener.onDownloadFailed();}});}}

上面通过ProgressManager.getInstance().with(builder).build();创建的okHttpClient也就相当于把ProgressManager加入到了OkHttp中,这样ProgressManager监听才会有效!
然后我们在需要下载apk的页面中加入:

  /*** 有新版本*/final static int PG_WRITE = 0X0008;@AfterPermissionGranted(PG_WRITE)private void showNewVersion() {String[] perms = {READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE};if (!EasyPermissions.hasPermissions(this, perms)) {EasyPermissions.requestPermissions(MainActivity.this, getString(R.string.need_permiss), PG_WRITE, perms);return;}new ApkUpdateDialog(MainActivity.this, "软件更新", "下载QQ测试", new ApkUpdateDialog.OnOkListener() {@Overridepublic void onOkClick() {String down_url = "https://qd.myapp.com/myapp/qqteam/AndroidQQ/mobileqq_android.apk";downloadApk(MainActivity.this, down_url);}}).show();}public void downloadApk(final Activity context, String down_url) {dialog.show();DownloadUtils.getInstance().download(down_url, SdUtils.getDownloadPath(), "QQ.apk", new DownloadUtils.OnDownloadListener() {@Overridepublic void onDownloadSuccess() {dialog.dismiss();L.i("恭喜你下载成功,开始安装!==" + SdUtils.getDownloadPath() + "QQ.apk");showToast("恭喜你下载成功,开始安装!");String successDownloadApkPath = SdUtils.getDownloadPath() + "QQ.apk";installApkO(MainActivity.this, successDownloadApkPath);}@Overridepublic void onDownloading(ProgressInfo progressInfo) {dialog.setProgress(progressInfo.getPercent());boolean finish = progressInfo.isFinish();if (!finish) {long speed = progressInfo.getSpeed();dialog.setMsg("(" + (speed > 0 ? FormatUtils.formatSize(context, speed) : speed) + "/s)正在下载...");} else {dialog.setMsg("下载完成!");}}@Overridepublic void onDownloadFailed() {dialog.dismiss();showToast("下载失败!");}});}/*** 兼容8.0安装位置来源的权限*/private void installApkO(Context context, String downloadApkPath) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {//是否有安装位置来源的权限boolean haveInstallPermission = getPackageManager().canRequestPackageInstalls();if (haveInstallPermission) {L.i("8.0手机已经拥有安装未知来源应用的权限,直接安装!");AppUtils.installApk(context, downloadApkPath);} else {new CakeResolveDialog(context, "安装应用需要打开安装未知来源应用权限,请去设置中开启权限", new CakeResolveDialog.OnOkListener() {@Overridepublic void onOkClick() {Uri packageUri = Uri.parse("package:"+ AppUtils.getAppPackageName());Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,packageUri);}}).show();}} else {AppUtils.installApk(context, downloadApkPath);}}@Overridepublic void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);}@Overridepublic void onPermissionsGranted(int requestCode, List<String> perms) {T.showShort("已经获得此权限!");}@Overridepublic void onPermissionsDenied(int requestCode, List<String> perms) {L.i("申请的权限被拒绝了,提醒用户去设置");if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {new AppSettingsDialog.Builder(this).setTitle(R.string.need_open_relevant_power).setRationale(R.string.some_permission_must_need).build().show();}}@Overrideprotected void onActivityResult(int requestCode, int resultCode, Intent data) {super.onActivityResult(requestCode, resultCode, data);L.i("回调。。。requestCode==" + requestCode + "  resultCode==" + resultCode);if (requestCode == AppSettingsDialog.DEFAULT_SETTINGS_REQ_CODE) {Toast.makeText(this, R.string.some_permission_must_need, Toast.LENGTH_SHORT).show();}if (requestCode == 10086) {L.i("设置了安装未知应用后的回调。。。");String successDownloadApkPath = SdUtils.getDownloadPath() + "QQ.apk";installApkO(MainActivity.this, successDownloadApkPath);}}

上面代码第一段showNewVersion就是对读写权限的申请,downloadApk下载进度的监听,下载完成通过installApkO来安装,installApkO中判断如果没有安装位置来源的权限就跳转到设置开启安装位置来源权限的页面,设置完成后回到这个页面继续安装!
上面的AppUtils.installApk是我写的一个工具方法,

public static  void installApk(Context context,String downloadApk) {Intent intent = new Intent(Intent.ACTION_VIEW);File file = new File(downloadApk);L.i("安装路径=="+downloadApk);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {Uri apkUri = FileProvider.getUriForFile(context, AppUtils.getAppPackageName()+".fileprovider", file);intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);intent.setDataAndType(apkUri, "application/vnd.android.package-archive");} else {intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);Uri uri = Uri.fromFile(file);intent.setDataAndType(uri, "application/vnd.android.package-archive");}context.startActivity(intent);}

判断如果>=7.0就通过fileprovider来创建Uri,避免安装出现解析包异常!6.0的读写权限通过showNewVersion()方法进行了申明,8.0的安装未知来源应用权限在installApkO进行了判断申请,从而使安装APK兼容了6,7,8!9.0的机子还没用过,不过如果没改动,应该也可以安装!

这样一个apk从开始下载,进度显示到安装就完成了!说起来就是一个apk的下载安装,但是其实代码量和坑还是挺多的:

坑一:最开始没有使用ProgressManager来进度监听,而是在download方法的写文件中监听下载进度:

public void download(final String url, final String saveDir, final OnDownloadListener listener) {Request request = new Request.Builder().url(url).build();okHttpClient.newCall(request).enqueue(new Callback() {@Overridepublic void onFailure(Call call, IOException e) {// 下载失败listener.onDownloadFailed();}@Overridepublic void onResponse(Call call, Response response) throws IOException {InputStream is = null;byte[] buf = new byte[2048];int len = 0;FileOutputStream fos = null;// 储存下载文件的目录String savePath = isExistDir(saveDir);try {is = response.body().byteStream();long total = response.body().contentLength();File file = new File(savePath, getNameFromUrl(url));fos = new FileOutputStream(file);long sum = 0;while ((len = is.read(buf)) != -1) {fos.write(buf, 0, len);sum += len;int progress = (int) (sum * 1.0f / total * 100);// 下载中listener.onDownloading(progress);}fos.flush();// 下载完成listener.onDownloadSuccess();} catch (Exception e) {listener.onDownloadFailed();} finally {try {if (is != null)is.close();} catch (IOException e) {}try {if (fos != null)fos.close();} catch (IOException e) {}}}});}

这样监听到的下载进度并不是真的下载进度,而是下载文件后写入到手机的速度,体现到界面就是最开始下载进度是0%一直不动,然后2秒钟就从0%转圈到100%,就下载完成了,给用户感觉一点都不真实!

坑二:下载完成后解析包错误:

a.主要原因就是没有使用Uri apkUri = FileProvider.getUriForFile(context, AppUtils.getAppPackageName()+".fileprovider", file);的方式来创建Uri 安装,
b.还有就是因为文件名字不正确,最开始我的download方法中没有saveName方法,而是通过下载地址截取最后的“/”来写入文件名的,但是有的下载地址并不是以apk结尾,从而导致解析包错误!
c.还有就是根本没有这个文件路径,从而导致写错误,所以在download方法中写入本地文件前我加入了如果没有文件路径就先创建当前路径

File file = new File(saveDir, saveName);
if (!file.getParentFile().exists()) file.getParentFile().mkdirs();

坑三:下载出错:CertPathValidatorException: Trust anchor for certification path not found,(上面的代码没有加入,因为每个人的OkHttpClient都不同,我是写了个工具类来创建的OkHttpClient,所以工具类中加入了进度读取和跳过SSL验证的,由于自私原因,大家自己加吧。)
相信有的应用是放在自己的服务器的,而又有https,但很多都是没有证书的,导致下载不了!所以我们就需要Okhttp绕过证书验证,参考:
https://blog.csdn.net/O0mm0O/article/details/76686917
坑四:无法安装:
Android8.0需要安装权限:<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>,同时7.0以上还需要安装未知来源的权限,不然也无法安装,可以参考:
https://www.jianshu.com/p/a6209440a518
坑五:无法安装:由于配置xml中的provider_paths.xml只写了

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android"><external-pathname="images"path="test/"/>
</paths>

导致无法读取路径而无法安装,因为我们是下载到Download的所以还要加入:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android"><external-pathname="images"path="test/"/><external-pathname="download"path="Download/"/>
</paths>

这样才算完整!关于fileprovider路径介绍:
https://blog.csdn.net/leilifengxingmw/article/details/57405908
坑六:安装完成后闪退,或安装完成后点击打开闪退
在大部分手机上没有问题,但在Vivo X9上居然安装完成后闪退了,我觉得这应该是应用已经死了,所有安装完成后立即启动有问题,而其他手机就没有问题,我觉得还是Vivo手机的厂商定制问题!所以解决办法就是安装的时候启动一个新的任务栈来安装:

 public static  void installApk(Context context,String downloadApk) {Intent intent = new Intent(Intent.ACTION_VIEW);File file = new File(downloadApk);L.i("安装路径=="+downloadApk);if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {Uri apkUri = FileProvider.getUriForFile(context, AppUtils.getAppPackageName()+".fileprovider", file);intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);intent.setDataAndType(apkUri, "application/vnd.android.package-archive");} else {intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);Uri uri = Uri.fromFile(file);intent.setDataAndType(uri, "application/vnd.android.package-archive");}context.startActivity(intent);}

其中:intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);就是关键啦!

下个应用如果还需要下载,我就把上面的代码复制进去,免得每次都要去找写了下载安装APK代码的项目,然后一部分一部分去查找复制!这么多代码记肯定是记不住的,这辈子都不可能记住的,所以写这里方便下次Copy!

Android apk下载与安装相关推荐

  1. Android Studio下载及安装3.0版本

    今天,继续简单讲讲Android studio3.0如何安装. 之前写过Android studio的安装,那个是Android 2.2.3版本,这次由于导入3.0项目,所以重新安装了Android ...

  2. Android Studio下载、安装、配置及连接真机开发第一个App ——入门选手快进

      最近的安卓测试给我心态搞蹦了,问也没人问,难受简直了!又是一轮自学之路(说实话这一路上教会我一个很大的道理就是一定要有愿意帮助你的老师,还有自己的自觉,一样也不能少.前者没有的话只能后者补了呜呜呜 ...

  3. Android 4.4环境搭建——Android SDK下载与安装

    学习开发Android应用程序,须要下载安装Android SDK.在Android的官方站点的二级域名http://developer.android.com/index.html中.能够下载到完整 ...

  4. IIS 添加mime 支持 apk,exe,.woff,IIS MIME设置 ,Android apk下载的MIME 设置 苹果ISO .ipa下载mime 设置...

    IIS 添加mime 支持 apk,exe,.woff,IIS MIME设置 ,Android apk下载的MIME 设置 苹果ISO .ipa下载mime 设置 原文:IIS 添加mime 支持 a ...

  5. Android Studio下载与安装

    文章目录 Android Studio下载与安装 一.进入官网,选择下载 二.双击打开 三.开始安装 四.配置信息 五.修改SDK位置 六.创建项目 七.创建手机模拟器 八.下载其它版本 Androi ...

  6. Android SDK下载与安装(仅支持jdk1.8及以下)

    1.Android SDK下载与安装(仅支持jdk1.8及以下) 1.Android SDK下载 下载地址:https://www.androiddevtools.cn/最新版 2.Android S ...

  7. android 版本更新 apk,Android 版本升级(APK下载和安装)

    将手机上应用版本号等相关信息发送到服务器,服务器检测是否有新版本.如果有新版本,将返回客户端版本号.版本名称.下载地址.版本更新说明等信息.客户端获取到此信息后,将询问用户是否下载,确定后,开始下载. ...

  8. Android中下载、安装和卸载(原)

    应用场景:在检查版本更新的时候经常需要从服务器端下载然后安装到手机中 使用工具: XUtils,这个开源的框架真的是需要花大把时间去阅读和理解的,十分有用的,on the way ! fighting ...

  9. Flutter APP内部APK下载以及安装

    本帖主要实现Flutter安卓系统 APK在线下载以及安装 检查APK权限 //必要权限,安装权限 <uses-permission android:name="android.per ...

  10. Android Studio电脑上怎么下载-Android Studio下载和安装图文教程[超详细]

    安装 Android Studio 只需轻松点击几下,即可完成 Android Studio 的设置. 首先,请务必下载最新版本的 Android Studio. Windows 如需在 Window ...

最新文章

  1. 机器学习(2)特征提取1 -- 字典特征提取
  2. myeclipse 如何显示序号
  3. 程序是什么--过滤器和状态机
  4. 百练OJ:2807:两倍
  5. kettle数据源连接的集群设置
  6. android监控指纹信息变化,android监听指纹变化(解决反射思路在android10不生效的问题)...
  7. 【汇编语言与计算机系统结构笔记13】简单的上机过程示例
  8. CodeForces - 837F(二分组合思维)
  9. 腾讯云服务器安装AMH控制面板
  10. java rds 数据库_java项目连接rds数据库
  11. 华为防火墙配置SSL+自签CA证书挑战登录
  12. 微信是胖客户端瘦服务器,一款管理微信客户软件的系统体系结构设计?
  13. k8s集群-master节点迁移
  14. Elasticsearch:Rank feature query - 排名功能查询
  15. 【取证分析】Linux硬盘镜像获取与还原(dd、AccessData FTK Imager)
  16. Mascarpone Cheese
  17. NLPIR语义平台全新上线九眼专业合同智能审核系统
  18. 游戏地图编辑器(tileStdio)
  19. PS“矩形工具”的多样式
  20. springboot+jsp教育机构OA系统(源码免费获取+论文+答辩PPT)

热门文章

  1. 图片转字符画-----做出酷炫的头像
  2. 实现OPEN 哈希表模板类
  3. 开源dns软件PowerDNS BIND9 mydns
  4. 微信模拟地理位置_微信电脑版伪装地理位置的方法
  5. 智能家居,“智能”是前提,“安全”是根本
  6. Spring源码解析:Spring Aware 原理解析
  7. 电子手轮控制步进电机程序手轮控制步进电机的頻率,脉冲数,内容包括欧姆龙plc程序,mcgs程序
  8. hadoop工作原理
  9. PC微信逆向:破解聊天记录文件!
  10. 2019年上半年软件设计师上午真题及答案解析