欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android开发之做一键批量卸载App功能的详细讲解

程序员文章站 2022-06-02 22:22:14
首先准备一部已经root的手机,然后打开android studio,下面我们开始快乐的写代码吧~ 首先我们先分析具体的业务需求: 很简单的一个需求,最主要的功能就是可以卸载app; 同时要求可以批...

首先准备一部已经root的手机,然后打开android studio,下面我们开始快乐的写代码吧~

首先我们先分析具体的业务需求:

很简单的一个需求,最主要的功能就是可以卸载app;

同时要求可以批量卸载;

既然能够批量卸载,也就是说我们在ui交互上可以批量选择;

能大量展示待卸载的app。

好的我们现在一步一步的来:首先我们先解决最主要的需求,卸载app!

有两种方式可以实现app卸载:分为静默方式和非静默方式。

什么是静默方式?意思就是说卸载完全是在后台进行的,不需要用户去点击确认卸载。非静默方式的意思显而易见,卸载的时候需要用户点击确认,只有用户确认卸载才会卸载。

我们先说非静默方式卸载:

非静默方式卸载的代码如下;


 public void unstallapp(string pagename){

  intent uninstallintent = new intent();
  uninstallintent.setaction(intent.action_delete);
  uninstallintent.setdata(uri.parse("package:"+pagename));
  
  startactivityforresult(uninstall_intent,1);

 }

从代码中我们就可以看出来,这里开启了一个活动,也就是所谓的应用卸载程序,然后把需要卸载的app包名交给它,它就会把这个app给卸载掉。这是正常的app卸载步骤。开启这个应用卸载程序活动后,页面就会跳转到卸载页面,然后等待用户点击确定或者取消,点击确定就会执行卸载程序,点击取消就会回退到原来的活动。在这里我们使用了startactivityforresult()方法来开启应用卸载活动,目的是为了卸载完成后在回掉函数里面可以更新原来的app列表页面。

非静默方式代码非常的简单,也非常容易理解,但是这里有个不足之处,那就是如果我们一次性需要卸载十个app应用,那么页面将会跳转十次,同时你也需要点击十次确定!别忘了我们这里可是要求批量卸载,如果让用户去连续点击十次确定,这样会非常影响用户体验!所以非静默方式卸载在这里使用并不是很好,静默方式是更好的选择!

静默方式卸载:

静默方式也就是意味着我们需要绕过安卓的界面,在后台执行卸载命令,那么怎么做呢?很显然,当然是使用命令了!使用命令的方式我们可以绕过安卓界面执行。

这里有两种卸载app命令:

首先是adb命令:adbuninstall 包名>

还有一个pm命令:pm uninstall包名>

我们可以看到这两种命令写法相同,命令的开头不同,那么他们具体的差别在什么地方呢?应该用哪一种命令方式?还是两种命令方式都合适呢?

我先不说区别,我们去实地的测试一下,首先我们先用adb命令去卸载。

代码如下:


package com.example.uninstallapk;

import android.util.log;

import java.io.dataoutputstream;

/**
 * created by 王将 on 2018/7/23.
 */


//adb命令翻译执行类
public class rootcmd {
 /***
  * @param command
  * @return
  */
 public static boolean exusecmd(string command) {
  process process = null;
  dataoutputstream os = null;
  try {
process = runtime.getruntime().exec("su");
os = new dataoutputstream(process.getoutputstream());
os.writebytes(command + "\n");
os.writebytes("exit\n");
os.flush();
log.e("updatefile", "======000==writesuccess======");
process.waitfor();
  } catch (exception e) {
log.e("updatefile", "======111=writeerror======" + e.tostring());
return false;
  } finally {
try {
 if (os != null) {
  os.close();
 }
 if (process != null) {
  process.destroy();
 }
} catch (exception e) {
 e.printstacktrace();
}
  }
  return true;
 }

 public static void uninstallapk(string pagename){

  exusecmd("adb uninstall "+pagename);
 }

}

主活动中我们调用:


rootcmd.uninstallapk("com.example.tset");

把想要卸载的app包名传进去,运行一下,很快你就发现:整个应用崩溃了,出现了anr问题,应用无反应。

好,我们改为pm命令试一下,结果发现成功了!

那么现在我们分析一下为什么adb命令会导致出现anr问题,而pm命令就不会出现错误。

