我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Android中的全量更新、增量更新以及热更新

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Android中的全量更新、增量更新以及热更新

在客户端开发过程中,我们可能会遇到这样一种需求:点击某个按钮弹出一个弹窗,提示我们可以更新到apk的某个版本,或者我们可以通过服务端接口进行强制更新。在这种需求中,我们是不需要通过应用商店来更新我们的apk的,而是直接在apk内部进行版本更新。这次我们就来看看实现这种应用内更新的几种方式。当然,这种玩法只能在国内玩,海外的话会被Googleplay据审的。如果是海外的应用要更新apk,只能在GooglePlay上上传新版本的包。

全量更新

什么是全量更新呢?举个例子,假设现在用户手机上的apk是1.0版本,如果想要升级到2.0版本,全量更新的处理方式则是把2.0版本的apk全部下载下来进行覆盖安装。那么,我们该如果设计一个合理的全量更新方案呢?

  • 服务端
    需要提供一个接口,这个接口返回来的body中包含新版本的包的下载地址以及该包的md5值用于下载完成之后进行校验用
  • 客户端
    访问该服务端接口,下载新版本的包(其实就是字节流的读写),然后进行覆盖安装

做完上面这2点其实就可以实现一个较为完整的全量更新功能。

客户端核心代码如下:

