android home键返回桌面再次点击应用图标应用重启

本文探讨了从桌面启动应用与通过安装程序启动应用的不同行为,特别是后者可能导致重复加载登录界面的问题。通过深入分析Android系统中Activity的Task管理和桌面启动管理机制,揭示了这一现象的根本原因。

下文转自https://blog.youkuaiyun.com/shangming150/article/details/78343103

今天测试MM偶尔发现了这么个神奇的现象:给测试MM发了个应用安装包,MM安装后打开应用,进入主界面后,点击home键返回了桌面,然后从桌面再点击应用图标再次打开应用,这时候神奇的事情发生了~~~竟然打开了应用的登录界面(MAIN_action的Activity)!这不科学啊,这不是应该之前停留的界面么!~

我自测了一遍,按照测试MM的操作就出现问题,但我自己用adb命令安装后启动app就没有问题!这真吭啊~~ 赶紧分析分析找找解决办法吧~

现象分析:
  大致说一下项目结构,MAIN-Activity是登录界面(普通的Activity,启动模式为standard),然后根据一堆初始化操作和判断,一般是接着进入【主页Activity】(Activity的启动模式为singleTask);点击home键不做任何拦截处理,按照系统默认逻辑返回Lanuch桌面。
  也就是说,app的整体交互逻辑并没有特殊之处,并非业务逻辑导致的bug。那么回顾下不同的地方,也就是启动App的入口的区别了,一者是平常的桌面Icon图标启动,一者是安装程序启动入口。我们都知道,桌面启动的话也是通过startActivity这个api通过特定的Intent向ActivityManagerServer发起启动任务;所以我们可以推导出安装程序中的【打开】,也是通过Intent启动对应的App。
  再往下分析的话,可能需要一些前置知识需要了解才能更好的理解。

前置知识

1、Activity的Task管理
  一般来说,整个Android系统的App启动与切换管理依赖于相关Activity的Task的管理。一个Task之中可能含有若干个Activity,为了简便起见,我们这里记录【Task A】的Activity分别为 【A1】 、【A2】等,【Task B】的Activity分别为 【B1】 、【B2】。
那么我们来分析下App之间是怎么切换的。
  假设应用都是单Task应用(相对于大部分的普通App来说,都是采用单一Task来管理的)
  桌面程序App:【TaskA】 —- 存在Activity有【A1】 —- 其栈的结构为 A1
  应用程序B:【TaskB】 —- 存在Activity有【B1】【B2】 —- 其栈的结构为 B1B2
  应用程序C: 【TaskC】 —- 存在Activity有【C1】【C2】 —- 其栈的结构为 C1C2
  
a、那么我们进入桌面时:Task之间的结构是 A1 —- 也就是只有一个【TaskA】栈(桌面Task),并且位于最前端(这里表现为最后添加的末端)

b、然后我们点击应用程序B的图标,启动B :Task之间的结构是 A1B1B2 —- 添加了一个【TaskB】,而且【TaskB】也是位于最前端,现在显示的是【TaskB】的B2的Activity的界面

c、接着点击home键: Android对于home做了特殊默认处理,就是会把桌面Task挪到所以Task最前端,Task结构应该变成 B1B2A1 —- 【TaskA】挪到队列最前端,现在显示的是【TaskA】的A1的Activity的界面,也就是桌面

d、我们再在桌面点击应用程序C的图标,启动C : Task之间的结构变成 B1B2A1C1C2 —- 添加了一个【TaskC】,而且【TaskC】也是位于最前端,现在显示的是【TaskC】的C2的Activity的界面

从上面的例子,我们可以大致了解到Android是怎么管理不同app之间切换的逻辑:
  我们编写任何一个Activity的时候,都可以在AndroidManifest里面显式指定一个taskAffinity的属性,也就是说该Activity归属于对应taskAffinity的栈;如果没有指定任何taskAffinity,那么该Activity将会直接归属于包名所在的Task之下。而我们启动一个Activity时(这里只讨论standard启动模式),那么回去先搜寻对应的Task是否存在,如果不存在,新建一个Task并将Activity入栈,如果已经存在对应的Task,那么直接在对应Task入栈即可。
  那么问题来了:如果我们在上面第d步点击的图片并不是程序C的图标,而是重新点击了程序B的图标,此时【TaskB】是已经存在的了,那么为了不会讲B的入口activity(B1)直接在【TaskB】入栈,而是将【TaskB】挪到前台并不做任何Activity启动的操作呢?

