Android开发:详细讲解该如何做一键批量卸载App功能

本篇博文所用到的具体项目源码我已经上传至GitHub,项目地址:一键大批量卸载手机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命令:adb uninstall <App包名>

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

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

我先不说区别,我们去实地的测试一下,首先我们先用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,具体在博主这里的完整路径为:

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

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

首先是界面UI部分:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_weight="10">
        <LinearLayout
            android:id="@+id/linear1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

        </LinearLayout>
    </ScrollView>

    <Button
        android:id="@+id/start_delete"
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_weight="1"
        android:text="一键卸载"/>
</LinearLayout>

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

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

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <CheckBox
        android:id="@+id/page_id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <TextView
        android:id="@+id/page_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="应用包名"
        android:textColor="#000000"/>
</LinearLayout>

很简单,两个控件组成,一个ChexBox控件提供勾选,一个TextView用来展示App的标签。

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

首先贴上我们的MainActivity代码:

public class MainActivity extends AppCompatActivity {

    LinearLayout linearLayout;

    List<Integer> pages=new ArrayList<>();
    List<View> views=new ArrayList<>();

    ProgressDialog progressDialog;

    List<PackageInfo> 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就方便多了。

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

猜你喜欢

转载自blog.csdn.net/qq_34149335/article/details/81216618