package com.mvp.myapplication.update;import android.app.Service;import android.content.ComponentName;import android.content.Context;import android.content.Intent;import android.content.ServiceConnection;import android.content.pm.PackageManager;import android.content.pm.ProviderInfo;import android.net.Uri;import android.os.AsyncTask;import android.os.Binder;import android.os.Build;import android.os.Environment;import android.os.IBinder;import android.text.TextUtils;import android.util.Log;import androidx.core.content.FileProvider;import com.mvp.myapplication.utils.MD5Util;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.io.InputStream;import java.net.HttpURLConnection;import java.net.URL;public class UpdateService extends Service {    public static final String KEY_MD5 = "MD5";    public static final String URL = "downloadUrl";    private boolean startDownload;//开始下载    public static final String TAG = "UpdateService";    private DownloadApk downloadApkTask;    private String downloadUrl;    private String mMd5;    private UpdateProgressListener updateProgressListener;    private LocalBinder localBinder = new LocalBinder();    public class LocalBinder extends Binder {        public void setUpdateProgressListener(UpdateProgressListener listener) {            UpdateService.this.setUpdateProgressListener(listener);        }    }    private void setUpdateProgressListener(UpdateProgressListener listener) {        this.updateProgressListener = listener;    }        private static String getFileProviderAuthority(Context context) {        try {            for (ProviderInfo provider : context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_PROVIDERS).providers) {                if (FileProvider.class.getName().equals(provider.name) && provider.authority.endsWith(".update_app.file_provider")) {                    return provider.authority;                }            }        } catch (PackageManager.NameNotFoundException ignore) {        }        return null;    }    private static Intent installIntent(Context context, String path) {        Intent intent = new Intent(Intent.ACTION_VIEW);        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);        intent.addCategory(Intent.CATEGORY_DEFAULT);        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {            Uri fileUri = FileProvider.getUriForFile(context, getFileProviderAuthority(context), new File(path));            intent.setDataAndType(fileUri, "application/vnd.android.package-archive");            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);        } else {            intent.setDataAndType(Uri.fromFile(new File(path)), "application/vnd.android.package-archive");        }        return intent;    }    public UpdateService() {    }    @Override    public int onStartCommand(Intent intent, int flags, int startId) {        if (!startDownload && intent != null) {            startDownload = true;            mMd5 = intent.getStringExtra(KEY_MD5);            downloadUrl = intent.getStringExtra(URL);            downloadApkTask = new DownloadApk(this, mMd5);            downloadApkTask.execute(downloadUrl);        }        return super.onStartCommand(intent, flags, startId);    }    @Override    public IBinder onBind(Intent intent) {        return localBinder;    }    @Override    public boolean onUnbind(Intent intent) {        return true;    }    @Override    public void onDestroy() {        if (downloadApkTask != null) {            downloadApkTask.cancel(true);        }        if (updateProgressListener != null) {            updateProgressListener = null;        }        super.onDestroy();    }    private static String getSaveFileName(String downloadUrl) {        if (downloadUrl == null || TextUtils.isEmpty(downloadUrl)) {            return System.currentTimeMillis() + ".apk";        }        return downloadUrl.substring(downloadUrl.lastIndexOf("/"));    }    private static File getDownloadDir(UpdateService service) {        File downloadDir = null;        if (Environment.getExternalStorageDirectory().equals(Environment.MEDIA_MOUNTED)) {            downloadDir = new File(service.getExternalCacheDir(), "update");        } else {            downloadDir = new File(service.getCacheDir(), "update");        }        if (!downloadDir.exists()) {            downloadDir.mkdirs();        }        return downloadDir;    }    private void start() {        if (updateProgressListener != null) {            updateProgressListener.start();        }    }    private void update(int progress) {        if (updateProgressListener != null) {            updateProgressListener.update(progress);        }    }    private void success(String path) {        if (updateProgressListener != null) {            updateProgressListener.success(path);        }        Intent i = installIntent(this, path);        startActivity(i);//自动安装        stopSelf();    }    private void error() {        if (updateProgressListener != null) {            updateProgressListener.error();        }        stopSelf();    }    private static class DownloadApk extends AsyncTask<String, Integer, String> {        private final String md5;        private UpdateService updateService;        public DownloadApk(UpdateService service, String md5) {            this.updateService = service;            this.md5 = md5;        }        @Override        protected void onPreExecute() {            super.onPreExecute();            if (updateService != null) {                updateService.start();            }        }        @Override        protected String doInBackground(String... strings) {            final String downloadUrl = strings[0];            final File file = new File(UpdateService.getDownloadDir(updateService),                    UpdateService.getSaveFileName(downloadUrl));            Log.d(TAG, "download url is " + downloadUrl);            Log.d(TAG, "download apk cache at " + file.getAbsolutePath());            File dir = file.getParentFile();            if (!dir.exists()) {                dir.mkdirs();            }            HttpURLConnection httpConnection = null;            InputStream is = null;            FileOutputStream fos = null;            long updateTotalSize = 0;            URL url;            try {                url = new URL(downloadUrl);                httpConnection = (HttpURLConnection) url.openConnection();                httpConnection.setConnectTimeout(20000);                httpConnection.setReadTimeout(20000);                Log.d(TAG, "download status code: " + httpConnection.getResponseCode());                if (httpConnection.getResponseCode() != 200) {                    return null;                }                updateTotalSize = httpConnection.getContentLength();                if (file.exists()) {                    if (updateTotalSize == file.length()) {                        // 下载完成                        if (TextUtils.isEmpty(md5) || MD5Util.getMD5String(file).toUpperCase().equals(md5.toUpperCase())) {return file.getAbsolutePath();                        }                    } else {                        file.delete();                    }                }                file.createNewFile();                is = httpConnection.getInputStream();                fos = new FileOutputStream(file, false);                byte buffer[] = new byte[4096];                int readSize = 0;                long currentSize = 0;                while ((readSize = is.read(buffer)) > 0) {                    fos.write(buffer, 0, readSize);                    currentSize += readSize;                    publishProgress((int) (currentSize * 100 / updateTotalSize));                }                // download success            } catch (Exception e) {                e.printStackTrace();                return null;            } finally {                if (httpConnection != null) {                    httpConnection.disconnect();                }                if (is != null) {                    try {                        is.close();                    } catch (IOException e) {                        e.printStackTrace();                    }                }                if (fos != null) {                    try {                        fos.close();                    } catch (IOException e) {                        e.printStackTrace();                    }                }            }            try {                if (TextUtils.isEmpty(md5) || MD5Util.getMD5String(file).toUpperCase().equals(md5.toUpperCase())) {                    return file.getAbsolutePath();                }            } catch (IOException e) {                e.printStackTrace();                return file.getAbsolutePath();            }            Log.e(TAG, "md5 invalid");            return null;        }        @Override        protected void onProgressUpdate(Integer... values) {            super.onProgressUpdate(values);            if (updateService != null) {                updateService.update(values[0]);            }        }        @Override        protected void onPostExecute(String s) {            super.onPostExecute(s);            if (updateService != null) {                if (s != null) {                    updateService.success(s);                } else {                    updateService.error();                }            }        }    }    public static class Builder {        private String downloadUrl;        private String md5;        private ServiceConnection serviceConnection;        protected Builder(String downloadUrl) {            this.downloadUrl = downloadUrl;        }        public static Builder create(String downloadUrl) {            if (downloadUrl == null) {                throw new NullPointerException("downloadUrl == null");            }            return new Builder(downloadUrl);        }        public String getMd5() {            return md5;        }        public Builder setMd5(String md5) {            this.md5 = md5;            return this;        }        public Builder build(Context context, UpdateProgressListener listener) {            if (context == null) {                throw new NullPointerException("context == null");            }            Intent intent = new Intent();            intent.setClass(context, UpdateService.class);            intent.putExtra(URL, downloadUrl);            intent.putExtra(KEY_MD5, md5);            UpdateProgressListener delegateListener = new UpdateProgressListener() {                @Override                public void start() {                    if (listener != null) {                        listener.start();                    }                }                @Override                public void update(int var1) {                    if (listener != null) {                        listener.update(var1);                    }                }                @Override                public void success(String path) {                    try {                        context.unbindService(serviceConnection);                    } catch (Throwable t) {                        Log.e("UpdateService", "解绑失败" + t.getMessage());                    }                    if (listener != null) {                        listener.success(path);                    }                }                @Override                public void error() {                    try {                        context.unbindService(serviceConnection);                    } catch (Throwable t) {                        Log.e("UpdateService", "解绑失败" + t.getMessage());                    }                    if (listener != null) {                        listener.error();                    }                }            };            serviceConnection = new ServiceConnection() {                @Override                public void onServiceConnected(ComponentName name, IBinder service) {                    LocalBinder binder = (LocalBinder) service;                    binder.setUpdateProgressListener(delegateListener);                }                @Override                public void onServiceDisconnected(ComponentName name) {                }            };            context.bindService(intent, serviceConnection, Context.BIND_IMPORTANT);            context.startService(intent);            return this;        }    }    public interface UpdateProgressListener {        void start();        void update(int var);        void success(String path);        void error();    }}
package com.mvp.myapplication;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.util.Log;import android.view.View;import android.widget.Button;import com.mvp.myapplication.update.UpdateService;public class MainActivity extends AppCompatActivity {    private Button btnAllUpdate, btnAddUpdate, btnHotUpdate;    private String url,md5;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        btnAddUpdate = findViewById(R.id.btn_add_update);        btnAllUpdate = findViewById(R.id.btn_all_update);        btnHotUpdate = findViewById(R.id.btn_hot_update);        Log.e("MainActivity","onCreate");        btnAllUpdate.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                UpdateService.Builder.create(url)                        .setMd5(md5)                        .build(MainActivity.this, new UpdateService.UpdateProgressListener() {@Overridepublic void start() {    Log.e("MainActivity", "start");}@Overridepublic void update(int var) {    Log.e("MainActivity", "update ===> " + var);}@Overridepublic void success(String path) {    Log.e("MainActivity", "success ===> " + path);}@Overridepublic void error() {    Log.e("MainActivity", "error");}                        });            }        });    }}
  • AndroidManifest
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools">    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>    <application        android:allowBackup="true"        android:dataExtractionRules="@xml/data_extraction_rules"        android:fullBackupContent="@xml/backup_rules"        android:icon="@mipmap/ic_launcher"        android:label="@string/app_name"        android:roundIcon="@mipmap/ic_launcher_round"        android:supportsRtl="true"        android:theme="@style/Theme.MyApplication"        tools:targetApi="31">        <service            android:name=".update.UpdateService"            android:enabled="true"            android:exported="true"></service>        <activity            android:name=".MainActivity"            android:exported="true">            <intent-filter>                <action android:name="android.intent.action.MAIN" />                <category android:name="android.intent.category.LAUNCHER" />            </intent-filter>            <meta-data                android:name="android.app.lib_name"                android:value="" />        </activity>        <!-- Android7以上需要 -->        <provider            android:name="androidx.core.content.FileProvider"            android:authorities="${applicationId}.update_app.file_provider"            android:exported="false"            android:grantUriPermissions="true"            tools:replace="android:authorities">            <meta-data                android:name="android.support.FILE_PROVIDER_PATHS"                android:resource="@xml/update_app_path" />        </provider>    </application></manifest>
  • update_app_path