一个命令的下达,肯定会调用相应的方法去处理,只不过这个调用过程在系统的内部,我们外界是看不到的,只能得到命令执行的结果。就好比我们使用命令去卸载app应用,同样也是在内部调用了卸载方法,那么具体这个方法是什么?在哪里呢?下面我们就去深入的探讨一下。

android系统卸载app应用都是调用了一个类 中方法,不管是非静默模式还是静默模式,这个类就是packageinstaller类。当然android系统安装app也同样是调用的它里面的方法,这个类功能从它的名字上就可以看出来:打包安装程序。

当然这个类我们在平常的开发中是用不到的,同样也是无法调用的,这个类同样也是一个底层调用的类。在这个类中我们可以找到具体的卸载app方法,让我们看一下:


/**
  * uninstall the given package, removing it completely from the device. this
  * method is only available to the current "installer of record" for the
  * package.
  *
  * @param packagename the package to uninstall.
  * @param statusreceiver where to deliver the result.
  */
 public void uninstall(@nonnull string packagename, @nonnull intentsender statusreceiver) {
  uninstall(packagename, 0 /*flags*/, statusreceiver);
 }

 /**
  * uninstall the given package, removing it completely from the device. this
  * method is only available to the current "installer of record" for the
  * package.
  *
  * @param packagename the package to uninstall.
  * @param flags flags for uninstall.
  * @param statusreceiver where to deliver the result.
  *
  * @hide
  */
 public void uninstall(@nonnull string packagename, @deleteflags int flags,
@nonnull intentsender statusreceiver) {
  uninstall(new versionedpackage(packagename, packagemanager.version_code_highest),
 flags, statusreceiver);
 }

 /**
  * uninstall the given package with a specific version code, removing it
  * completely from the device. this method is only available to the current
  * "installer of record" for the package. if the version code of the package
  * does not match the one passed in the versioned package argument this
  * method is a no-op. use {@link packagemanager#version_code_highest} to
  * uninstall the latest version of the package.
  *
  * @param versionedpackage the versioned package to uninstall.
  * @param statusreceiver where to deliver the result.
  */
 public void uninstall(@nonnull versionedpackage versionedpackage,
@nonnull intentsender statusreceiver) {
  uninstall(versionedpackage, 0 /*flags*/, statusreceiver);
 }

 /**
  * uninstall the given package with a specific version code, removing it
  * completely from the device. this method is only available to the current
  * "installer of record" for the package. if the version code of the package
  * does not match the one passed in the versioned package argument this
  * method is a no-op. use {@link packagemanager#version_code_highest} to
  * uninstall the latest version of the package.
  *
  * @param versionedpackage the versioned package to uninstall.
  * @param flags flags for uninstall.
  * @param statusreceiver where to deliver the result.
  *
  * @hide
  */
 @requirespermission(anyof = {
manifest.permission.delete_packages,
manifest.permission.request_delete_packages})
 public void uninstall(@nonnull versionedpackage versionedpackage, @deleteflags int flags,
@nonnull intentsender statusreceiver) {
  preconditions.checknotnull(versionedpackage, "versionedpackage cannot be null");
  try {
minstaller.uninstall(versionedpackage, minstallerpackagename,
  flags, statusreceiver, muserid);
  } catch (remoteexception e) {
throw e.rethrowfromsystemserver();
  }
 }

这个是packageinstaller类中的四个uninstall()方法,具体的功能就是卸载app应用。

当然这四个方法用于卸载不同状态的应用,具体的使用请看官方给出的描述文档,这里不再具体的做出分析。

现在我们知道了卸载app调用的是packageinstaller类的uninstall()方法,那么这个和命令的方式有什么关系呢?我们看一下packageinstaller类的所处路径你就明白了,packageinstaller类的所处路径为/android/content/pm/packageinstaller.java,具体在博主这里的完整路径为:

Android开发之做一键批量卸载App功能的详细讲解

很明显,在/pm路径下。pm全称package manager,意思包的管理者,pm命令说白了就是包管理命令,进一步说,只有使用pm命令才会调用/pm路径下的底层方法,也就是说才会执行包文件的操作。这下你明白为什么使用adb会导致anr问题了吧,因为程序找不到执行方法啊!

好了,现在我们解决了最重要的需求,静默卸载app,那么接下来的需求就很简单实现了,批量卸载,批量选择,这里直接使用一个循环不停的执行卸载命令就好了。按照这个思路我们开始写代码。

首先是界面ui部分:





 
  

  
 

使用scrollview嵌套一个linearlayout布局来实现app列表,其中单个的app信息使用动态加载的形式添加。

