-
2018-11-15 16:53:39
示例:
零、准备工作
0.1第三方库
implementation ‘io.reactivex.rxjava2:rxjava:2.2.2’
implementation ‘io.reactivex.rxjava2:rxandroid:2.1.0’
implementation ‘io.reactivex.rxjava2:rxkotlin:2.3.0’
implementation ‘com.squareup.okhttp3:okhttp:3.11.0’
implementation ‘com.squareup.okio:okio:2.0.0’0.2权限
<uses-permission android:name="android.permission.INTERNET" /> <!-- 写入权限 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
0.3格式
“Code”: 0,
“Msg”: “”,
“UpdateStatus”: 1,
“VersionCode”: 3,
“VersionName”: “1.0.2”,
“ModifyContent”: “1、优化api接口。\r\n2、添加使用demo演示。\r\n3、新增自定义更新服务API接口。\r\n4、优化更新提示界面。”,
“DownloadUrl”: “https://raw.githubusercontent.com/xuexiangjys/XUpdate/master/apk/xupdate_demo_1.0.2.apk”,
“ApkSize”: 2048
“ApkMd5”: “…” //md5值没有的话,就无法保证apk是否完整,每次都会重新下载。一、检测是否是最新版本,不是则更新
private Disposable downDisposable; private ProgressBar progressBar; private TextView textView4; private Button upgrade; private long downloadLength=0; private long contentLength=0; private String[] PERMISSIONS_STORAGE = { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE};
//判断版本是否最新,如果不是最新版本则更新
private void test(){ Observable.create(new ObservableOnSubscribe<String>() { @Override public void subscribe(ObservableEmitter<String> emitter) throws Exception { OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("url") .build(); client.newCall(request).enqueue(new okhttp3.Callback() { @Override public void onFailure(Call call, IOException e) { emitter.onError(e); } @Override public void onResponse(Call call, Response response) throws IOException { String result=""; if (response.body()!=null) { result=response.body().string(); }else { //返回数据错误 return; } emitter.onNext(result); } }); // emitter.onNext("123"); } }).subscribeOn(Schedulers.io())// 将被观察者切换到子线程 .observeOn(AndroidSchedulers.mainThread())// 将观察者切换到主线程 .subscribe(new Observer<String>() { private Disposable mDisposable; @Override public void onSubscribe(Disposable d) { mDisposable = d; } @Override public void onNext(String result) { if (result.isEmpty()){ return; } //2.判断版本是否最新,如果不是最新版本则更新 String downloadUrl="https://raw.githubusercontent.com/xuexiangjys/XUpdate/master/apk/xupdate_demo_1.0.2.apk"; String title="是否升级到4.1.1版本?"; String size="新版本大小:未知"; String msg="1、优化api接口。\r\n2、添加使用demo演示。\r\n3、新增自定义更新服务API接口。\r\n4、优化更新提示界面。"; int versionCode=20000; try { int version = getPackageManager(). getPackageInfo(getPackageName(), 0).versionCode; if (versionCode>version){ LayoutInflater inflater = LayoutInflater.from(TestActivity.this); View view = inflater.inflate(R.layout.layout_dialog, null); AlertDialog.Builder mDialog = new AlertDialog.Builder(TestActivity.this,R.style.Translucent_NoTitle); mDialog.setView(view); mDialog.setCancelable(true); mDialog.setOnKeyListener(new DialogInterface.OnKeyListener() { @Override public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { return keyCode == KeyEvent.KEYCODE_BACK; } }); upgrade= view.findViewById(R.id.button); TextView textView1= view.findViewById(R.id.textView1); TextView textView2= view.findViewById(R.id.textView2); TextView textView3= view.findViewById(R.id.textView3); textView4= view.findViewById(R.id.textView4); ImageView iv_close= view.findViewById(R.id.iv_close); progressBar= view.findViewById(R.id.progressBar); progressBar.setMax(100); textView1.setText(title); textView2.setText(size); textView3.setText(msg); upgrade.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //动态询问是否授权 int permission = ActivityCompat.checkSelfPermission(getApplication(), Manifest.permission.WRITE_EXTERNAL_STORAGE); if (permission != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(TestActivity.this, PERMISSIONS_STORAGE, 1); }else { upgrade.setVisibility(View.INVISIBLE); down(downloadUrl); } } }); iv_close.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { finish(); } }); mDialog.show(); }else { } } catch (PackageManager.NameNotFoundException e) { e.printStackTrace(); } mDisposable.dispose(); } @Override public void onError(Throwable e) { test(); } @Override public void onComplete() { } }); }
//下载apk并更新进度条
private void down(String downloadUrl){ Observable.create(new ObservableOnSubscribe<Integer>() { @Override public void subscribe(ObservableEmitter<Integer> emitter) throws Exception { downApk(downloadUrl,emitter); } }).subscribeOn(Schedulers.io())// 将被观察者切换到子线程 .observeOn(AndroidSchedulers.mainThread())// 将观察者切换到主线程 .subscribe(new Observer<Integer>() { @Override public void onSubscribe(Disposable d) { downDisposable = d; } @Override public void onNext(Integer result) { //设置ProgressDialog 进度条进度 progressBar.setProgress(result); textView4.setText(result+"%"); } @Override public void onError(Throwable e) { Toast.makeText(getApplication(),"网络异常!请重新下载!",Toast.LENGTH_SHORT).show(); upgrade.setEnabled(true); } @Override public void onComplete() { Toast.makeText(getApplication(),"服务器异常!请重新下载!",Toast.LENGTH_SHORT).show(); upgrade.setEnabled(true); } }); }
二、下载apk
//下载apk
private void downApk(String downloadUrl,ObservableEmitter<Integer> emitter){ OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url(downloadUrl) .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { //下载失败 breakpoint(downloadUrl,emitter); } @Override public void onResponse(Call call, Response response) throws IOException { if (response.body() == null) { //下载失败 breakpoint(downloadUrl,emitter); return; } InputStream is = null; FileOutputStream fos = null; byte[] buff = new byte[2048]; int len; try { is = response.body().byteStream(); File file = createFile(); fos = new FileOutputStream(file); long total = response.body().contentLength(); contentLength=total; long sum = 0; while ((len = is.read(buff)) != -1) { fos.write(buff,0,len); sum+=len; int progress = (int) (sum * 1.0f / total * 100); //下载中,更新下载进度 emitter.onNext(progress); downloadLength=sum; } fos.flush(); //4.下载完成,安装apk installApk(TestActivity.this,file); } catch (Exception e) { e.printStackTrace(); breakpoint(downloadUrl,emitter); } finally { try { if (is != null) is.close(); if (fos != null) fos.close(); } catch (Exception e) { e.printStackTrace(); } } } }); }
//断点续传
private void breakpoint(String downloadUrl,ObservableEmitter<Integer> emitter){ OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url(downloadUrl) .addHeader("RANGE", "bytes=" + downloadLength + "-" + contentLength) .build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { //下载失败 breakpoint(downloadUrl,emitter); } @Override public void onResponse(Call call, Response response) throws IOException { if (response.body() == null) { //下载失败 breakpoint(downloadUrl,emitter); return; } InputStream is = null; RandomAccessFile randomFile = null; byte[] buff = new byte[2048]; int len; try { is = response.body().byteStream(); String root = Environment.getExternalStorageDirectory().getPath(); File file = new File(root,"updateDemo.apk"); randomFile = new RandomAccessFile(file, "rwd"); randomFile.seek(downloadLength); long total = contentLength; long sum = downloadLength; while ((len = is.read(buff)) != -1) { randomFile.write(buff,0,len); sum+=len; int progress = (int) (sum * 1.0f / total * 100); //下载中,更新下载进度 emitter.onNext(progress); downloadLength=sum; } //4.下载完成,安装apk installApk(TestActivity.this,file); } catch (Exception e) { e.printStackTrace(); breakpoint(downloadUrl,emitter); } finally { try { if (is != null) is.close(); if (randomFile != null) randomFile.close(); } catch (Exception e) { e.printStackTrace(); } } } }); }
/**
- 路径为根目录
- 创建文件名称为 updateDemo.apk
*/
private File createFile() { String root = Environment.getExternalStorageDirectory().getPath(); File file = new File(root,"updateDemo.apk"); if (file.exists()) file.delete(); try { file.createNewFile(); return file; } catch (IOException e) { e.printStackTrace(); } return null ; }
三、安装apk
3.1项目的src/res新建个xml文件夹再自定义一个file_paths文件
<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <files-path name="name1" path="test1" /> </paths>
3.2在清单文件中配置
<provider android:name="android.support.v4.content.FileProvider" android:authorities="com.mydomain.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>
3.3安装apk
//安装apk,包含7.0
public void installApk(Context context, File file) { if (context == null) { return; } String authority = getApplicationContext().getPackageName() + ".fileProvider"; Uri apkUri = FileProvider.getUriForFile(context, authority, file); Intent intent = new Intent(Intent.ACTION_VIEW); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); //判读版本是否在7.0以上 if (Build.VERSION.SDK_INT >= 24) { intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); } else { intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive"); } context.startActivity(intent); //弹出安装窗口把原程序关闭。 //避免安装完毕点击打开时没反应 Process.killProcess(android.os.Process.myPid()); }
四、取消订阅
@Override protected void onDestroy() { super.onDestroy(); downDisposable.dispose();//取消订阅 }
五、自定义Dialog
5.1UI
见一、检测是否是最新版本,不是则更新
5.2布局
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android=“http://schemas.android.com/apk/res/android”
xmlns:app=“http://schemas.android.com/apk/res-auto”
xmlns:tools=“http://schemas.android.com/tools”
android:layout_width=“match_parent”
android:layout_height=“match_parent”><ImageView android:id="@+id/imageView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/lib_update_app_top_bg" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <View android:id="@+id/view" android:layout_width="0dp" android:layout_height="0dp" android:background="@drawable/lib_update_app_info_bg" app:layout_constraintBottom_toTopOf="@+id/line" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/imageView1" /> <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:text="是否升级到1.0版本?" android:textColor="@android:color/black" android:textSize="15sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/imageView1" /> <TextView android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:text="新版本大小:" android:textColor="#666" android:textSize="14sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView1" /> <ScrollView android:id="@+id/scrollView2" android:layout_width="0dp" android:layout_height="60dp" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginEnd="16dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView2"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/textView3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="1,xxxxxxxx\n2,ooooooooo" android:textColor="#666" android:textSize="14sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView2" /> </LinearLayout> </ScrollView> <Button android:id="@+id/button" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="16dp" android:layout_marginEnd="16dp" android:background="@drawable/textview_round_red" android:gravity="center" android:minHeight="40dp" android:text="升级" android:textColor="@android:color/white" android:textSize="15sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/scrollView2" /> <ProgressBar android:id="@+id/progressBar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" app:layout_constraintBottom_toBottomOf="@+id/button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> <TextView android:id="@+id/textView4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="0%" android:textColor="#E94339" app:layout_constraintBottom_toTopOf="@+id/progressBar" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> <android.support.constraint.Guideline android:id="@+id/guideline1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintGuide_percent="0.5" /> <View android:id="@+id/line" android:layout_width="1dp" android:layout_height="50dp" android:layout_marginStart="8dp" android:layout_marginTop="16dp" android:layout_marginEnd="8dp" android:background="#d8d8d8" android:visibility="visible" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.501" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button" /> <ImageView android:id="@+id/iv_close" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:src="@mipmap/lib_update_app_close" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/line" /> <android.support.constraint.Guideline android:id="@+id/guideline2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.2" />
</android.support.constraint.ConstraintLayout>
5.3其他文件
5.3.1textview_round_red.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <!-- view背景色 --> <solid android:color="#E94339" /> <!-- 边框颜色 宽度 --> <stroke android:width="1dip" android:color="#E94339" /> <!-- 边框圆角 --> <corners android:bottomRightRadius="5dp" android:topRightRadius="5dp" android:bottomLeftRadius="5dp" android:topLeftRadius="5dp"/> </shape >
5.3.2lib_update_app_info_bg.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <corners android:bottomLeftRadius="5dp" android:bottomRightRadius="5dp"/> <solid android:color="@android:color/white"/> </shape>
5.3.3styles文件
<style name="Translucent_NoTitle" parent="android:style/Theme.Dialog"> <item name="android:background">#00000000</item> <!-- 设置自定义布局的背景透明 --> <item name="android:windowBackground">@android:color/transparent</item> <!-- 设置window背景透明,也就是去边框 --> </style>
5.4图片
更多相关内容 -
Android 一个app启动另一个app
2014-09-12 16:08:11一个app启动另一个app,这个玩法挺火的嘛,有没有试过更新QQ到5.1版本,QQ的健康里面就可以添加其他app,实现从QQ跳转到其他app应用,这里模拟写了一个demo -
App版本升级更新
2016-11-03 12:39:58App版本升级更新,集成轻松搞定 -
[干货]手把手教你写一个安卓app
2021-04-27 21:06:49我想大家是想写一个手机app吧,前面已经分享了在QT上如何写一个安卓蓝牙app,虽然qt可以做app但是比起Android Studio还是差很多了!这里我们介绍一种快速入门的方法来制作一款app,就算你是零基础小白没有学习过java...
摘要:最近有很多小伙伴在后台留言:Android Studio。我想大家是想写一个手机app,前面已经分享了在QT上如何写一个安卓蓝牙app,虽然qt可以做app,但是比起Android Studio还是差很多。这里介绍一种快速入门的方法来制作一款app,就算你是零基础小白没有学习过java语言也没有关系,相信看完我的文章,半天时间也能做一个安卓app。本文针对初学者,大佬勿喷啊1. 创建HelloWorld项目
这里我就不介绍如何安装这个Android Studio软件了,网上有很多教程或者去B站找对应的安装视频就可以了。安装好软件之后就开始按照下面的步骤新建工程了。
选择一个空应用
按照图片的配置方法,设置好工程名和路径
2. 修改阿里云镜像源
这一步一定要需要,不然的话你需要编译很久,因为在sync的过程中要下载的很多资源是在外网的,这里使用阿里云镜像源就会很快。修改后只对本项目有效:
第一处代码
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' } maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' } maven { url 'http://maven.aliyun.com/nexus/content/repositories/gradle-plugin' }
第二处代码
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' } maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' } maven { url 'http://maven.aliyun.com/nexus/content/repositories/gradle-plugin' }
这样编译起来就会快很多,建议这样修改,不然很可能下载失败导致编译不成功!
3. 真机调试
我们可以编译完成后打包成apk文件发送到你的手机进行安装运行,但我建议还是手机连上数据线在线调试比较好,省去很多时间也非常方便。手机连接电脑后打开USB调试,这里以华为荣耀V10手机作为参考。
- 1.选择USB连接方式是MIDI(将设备用做MIDI输入设备)
- 2.在设置的“系统和更新”—>开发人员选项—>打开USB调试
设备作为MIDI设备
开启USB调试
然后点击这个三角形,就可以看到手机上的APP显示了。
运行结果和上图一样。到这里我们已经完成了一个app的制作怎么样是不是很简单啊!
接下来介绍一下代码目录,方便大家能够快速的掌握和了解项目所生成文件功能和用途!
4. Android代码目录
这里有两种文件架构,所打开的也是两种不同的目录文件。
5. Android应用程序大致启动流程
5.1. APP配置文件
5.2. 活动文件(Java)
5.3. 布局文件(XML)
Android设计讲究前后端分离设计,上面的java文件是后端,引入了activity_main这个前端界面布局文件,如果想再设计一个界面就在layout文件夹下再新建一个 .xml文件就可以了。
5.4. res资源目录(统一管理)
5.4.1. colors.xml
三个颜色有点少我们可以在加一些颜色但这里面来。<color name="white">#FFFFFF</color> <!--白色 --> <color name="ivory">#FFFFF0</color> <!--象牙色 --> <color name="lightyellow">#FFFFE0</color> <!--亮黄色 --> <color name="yellow">#FFFF00</color> <!--黄色 --> <color name="snow">#FFFAFA</color> <!--雪白色 --> <color name="floralwhite">#FFFAF0</color> <!--花白色 --> <color name="lemonchiffon">#FFFACD</color> <!--柠檬绸色 --> <color name="cornsilk">#FFF8DC</color> <!--米绸色 -->
5.4.2. strings.xml
5.4.3. styles.xml
***
5、主界面布置
5.1线性布局(LinearLayout)
线性布局的形式可以分为两种,第一种横向线性布局,第二种纵向线性布局,总而言之都是以线性的形式一个个排列出来的,纯线性布局的缺点是很不方便修改控件的显示位置,所以开发中经常会以线性布局与相对布局嵌套的形式设置布局。
5.2相对布局(RelativeLayout)
相对布局是android布局中最为强大的,首先它可以设置的属性是最多了,其次它可以做的事情也是最多的。android手机屏幕的分辨率五花八门,为了考虑屏幕自适应的情况,在开发中建议大家都去使用相对布局,它的坐标取值范围都是相对的,所以使用它来做自适应屏幕是正确的。
5.3帧布局(FrameLayout)
帧布局原理是在控件中绘制任何一个控件都可以被后绘制的控件覆盖,最后绘制的控件会盖住之前的控件。界面中先绘制的ImageView 然后再绘制的TextView和EditView,后者就会覆盖在前者上面。
5.4绝对布局(AbsoluteLayout)
使用绝对布局可以设置任意控件在屏幕中XY坐标点,和帧布局一样绘制的控件会覆盖住之前绘制的控件,不建议大家使用绝对布局。android的手机分辨率五花八门,使用绝对布局的话在其它分辨率的手机上就无法正常的显示了。
5.5表格布局(TableLayout)
在表格布局中可以设置TableRow,可以设置表格中每一行显示的内容以及位置 ,可以设置显示的缩进,对齐的方式。
在实际应用中线行布局和相对布局是最常用的,一般自己写的app布局都相对比较简单,所以这里我们使用线性布局。打开APP配置文件中的
activity_main.xml
,就可以在这里面愉快的编程了。如果你之前没有玩过Android Studio也没有关系,左边修改右边预览多试试几次就大概明白了。在这里我们可以修改点击图片所转换的网址,大家打开源码就知道如何修改了,这里就不在赘述!
在
activity_main.xml
文件中我们可以修改界面的布局。
到这里基本上一个简单的安卓应用就完成了。只要你安装了Android Studio软件并且拿到我的源码就可以愉快的玩耍了。什么?你拿到我的代码却不能正常编译通过?下面就教大家如何把别人的源码拿到自己的软件中编译通过!
6、代码移植
以下是需要修改文件的地方,具体修改成啥样,可以参考一个你可以打的开的工程中的配置,参考对应的文件即可。
1.修改build.gradle文件
2.修改app/build.gradle文件
修改版本号
3.修改gradle/wrapper/gradle-wrapper.properties
这个地方修改成你可以打开的工程的 . zip包
4.修改local.properties
这个地方是你的软件安装路径所在的位置,要修改成你自己的安装路径
公众号后台回复:firstapp,即可获取源码和教程文档! -
web打包app(h5+app)版本自动更新的实现
2019-09-11 17:19:56文章目录背景说明原生app自动更新实现android自动更新实现ios自动更新实现h5+app的特点说明h5+app自动更新实现 背景说明 web打包的app(也称为h5+app),是指将基于html5等移动端web技术,开发的web应用打包成的app。...背景说明
web打包的app(也称为h5+app),是指将基于html5等移动端web技术,开发的web应用打包成的app。区别于原生app,5+app相当于给web应用加上了一层本地程序(ios、android等)的壳子。其原理是,使用了原生程序的webview组件,即在原生程序内部调用内置浏览器,实现应用的核心功能。h5+app打包app使用的hbuiderx打包的,打包相关的方法可以参考,本人文章:
将H5站点打包成app完美攻略原生app自动更新实现
原生app主要是基于ios和android的原生平台app,其打包原理,主要源于平台分发管理的不同和行业通用做法。ios是闭源的独立平台,apple store只有一个平台;android碎片化比较严重,世面上有上万家app store。各平台审核机制又不一样。这些方面都决定了ios和android的app,更新策略有所不同。
行业经验
Android 和 iOS 应用的更新都可以不用做,可以让第三方应用商店来帮你做,你只需更改应用的版本就行了。目前 Android 的通用做法是,在应用内检查版本号,通过跟服务器的版本号来对比,版本号不同就更新,具体的做法是可以在应用内写个下载程序,也可以在弹出浏览器来下载。iOS 应用如果要上传到 app store,是不允许在应用内检查更新的,否则不让上架,iOS 的更新更简单,让苹果 app store 来做就行了,你在 build 的时候,改变版本号就行,希望对你有帮助。以下ios和android app自动更新的实现,都是基于行业最佳实践。
android自动更新实现
在服务器需要一个json或xml文件,如:check.json。
#check.json [ { "version":1, "name":1.0, "miniVersion":1, "description":"asdfas/n", "forceUpdate":false, "size":3065, "uri":"h5app-1.0.apk" }, { "version":2, "name":1.1, "miniVersion":1, "description":"bbb/n", "forceUpdate":"true", "size":3065, "uri":"h5app-1.1.apk" }, { "version":3, "name":1.1, "miniVersion":1, "description":"bbb/n", "forceUpdate":"false", "size":3065, "uri":"h5app-1.1.apk" } ]
以上文件维护平台app的发布版本,基于些版本,提供apk自动更新。
“version”:1, #int 类型版本号,版本是递增的
“name”:1.0,
“miniVersion”:1, #平台支持最小的版本号,小于些的版本都会强制更新
“description”:“asdfas/n”, #版本说明
“forceUpdate”:false, #版本是否强制更新
“size”:3065, #app大小,单位kb
“uri”:“h5app-1.0.apk” #app下载路径程序文件结构如下:
version:发布apk包目录。
check.json:apk版本配置文件。相关接口如下
/** * h5app 版本检查[android]:方法只提供android版本的app,版本检查,对于ios,依赖apple store的版本管理,进行版本检查和更新。目前,苹果store app上架审核不允许程序进行自动版本更新。 * @return AppVersionBean * version 更新版本,始终为平台最新版本app * uri为app下载的url */ @RequestMapping("h5app/check") @ResponseBody public AppVersionBean h5appCheck(String currentVersion) { AppVersionBean result = new AppVersionBean(); return result; } /** * h5app下载 * @return */ @RequestMapping("h5app") @ResponseBody public Object h5app(HttpRequest request) { //request 请求设备类型,ios 、android做不同的返回 return null; } /** * h5app 应用版本更新下载[android]: * @param uri 文件名 * @return */ /** * uri为程序下载文件名 */ @RequestMapping("h5app/{uri}") public void getH5app(@PathVariable String uri) { //下载实现 } /** * app版本信息实体类 */ @Data class AppVersionBean { Integer version; String name; Integer miniVersion; String description; //是否强制更新 boolean forceUpdate; Long size; String uri; //是否有更新 boolean isUpdate; }
如上接口,app检查更新接口传入currentVersion。服务端根据currentVersion和服务端版本管理配置(miniVersion,forceUpdate等)确定app是否更新和是否强制更新。
app启动或者提供检查更新按钮,调用后台检查更新接口。客户端保存currentVersion和忽略版本列表,用于来检查更新,忽略的版本不再提示更新。立即更新,调用下载apk接口下载程序,完成更新。具体本地android代码,不在此提供,网上有很多。
ios自动更新实现
ios apk自动更新,基于apple store来实现。基于现在的上架审核策略,不允许使用检查更新,让苹果 app store 来做就行了,你在 build 的时候,改变版本号就行。
当然如果审核能通过,可以加入检查更新的逻辑,主要思路如下:
1.版本自动更新一般采用API对应的方式 获取当前App Store上版本号 于本地存储的版本号对比。
2.由服务端返回版本控制升级(容易审核不通过)
参考地址(很详细)
https://blog.csdn.net/lcg910978041/article/details/51426084
问题:
https://www.jianshu.com/p/9e237cd62129h5+app的特点说明
h5+app的特点是本地app只是壳子,应用整体在web服务上。app更新比较少,但是也可能涉及打包更新,更新需要在h5的web服务实现。
h5+app自动更新实现
如上为打包配置(配置实现版本更新):
var H5_SERVER = “http://www.h5net.com/m/?origin=app¤tVersion=1”;
origin:标志h5,是app访问,配置使web服务能兼容手机web和app使用。
currentVersion:app当前发布版本号。
打包参考:将H5站点打包成app完美攻略更新的核心逻辑原理跟原生app一致,核心实现检查更新代码,可以参考如下:
//app热更新下载 //假定字符串的每节数都在5位以下 function toNum(a) { //也可以这样写 var c=a.split(/\./); var c = a.split('.'); var num_place = ["", "0", "00", "000", "0000"], r = num_place.reverse(); for(var i = 0; i < c.length; i++) { var len = c[i].length; c[i] = r[len] + c[i]; } var res = c.join(''); return res; } var btn = ["确定升级", "取消"]; //获取app系统更新[是否手动点击获取更新] function appUpdate(Index) { console.log('appUpdate'); mui.plusReady(function() { plus.runtime.getProperty(plus.runtime.appid, function(inf) { ver = inf.version + ''; console.log('ver:' + ver); var client; var ua = navigator.userAgent.toLowerCase(); if(/iphone|ipad|ipod/.test(ua)) { //苹果手机 mui.ajax({ type: "get", dataType: 'json', url: "https://itunes.apple.com/lookup?id=1462614850", //获取当前上架APPStore版本信息 data: { id: 1462614850 //APP唯一标识ID }, contentType: 'application/x-www-form-urlencoded;charset=UTF-8', success: function(data) { console.log('data:' + JSON.stringify(data)); var resultCount = data.resultCount; for(var i = 0; i < resultCount; i++) { var normItem = data.results[i].version; console.log('normItem:' + normItem) if(normItem > ver) { var _msg = "发现新版本:V" + normItem; //plus.nativeUI.alert("发现新版本:V" + normItem); mui.confirm(_msg, '升级确认', btn, function(e) { if(e.index == 0) { //执行升级操作 document.location.href = 'https://itunes.apple.com/cn/app/%E5%AD%A9%E5%84%BF%E6%AC%A2/id1462614850?mt=8'; //上新APPStore下载地址 } }); return; } } if(ismanual) { mui.toast('当前版本号已是最新'); } return; } }); } else if(/android/.test(ua)) { mui.ajax(ip + "APIVApp/SelectVApp", { data: { apkVersion: ver, }, dataType: 'json', type: 'get', timeout: 10000, success: function(data) { console.log('data:' + JSON.stringify(data)) console.log(toNum(ver)) if(toNum(data[0]._vname) > toNum(ver)) { var _msg = "发现新版本:V" + data[0]._vname; mui.confirm(_msg, '升级确认', btn, function(e) { if(e.index == 0) { //执行升级操作 downWgt(); } }); } else { console.log(Index); if(Index) { mui.toast('当前版本号已是最新'); } return; } }, error: function(xhr, type, errerThrown) { mui.toast('网络异常,请稍候再试'); } }); } }); }); } // 下载wgt文件 function downWgt() { var wgtUrl = ip + "app/H5750CDB5.wgt"; plus.nativeUI.showWaiting("下载更新文件..."); plus.downloader.createDownload(wgtUrl, { filename: "_doc/update/" }, function(d, status) { if(status == 200) { console.log("下载更新文件成功:" + d.filename); installWgt(d.filename); //安装wgt包 } else { console.log("下载失败!"); plus.nativeUI.alert("下载失败!"); } plus.nativeUI.closeWaiting(); }).start(); } function installWgt(path) { plus.nativeUI.showWaiting("安装更新文件..."); plus.runtime.install(path, {}, function() { plus.nativeUI.closeWaiting(); console.log("安装更新文件成功!"); plus.nativeUI.alert("应用资源更新完成!", function() { plus.runtime.restart(); }); }, function(e) { plus.nativeUI.closeWaiting(); console.log("安装更新文件失败[" + e.code + "]:" + e.message); plus.nativeUI.alert("安装更新文件失败[" + e.code + "]:" + e.message); if(e.code == 10) { alert('请清除临时目录'); } }); }
本文是实践的一些总结,希望对你有所帮助。有什么问题可以留言讨论,有更好的实践方法也欢迎斧正指导。
参考:
h5打包app实现版本自动更新
https://www.cnblogs.com/lijia-kapok/p/6553823.html
https://blog.csdn.net/weixin_41472521/article/details/90274142
android:
https://www.cnblogs.com/zhujiabin/p/7384902.html
ios:
https://www.jianshu.com/p/9e237cd62129臭味相投的朋友们,我在这里:
猿in小站:http://www.yuanin.net
csdn博客:https://blog.csdn.net/jiabeis
简书:https://www.jianshu.com/u/4cb7d664ec4b
微信免费订阅号“猿in”
-
Android APP版本更新源码
2015-06-04 07:57:10Android APP版本更新源码,利用进度条实时显示下载进度。 -
android 后台更新app demo
2015-02-26 10:56:06android 后台更新app demo,通知栏显示进度。 -
flutter APP自动更新
2020-08-14 14:03:58近期做flutter APP框架的搭建封装,在APP自动更新这块,参考了很多网址,但都不全面;故自己动手封装了一套,主要采用flutter_downloader及progress_dialog等;在APP启动成功后,若有最新版本,便会自动弹框提示是否...flutter APP自动更新
前言
近期做flutter APP框架的搭建封装,在APP自动更新这块,参考了很多网址,但都不全面;故自己动手封装了一套,主要采用flutter_downloader及progress_dialog等;在APP启动成功后,若有最新版本,便会自动弹框提示是否更新,若更新,将会下载最新APP,并显示下载进度,下载完成后,将自动提示是否安装最新包。
此功能主要针对android APP自动更新在pubspec.yaml中安装依赖
permission_handler: 5.0.0+hotfix.4 package_info: 0.4.1 path_provider: 1.6.11 open_file: 3.0.1 flutter_downloader: 1.5.0 progress_dialog: 1.2.0
在控制台输入 flutter packages get 命令,下载依赖包
在main.dart文件中,初始化FlutterDownLoader
... import 'package:flutter_downloader/flutter_downloader.dart'; ... void main() async { WidgetsFlutterBinding.ensureInitialized(); await FlutterDownloader.initialize( debug: true ); runApp(MyApp()); } ....
配置网络
在android/app/src/main/res目录下,新建文件夹xml,在xml文件夹下,新增network_security_config.xml
在network_security_config.xml中写入以下代码<?xml version="1.0" encoding="utf-8"?> <network-security-config> <base-config cleartextTrafficPermitted="true"> <trust-anchors> <certificates src="system" /> </trust-anchors> </base-config> </network-security-config>
如下图:
在AndroidManifest.xml新增如下配置
- 新增权限
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.RECORD_AUDIO" />
- 在<application …>中新增network_security_config配置
<application android:name="io.flutter.app.FlutterApplication" android:label="flutter助手" android:icon="@mipmap/ic_launcher" android:networkSecurityConfig="@xml/network_security_config" >
如下图:
3. 在<application …>标签内加入以下代码<provider android:name="vn.hunghd.flutterdownloader.DownloadedFileProvider" android:authorities="${applicationId}.flutter_downloader.provider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths"/> </provider> <provider android:name="androidx.work.impl.WorkManagerInitializer" android:authorities="${applicationId}.workmanager-init" android:enabled="false" android:exported="false" /> <provider android:name="vn.hunghd.flutterdownloader.FlutterDownloaderInitializer" android:authorities="${applicationId}.flutter-downloader-init" android:exported="false"> <!-- changes this number to configure the maximum number of concurrent tasks --> <meta-data android:name="vn.hunghd.flutterdownloader.MAX_CONCURRENT_TASKS" android:value="5" /> </provider> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileProvider" android:exported="false" android:grantUriPermissions="true" tools:replace="android:authorities"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths" tools:replace="android:resource" /> </provider>
如下图:
在项目入口dart文件中,新增自动更新逻辑代码
- 导入相关包
import 'dart:isolate'; import 'dart:ui'; import 'dart:async'; import 'dart:io'; import 'package:package_info/package_info.dart'; import 'package:path_provider/path_provider.dart'; import 'package:open_file/open_file.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:progress_dialog/progress_dialog.dart';
- 声明变量
String serviceVersionCode = ''; String appId = ''; ProgressDialog pr; String apkName ='app-release.apk'; String appPath = ''; ReceivePort _port = ReceivePort();
-
在initState中初始化
IsolateNameServer.registerPortWithName(_port.sendPort, 'downloader_send_port'); _port.listen(_updateDownLoadInfo); FlutterDownloader.registerCallback(_downLoadCallback);
-
判断,自动更新
@override void afterFirstLayout(BuildContext context) { // 如果是android,则执行热更新 if(Platform.isAndroid){ _getNewVersionAPP(context); } }
- 自动更新代码
/// 执行版本更新的网络请求 _getNewVersionAPP(context) async { HttpUtils.send( context, 'http://update.rwworks.com:8088/appManager/monitor/app/version/check/flutterTempldate', ).then((res) { serviceVersionCode = res.data["versionNo"]; appId = res.data['id']; _checkVersionCode(); }); } /// 检查当前版本是否为最新,若不是,则更新 void _checkVersionCode() { PackageInfo.fromPlatform().then((PackageInfo packageInfo) { var currentVersionCode = packageInfo.version; if (double.parse(serviceVersionCode.substring(0,3))> double.parse(currentVersionCode.substring(0,3))) { _showNewVersionAppDialog(); } }); } /// 版本更新提示对话框 Future<void> _showNewVersionAppDialog() async { return showDialog<void>( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: new Row( children: <Widget>[ new Padding( padding: const EdgeInsets.fromLTRB(30.0, 0.0, 10.0, 0.0), child: new Text("发现新版本")) ], ), content: new Text( serviceVersionCode ), actions: <Widget>[ new FlatButton( child: new Text('下次再说'), onPressed: () { Navigator.of(context).pop(); }, ), new FlatButton( child: new Text('立即更新'), onPressed: () { _doUpdate(context); }, ) ], ); }); } /// 执行更新操作 _doUpdate(BuildContext context) async { Navigator.pop(context); _executeDownload(context); } /// 下载最新apk包 Future<void> _executeDownload(BuildContext context) async { pr = new ProgressDialog( context, type: ProgressDialogType.Download, isDismissible: true, showLogs: true, ); pr.style(message: '准备下载...'); if (!pr.isShowing()) { pr.show(); } final path = await _apkLocalPath; await FlutterDownloader.enqueue( url: 'http://update.rwworks.com:8088/appManager/monitor/app/appload/' + appId + '', savedDir: path, fileName: apkName, showNotification: true, openFileFromNotification: true ); } /// 下载进度回调函数 static void _downLoadCallback(String id, DownloadTaskStatus status, int progress) { final SendPort send = IsolateNameServer.lookupPortByName('downloader_send_port'); send.send([id, status, progress]); } /// 更新下载进度框 _updateDownLoadInfo(dynamic data) { DownloadTaskStatus status = data[1]; int progress = data[2]; if (status == DownloadTaskStatus.running) { pr.update(progress: double.parse(progress.toString()), message: "下载中,请稍后…"); } if (status == DownloadTaskStatus.failed) { if (pr.isShowing()) { pr.hide(); } } if (status == DownloadTaskStatus.complete) { if (pr.isShowing()) { pr.hide(); } _installApk(); } } /// 安装apk Future<Null> _installApk() async { await OpenFile.open(appPath + '/' + apkName); } /// 获取apk存储位置 Future<String> get _apkLocalPath async { final directory = await getExternalStorageDirectory(); String path = directory.path + Platform.pathSeparator + 'Download';; final savedDir = Directory(path); bool hasExisted = await savedDir.exists(); if (!hasExisted) { await savedDir.create(); } this.setState((){ appPath = path; }); return path; }
效果图
-
手把手带你撸一个校园APP(四):APP功能设计及主页面框架
2020-02-07 23:50:22“科师有约” 校园APP 的定位是 “校园信息聚合平台” ,那就要最大程度上利用学校现有的线上资源,比如学校官网、线上图书馆、微信公众号、微博等信息,并且聚合到我们的APP中,方便学生用户去使用。 本文主要讲解... -
uni-app 打包后 app 内部在线 全局更新 版本
2019-07-19 12:45:10领导新需求,使用uni-app 如果在线更新app版本 虽然官方有例子,但是导入官方例子却不行。如何在app 内部更新。鼓捣了挺长时间,最后解决了问题。问题挺多我就一一列出来并附上代码。 1.完成思路 数据库存好版本号... -
APP强制更新和非强制更新测试要点
2020-04-09 15:46:08----------安装一个低版本应用在手机,发布一个强制更新版本 1.强制更新需要测试的点有: 1)强制升级是否可以升级成功 从老版本的包升级到新版版的包是否可以升级成功。 2)升级后的数据是否正常 查看老账户升级.... -
Android App图标静态更新方案
2016-12-27 12:22:18不能说支付宝App的“不坚挺”,只能说众大神太厉害了~~ 今天继续和大家分享Android中动态更新的内容。上篇博客中我和大家分享了如何实现Android中动态更新View的内容,从中大家也知道了如何使用DexClassLoader来动态... -
uni-app的热更新(安卓)
2020-07-02 15:27:24热更新的好处就不说了,直接上干活(安卓为例) 1,我是用HBuilderX生成的uni-app项目,然后打包成apk。 2,热更新的思路 (1)项目中有当前版本号... (1)main.js中定义一个全局的变量或常量 import ... -
uni-app在App平台如何实现升级更新?
2020-11-05 19:06:09使用 uni-app 开发,可将代码编译到iOS、Android、微信小程序等多个平台,升级时也需考虑多平台同步升级。 uni-app发布为小程序的升级模式较简单,只需将开发完的代码提交小程序后台,待审核通过后用户将自动升级。 ... -
加密相册、保险箱App打开就闪退、点击提示App Store不提供应用、需要更新才能使用的解决方案
2020-03-30 18:26:05遇到任何问题不要删除App,删除会丢失数据,请先联系技术支持QQ: 673368731,官方技术支持QQ群:961952432,技术支持微博:加密相册,微信公众号:加密相册app 1、加密相册、保险箱在iOS12下打开就闪退的,关闭所有... -
flutter app内更新升级
2020-01-10 21:33:56用flutter开发了一个简单的跨平台app,在网上找了很多 app内升级的博客,大部分都是复制的,不过有一篇好文章推荐给大家,重点是引用的插件尽量和此博客中的一样,否则编译时各种报错,Flutter 项目 app迭代更新 ... -
数字人民币APP更新后 打不开/闪退 的解决方法
2021-12-13 17:32:05把你的ROOT给我删了! 隐藏ROOT的方法我基本都试过了,没用,先把ROOT删除,然后重启手机,重装数字人民币APP。 -
一个简单粗暴的方法让后台数据改变的时候app端自动更新
2017-01-09 10:08:23一个简单粗暴的方法让后台数据改变的时候app端自动更新 -
App热更新原理
2017-12-09 16:24:37我们知道Java在运行时加载对应的类是通过ClassLoader来实现的,ClassLoader本身是一个抽象来,Android中使用PathClassLoader类作为Android的默认的类加载器, PathClassLoader其实实现的就是简单的从文件系统中... -
Android——app的版本更新(强制更新/非强制更新)
2017-12-29 08:45:501.App版本检测:要实现App的更新下载,我们上面介绍了,前提是服务器要保存一个App的版本号(通常的方式是保存versionCode,当然你要对比versionName也没关系)。当用户去手动检测版本,或者进入首页自动检测时,第... -
IOS开发入门之二——第一个App
2018-05-31 22:01:31如果你对怎么开始IOS... 本章将教大家创建一个标准的苹果手机应用并让它在手机模拟器上运行起来。 需要iOS开发视频资料可以加我微信: 1914532832 验证信息请注明:IOS开发 一、创建IOS工程 1. 打开Xcode软... -
老ipad不能下载和更新app store中的app解决方法
2020-02-12 11:22:34有一天,孩子上公开课,需要在store中下载新的app,结果死活都无法下载,没有打app客服之前,在网上查找过解决方案,总结一下 方法1.在无线网络设置中,修改wifi的dns,后面这三个dns都可以设置一下 :(114.114.114... -
iOS-app更新和强制更新
2017-03-09 15:04:10如出现重大更新,如果用户不更新,这个app都用不下去了。这个时候就要强制用户更新。 2. 第二位作为功能版本号。比如增加了一些新的功能。这个时候通过增加这个版本号,来添加功能。 3. 第三位作为修订版本号。如... -
H5 App实现热更新,不需要重新安装app
2019-05-16 23:05:43//app热更新下载 //假定字符串的每节数都在5位以下 function toNum(a) { //也可以这样写 var c=a.split(/\./); var c = a.split('.'); var num_place = ["", "0", "00", "000", "0000"], r = num_plac... -
Retrofit实现App更新
2016-06-01 00:07:26Github代码:https://github.com/SpikeKing/wcl-update-request-demo逻辑访问服务器, 根据是否包含新版本, 判断是否需要更新. 下载Apk, 下载完成后, 自动安装, 高版本会覆盖低版本.逻辑:public class MainActivity ... -
Anroid app版本更新
2018-06-03 10:40:171.前言 Android app更新是app必须拥有的功能,上线之后,可以强制用户更新也可以提示用户有新版,之所以写这篇文章,是为了介绍app版本更新的思路,内容还是比较简单的。 有人可能会说,现在比较了流行Hotfix也... -
手把手带你撸一个校园APP(一):项目简介
2018-06-23 23:22:02这个项目很早之前就在做,是为我大学母校(河北科技师范学院)做的一个项目,所以还是有很深的感情的。整体项目基于 Bmob 后端云来实现,其实功能和技术都并不算复杂,自己记录一下,也给需要的朋友们提供一些参考和... -
关于mui开发的APP自动更新的问题
2018-09-15 14:56:02关于APP自动更新的问题,可是有点坑啊,以下就是我遇到的问题 话不多说,直接上代码 1.先进行获取手机APP当前的信息 const wgtVer = null; //获取当前版本号 plus.runtime.getProperty(plus.... -
android 8.0 强制更新 或升级app 没反应 不跳转安装界面
2018-11-01 11:45:01最近用8.0的手机测试检查更新,发现下载完了,屏幕闪了一下,没有跳转到系统安装界面 然后用7.0的手机测试,是可以跳转到安装app界面 解决方案: android 8.0安装apk需要请求未知来源权限 在项目的配置文件 ... -
Android如何实现APP自动更新
2016-11-17 11:02:04对于安卓用户来说,手机应用市场说满天飞可是一点都不夸张,比如小米,魅族,百度,360,机锋,应用宝等等,当我们想上线一款新版本APP时,先不说渠道打包的麻烦,单纯指上传APP到各大应用市场的工作量就已经很大了...