<?xml version="1.0" encoding="utf-8"?><paths>    <cache-path        name="update_app_cache_files"        path="/update" />    <external-path        name="update_app_external_files"        path="/" />    <external-cache-path        name="update_app_external_cache_files"        path="/update" /></paths>

热更新

严格意义上来说,个人认为热更新并不是用来进行包体升级,更多的用来进行修复bug的。例如,由于某个程序员的失误,在某个类中抛出了一个空指针异常,导致程序执行到该类后一直崩溃。这种情况下,其实就可以使用热更新来处理。因为,我们并没有大改app中的功能,只是某个类报错了。但个人认为热更新其实也不能解决所有的奔溃问题的,这些黑科技或多或少都是有一些兼容性的问题的,像Tinker就必须要冷启动才能修复,而且受限于Android的版本。
具体是技术实现方式可以参考笔者之前写的一篇博客:Android热修复1以及Android热更新十:自己写一个Android热修复

增量更新

什么是增量更新呢?举了例子,假设我们需要将apk从v1.0升级到v2.0,这时我们可以通过全量更新的方式,下载2.0版本的apk然后进行覆盖安装。但是,一般情况下2.0版本的apk往往包含了1.0版本的功能,理论上我们只需要下载二者的差分包,然后将差分包与1.0版本的包进行合并即可生成一个2.0版本的包。这样做的好处自然就是节约了流量了。像几乎所有的应用商店都使用增量更新的方式来更新apk。那么,我们该我们使用增量更新呢?这个就要借助一个工具:bsdiff
注意:如果想要使用增量更新,那么必须要有一个旧版本的apk,如果用户安装完apk后直接把旧版本的apk删掉了,那么还是老老实实使用全量更新的方式吧。

  • 拆——拆分出差分包