下面是一个app信息子布局:





 
 

很简单,两个控件组成,一个chexbox控件提供勾选,一个textview用来展示app的标签。

接下来我们就需要写主活动中的逻辑性操作了:

首先贴上我们的mainactivity代码:


public class mainactivity extends appcompatactivity {

 linearlayout linearlayout;

 list pages=new arraylist<>();
 list views=new arraylist<>();

 progressdialog progressdialog;

 list packageinfos=new arraylist<>();

 @override
 protected void oncreate(bundle savedinstancestate) {
  super.oncreate(savedinstancestate);
  setcontentview(r.layout.activity_main);

  linearlayout = (linearlayout) findviewbyid(r.id.linear1);

  button button=(button) findviewbyid(r.id.start_delete);


  packagemanager packagemanager=getpackagemanager();
  packageinfos=packagemanager.getinstalledpackages(packagemanager.get_uninstalled_packages);

  int id=0;
  for (packageinfo packageinfo:packageinfos){
string str=packageinfo.applicationinfo.loadlabel(getpackagemanager()).tostring();
linearlayout.addview(getchoiceview(linearlayout,str,id));
id++;
  }

  button.setonclicklistener(new view.onclicklistener() {
@override
public void onclick(view v) {

 new deleteapk().execute();
}
  });
 }

 private view getchoiceview(linearlayout root, final string pagename, int id){
  final view view = layoutinflater.from(this).inflate(r.layout.choice_layout, root, false);

  final checkbox checkbox=(checkbox) view.findviewbyid(r.id.page_id);
  final textview textview=(textview) view.findviewbyid(r.id.page_name);

  view.settag(id);

  checkbox.settag(view);

  checkbox.setoncheckedchangelistener(new compoundbutton.oncheckedchangelistener() {
@override
public void oncheckedchanged(compoundbutton buttonview, boolean ischecked) {

 if (ischecked){

  views.add((view) checkbox.gettag());
  pages.add((int) view.gettag());

 }else {

  view view1=(view) checkbox.gettag();
  views.remove(view1);
  pages.remove(getindexpages((int) view1.gettag()));

 }

}
  });

  textview.settext(pagename);

  return view;
 }

 public int getindexpages(int id){
  int index=0;
  int j=0;

  for (int i:pages){
if (i==id){
 index=j;
 break;
}
j++;
  }

  return index;
 }

 class deleteapk extends asynctask {

  @override
  protected void onpreexecute() {
progressdialog=new progressdialog(mainactivity.this);
progressdialog.settitle("正在卸载中");
progressdialog.setmessage("请稍后...");
progressdialog.setcancelable(true);
progressdialog.show();
  }

  @override
  protected object doinbackground(object[] objects) {
for (int id:pages){
 rootcmd.uninstallapk(packageinfos.get(id).packagename);
}
return true;
  }

  @override
  protected void onpostexecute(object o) {
progressdialog.dismiss();

pages.removeall(pages);
for (view view:views){
 linearlayout.removeview(view);
}
views.removeall(views);
  }
 }
}

在代码中有两部分讲解一下:

首先是getchoiceview()方法。在这个方法中我们主要获取用户勾选的app是哪些。当用户点击勾选的时候,我们就把对应app的下标值给存下来,同时存下来的还有相应的子view,存放子view的目的是为了在卸载完成之后更新我们的app列表。在用户点击取消勾选,我们还需要把之前存放的相关信息给移除掉,确保卸载的都是用户最终确定删除的。

存好了相应的信息,下面就是执行pm命令部分。在这里我使用线程来开启pm命令,可以很清楚地看到,在这里我使用了asynctask 框架。在线程开启前,也就是pm命令开始之前,我们弹出一个progressdialog,目的就是告诉用户正在卸载请稍等,因为pm命令执行起来到结束会需要一定的时间;然后就开始执行pm命令,使用循环挨着挨卸载list中用户选定的app,执行结束后关闭progressdialog,然后清空我们的liset,同时还要更改我们的ui界面。

这里选用asynctask 框架有一个好处,那就是可以明确的知道命令执行结束的时间,在命令结束之后更改ui。如果不使用asynctask 框架,那么就比较难以掌握pm命令执行结束的时候,毕竟这个也没有什么相关的回掉函数,在结束后ui处理上难以下手。使用asynctask 框架后,就不需要担心这个问题,执行结束后自然会执行收尾工作,这样更新iui就方便多了。

好了,本篇文章到此结束。有需要引用的请标明出处,谢谢!