2、桌面的启动管理
回头研究下AndroidManifest这个文件,我们轻而易举发现,但凡是App入口Activity,那么一定会包含

<intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

这几行代码。这里到底有什么玄机呢?其实这个就是跟桌面约定好的启动拦截过滤器。因为桌面有一个很明显的需求就是,如果我们再次点击已经在后台的App图标时,是应该将该后台任务挪到前台而不是再次启动该App程序。

而从柯元旦所著的《android内核剖析》一书中有记录如下规则:

每次启动Intent导致新创建Task的时候,该Task会记录导致其创建的Intent;而如果后续需要有一个新的与创建Intent完全一致(完全一致定位为:启动类,action、category等等全部一样,不可多项也不可缺少),那么该Intent并不会触发Activity的新建启动,而只会将已经存在的对应Task移到前台;这也就是为什么桌面会在再次点击图标时将后台任务挪到前台而不是重新启动App的实现。

  
  那么为啥要指定入口Activity特定的action和category呢,有一个原因我们可以确定,就是为了让桌面启动app所用的Intent具有特殊性,也就是添加了特别的拦截器,避免其他应用内或者应用间的Intent对于这个启动方式的干扰。
  
说了这么多,我们可以着手分析上续bug的产生原因了。

原理剖析

  从以上的前置知识分析,我们大致得到这样的结论:安装程序直接打开APP,它的启动Intent和我们从桌面启动APP时用到的Intent不一致!
  
我们将桌面的Task记为【TaskL】,安装程序的Task记为【TaskQ】,我们应用的Task记为【TaskA】,那么分析如下:
进入桌面: L1 —- L1是单纯的桌面
安装程序,进入app安装: L1Q1Q2 —- Q2是安装完毕后询问是否启动对应安装程序的Activity
点击打开: L1Q1Q2A1A2 —- A1是入口登录界面,A2是主页Activity
返回桌面: Q1Q2A1A2L1 —- 回到桌面页,也就是L1前置
点击A的图标: Q1Q2L1A1A2A1 —- 找到【TaskA】,挪到前台;但此时该TaskA判断启动它的Intent和之前启动它的Intent不一致,所以重新启动了一个Activity到TaskA中;

bug出现了!!! 再次启动了登录界面,问题定位成功!

PS:如果主界面的launchMode为singleTask模式,那么在【点击打开】这一步时,task中存在的Activity为:L1Q1Q2A1A2,因为A1会被finish掉!

解决办法

让那些安装程序修正其启动Intent的设置,使其与原声桌面启动Intent保持完全一致。(PS:基本不可能)
自身业务代码规避:如果是多余的入口Activity,其基本不可能位于Task的根部,而正常启动的入口Activity必定在对应的Task的根部。所以我们可以从这个地方对于这个bug进行规避,方法就是在入口Activity的onCreate代码加入如下一段代码:

// 避免从桌面启动程序后,会重新实例化入口类的activity
if (!this.isTaskRoot()) { // 当前类不是该Task的根部,那么之前启动
   Intent intent = getIntent();
   if (intent != null) {
      String action = intent.getAction();
      if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(action)) { // 当前类是从桌面启动的
         finish(); // finish掉该类,直接打开该Task中现存的Activity
         return;
      }
   }
}
<think>嗯,用户这次想了解Android应用图标的热更新实现方式,看来ta对动态更新App图标的技术很感兴趣。从引用资料来看,用户已经查阅过一些相关资料,但可能对具体实现细节还不够清晰。用户引用的三篇资料分别提到了:1.主流App在节日时更换图标的现象2.利用activity-aliasPackageManager实现动态图标3.热更新相关的Intentflag处理其中引用[2]的技术内容特别关,提到了activity-aliasPackageManager这两个核心技术。用户应该需要具体的实现步骤,而不是泛泛而谈的概念。考虑到用户明确提到"热更新",需要特别注意:真正的热更新是指不重启应用就生效,但Android图标更换必须重启桌面进程(即Launcher)。这在回答时必须澄清,避免误导用户。用户可能是个中级Android开发者,因为ta能准确使用"热更新"这样的专业术语。不过ta可能忽略了系统限制,需要提醒实际存在的约束条件。ta的情绪似乎比较急切,想尽快实现这个功能。准备从这几个方面组织回答:1.先澄清技术限制(必须重启桌面)2.分步骤讲解核心实现方案3.补充版本兼容性处理4.提供资源更新方案5.加入防崩溃等注意事项6.最后提醒第三方Launcher的限制尤其要强调BuildConfig的使用技巧,这是引用[2]提到的亮方案,能提升代码可维护性。还要提到资源加密的方案,因为商业应用可能需要保护图标资源不被反编译。相关问题设计要引导用户思考更深层问题:-如何实现多入口图标(不同活动对应不同图标)-如何结合远程配置动态控制图标-如何实现无感切换(用户提到的"悄悄变"效果)</think> 基于您对 **Android 应用图标热更新** 的需求,结合引用资料的技术要,以下是清晰、可靠的实现方案: --- ### 核心实现步骤(基于 `activity-alias` + `PackageManager`) #### 1. **声明备选图标入口** 在 `AndroidManifest.xml` 中,为默认启动图标(`MainActivity`)声明 `activity-alias` 作为替换入口: ```xml <application> <activity android:name=".MainActivity" android:icon="@mipmap/ic_default"> <intent-filter>...</intent-filter> </activity> <!-- 声明节日图标入口(示例:春节图标) --> <activity-alias android:name=".LauncherAliasSpringFestival" android:targetActivity=".MainActivity" android:icon="@mipmap/ic_spring_festival" <!-- 目标图标资源 --> android:enabled="false"> <!-- 初始禁用 --> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity-alias> </application> ``` #### 2. **动态切换激活状态** 通过 `PackageManager` 在运行时启用目标入口,禁用旧入口: ```java // 禁用当前图标入口(例如默认图标) ComponentName defaultIcon = new ComponentName(this, "com.example.MainActivity"); ComponentName festivalIcon = new ComponentName(this, "com.example.LauncherAliasSpringFestival"); PackageManager pm = getPackageManager(); pm.setComponentEnabledSetting( defaultIcon, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, // 禁用旧图标 PackageManager.DONT_KILL_APP // 不杀进程 ); // 启用新图标入口 pm.setComponentEnabledSetting( festivalIcon, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, // 启用新图标 PackageManager.DONT_KILL_APP ); ``` #### 3. **强制刷新桌面进程** 由于Android重启Launcher才能更新图标,需触发桌面更新: ```java // 发送刷新Launcher的广播 Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); ``` --- ### 关优化技巧 1. **避免硬编码别名名称** 在 `BuildConfig` 中维护别名常量,确保灵活性: ```java // build.gradle android { buildTypes { debug { buildConfigField "String", "FESTIVAL_ALIAS", "\"com.example.LauncherAliasSpringFestival\"" } } } ``` 2. **图标资源热更新** - 通过网络下载新图标应用目录(如 `files/`) - 通过 `ResourceManager` 动态加载外部资源 ```java // 示例:从文件加载图标资源 Drawable newIcon = Drawable.createFromPath("/data/data/com.example/files/ic_new.png"); ``` 3. **版本兼容性** Android 7.0+ 需使用 `FileProvider` 安全访问文件: ```java if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Uri iconUri = FileProvider.getUriForFile(this, AUTHORITY, newIconFile); // 授权临时读写权限 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } ``` --- ### 注意事项 1. **桌面刷新延迟**:部分第三方Launcher(如小米、华为)需手动重启才能生效 2. **进程限制**:系统可能自动清理后台进程导致图标切换失败,建议结合前台服务执行操作 3. **资源加密**:若图标通过网络下载,需加密存储防篡改 4. **回滚机制**:保留旧图标入口,便于紧急恢复 > **技术本质**:此方案通过 **动态切换`android:icon`属性指向的资源** **激活/禁用`activity-alias`** 实现图标更新,实际修改的是应用的 **入口组件配置**,而非直接修改APK文件[^2]。 --- ### 相关问题拓展 1. **如何实现节日图标的自动切换?** - 方案:通过 `WorkManager` 定时检查远程配置(如Firebase Remote Config),触发图标更新逻辑 2. **如何确保Android 12+自适应图标兼容性?** - 需提供 `res/mipmap-anydpi/ic_launcher.xml` 定义自适应图层,动态替换其中资源 3. **如何兼容第三方ROM的权限限制?** - 需在设置页引导用户授予“修改快捷方式”权限(针对小米等定制系统) 4. **是否支持多用户场景的独立图标?** - 通过 `UserManager` 获取用户ID,为不同用户启用不同`activity-alias`实现隔离 [^1]: Android App图标静态更新方案 [^2]: Android 中的动态应用程序图标(核心技术原理) [^3]: Android应用热更新兼容性处理
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值