bsdiff oldfile newfile1 patchfile

  • 合——将旧版本的包与差分包进行合并

bspatch oldfile newfile2 patchfile

使用上面两步便可以完成差分包的拆分与合并,新生成的newfile2 与newfile1的md5是一致的。但是上面这两步法我们是在pc端进行的,我们该如何在代码中实现上面的逻辑呢?首先,拆分的逻辑还是在pc端中进行,客户端只需要关注如何合并差分包。
首先,我们需要导入bspatch相关的类
在这里插入图片描述
接着,我们新建一个类用于调用c相关的代码:

package com.mvp.myapplication.utils;public class BSPatchUtil {    // Used to load the 'native-lib' library on application startup.    static {        System.loadLibrary("bspatch");    }        public static native int bspatch(String oldApkPath, String outputApkPath,       String patchPath);}
package com.mvp.myapplication;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.os.Environment;import android.util.Log;import android.view.View;import android.widget.Button;import com.mvp.myapplication.update.UpdateService;import com.mvp.myapplication.utils.BSPatchUtil;import java.io.File;public class MainActivity extends AppCompatActivity {    private Button btnAllUpdate, btnAddUpdate, btnHotUpdate;    private String url, md5;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        btnAddUpdate = findViewById(R.id.btn_add_update);        btnAllUpdate = findViewById(R.id.btn_all_update);        btnHotUpdate = findViewById(R.id.btn_hot_update);        Log.e("MainActivity", "onCreate");        btnAllUpdate.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                UpdateService.Builder.create(url)                        .setMd5(md5)                        .build(MainActivity.this, new UpdateService.UpdateProgressListener() {@Overridepublic void start() {    Log.e("MainActivity", "start");}@Overridepublic void update(int var) {    Log.e("MainActivity", "update ===> " + var);}@Overridepublic void success(String path) {    Log.e("MainActivity", "success ===> " + path);}@Overridepublic void error() {    Log.e("MainActivity", "error");}                        });            }        });        btnAddUpdate.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                genNewApk();            }        });    }    private void genNewApk() {        String oldpath = getApplicationInfo().sourceDir;        String newpath = (this.getCacheDir().getAbsolutePath()+ File.separator                + "composed_hivebox_apk.apk");        String patchpath = (this.getCacheDir().getAbsolutePath()+ File.separator                + "bs_patch");        Log.e("MainActivity", "oldpath is " + oldpath + "\n newpath is " + newpath + "\n patchpath is " + patchpath);        BSPatchUtil.bspatch(oldpath, newpath, patchpath);    }}

注意:需要修改bspatch.c文件中的Java_com_mvp_myapplication_utils_BSPatchUtil_bspatch方法签名:改为BSPatchUtil 的包名,例如BSPatchUtil对应的路径为com.dxl.testbatch.util.BSPatchUtil,那么native方法签名就是Java_com_mvp_myapplication_utils_BSPatchUtil_bspatch
这样便可以通过jni调用到c层面的代码。

接着,修改build.gradle文件,添加下面圈中的闭包
在这里插入图片描述
最后,我们执行Make Project命令,正常情况下便可以生成如下几个so库
在这里插入图片描述
最后,我们在把so库放入jniLibs文件夹中,然后build下生成apk包
在这里插入图片描述

然后,我们将差分包bs_patch放入手机的data/data目录下,点击按钮就会生成composed_hivebox_apk.apk这个apk包,将其与v2.0的包进行MD5对比,发现是一致的。如此,我们便实现了一个简单的增量更新逻辑。
Demo地址:https://gitee.com/hzulwy/add_-update/tree/master/MyApplication

来源地址:https://blog.csdn.net/qq_36828822/article/details/129852751

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Android中的全量更新、增量更新以及热更新

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

mybatis-plus的批量新增/批量更新以及问题

这篇文章主要介绍了Mybatis-Plus实现批量新增与批量更新以及出现的问题,文章中有详细的代码示例,感兴趣的同学可以参考一下
2023-05-15

Android-App增量更新的使用姿势

简述 增量更新,根据字面理解,就是下载增加的那部分来达到更新的目的,实际就是这个意思。 原理 用一个旧的Apk安装与一个新的Apk安装包使用 bsdiff工具 ,执行命令生成一个差异文件,此差异文件就是我们修改需要更新下载的那部分。 引入代
2022-06-06

Android App增量更新详解及实例代码

Android App增量更新实例--Smart App Updates 介绍你所看到的,是一个用于Android应用程序增量更新的开源库。包括客户端、服务端两部分代码。原理自从 Android 4.1 开始,Google引入
2022-06-06

Win10:如何借助Windows手动更新下载安装增量更新

微软已经表示,他们将在Windows 10 beta版生命周期内发布多个增量更新,这意味着用户需要经常更新现有版本到新版本,一直到Windows 编程客栈10 RTM出现为止。下面就是用户检测增量更新和安装增量更新的办法。点击开始菜单,并在
2023-06-08

mybatis-plus的批量新增/批量更新问题怎么解决

本文小编为大家详细介绍“mybatis-plus的批量新增/批量更新问题怎么解决”,内容详细,步骤清晰,细节处理妥当,希望这篇“mybatis-plus的批量新增/批量更新问题怎么解决”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一
2023-07-06

mybatisplus添加真正的批量新增、批量更新的实现方法

这篇文章主要介绍mybatisplus添加真正的批量新增、批量更新的实现方法,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!使用mybatis-plus来进行批量新增和更新时,你会发现其实是一条条sql执行,下面进行优
2023-06-14

帝国模板变量更新后页面不更新原因及解决

如果你用的VFYgX是动www.cppcns.com态页编程客栈面,动态页面有缓存,要马上更新缓存到数据更新编程客栈中心删除临时文件就能更新 如果你用的是www.cppcns.com静态页面,生成内容页时要选择刷新全部才会更新已经生成的信息
2022-06-12

详解Android中实现热更新的原理

这篇文章就来介绍一下Android中实现热更新的原理。 一、ClassLoader 我们知道Java在运行时加载对应的类是通过ClassLoader来实现的,ClassLoader本身是一个抽象来,Android中使用PathClassLo
2022-06-06

python中的热更新或动态加载

遍览网络中关于动态加载模块的文章,发现有两种方法,一种是用守护进程的方法,一种是用python自带的reload函数。比较靠谱的文章是如下两篇:https://my.oschina.net/1123581321/blog/168720htt
2023-01-31

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录