IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    [原]Android中通过进程注入技术修改广播接收器的优先级

    jiangwei0910410003发表于 2014-12-23 10:02:45
    love 0

    前言

    这个周末又没有吊事,在家研究了如何通过进程的注入技术修改广播接收器的优先级,关于这个应用场景是很多的,而且也很重要,所以就很急的去fixed了。

    Android中的四大组件中有一个广播:Broadcast

    关于它的相关知识可以转战:http://blog.csdn.net/jiangwei0910410003/article/details/19150705

    我们这里就不做太多解释了,现在来看一下问题:


    知识前提

    这篇文章和我之前介绍一篇文章: Andrdoid中对应用程序的行为拦截实现方式之----从Java层进行拦截

    内容和知识点非常相似,如果想看明白这篇文章的话,那么必须先看懂上面的一篇文章,否则很难理解的。


    知识点

    1、进程注入技术

    2、获取系统中所有应用的IntentFilter


    问题

    一、让我们自己定义的广播接收器的最先接收到系统发送的安装应用的广播

    现在很多安全应用都会注册这个广播(360手机卫士、清理大师等),广播中可以设置优先级的。同时动态注册的广播的优先级比静态注册的广播的优先级高。

    通过上面的知识我们可以这么做:

    1、将自己的广播接收器注册的优先级设置最高,一般是用最大值:Integer.MAX_VALUE

    2、进行动态注册广播(那么我们可能需要开启一个后台服务了,不然应用退出之后,这个接收器就没有了,是接收不到广播的)

    通过上面的两步我们可以发现,我们自己的广播接收器的优先级应该比较高了。能够最先接收到广播了。


    问题来了,你的应用这么做,那么其他应用也会这么做的,而且这种做法的成本和代价不是很高。很容易实现的。

    好吧。那么我们现在该怎么办呢?

    这时候我们就需要通过Android中的进程注入技术来进行拦截操作,然后去修改其他应用的广播接收器的优先级了

    下面来看一下具体实现:

    首先关于进程的注入技术这里就不做介绍了,如果有疑问的同学可以转战:

    http://blog.csdn.net/jiangwei0910410003/article/details/40949475

    注:还有一个问题,我今天介绍的内容其实和这篇文章非常相似,功能差不多。所以强烈建议大家最好认真的把这篇文章从头到尾仔细的读一遍,同时最后动手去实现以下,那么在来看这篇文章就简单多了。不然是很费劲的。


    这里我们需要注入到系统进程:system_server

    然后在Java层自定义一个Binder对象,在通过底层去替换系统的Binder。然后我们需要做的工作就是在Java中的自定义的这个Binder对象的onTransact方法中进行操作


    不多说了,先来看代码:

    DemoInject3项目:

    EntryClass.java

    package com.demo.inject3;
    
    import android.os.Binder;
    import android.os.IBinder;
    import android.os.Parcel;
    import android.os.RemoteException;
    import android.util.Log;
    
    /**
     * 
     * @author boyliang
     * 
     */
    public final class EntryClass {
    
    	private static final class ProxyActivityManagerServcie extends Binder {
    		private static final String CLASS_NAME = "android.app.IActivityManager";
    		private static final int s_broadcastIntent_code;
    
    		private SmsReceiverResorter mResorter;
    
    		static {
    			if (ReflecterHelper.setClass(CLASS_NAME)) {
    				s_broadcastIntent_code = ReflecterHelper.getStaticIntValue("BROADCAST_INTENT_TRANSACTION", -1);
    			} else {
    				s_broadcastIntent_code = -1;
    			}
    		}
    
    		private IBinder mBinder;
    
    		public ProxyActivityManagerServcie(IBinder binder) {
    			Logger.init();
    			mBinder = binder;
    			mResorter = new SmsReceiverResorter(binder);
    		}
    
    		@Override
    		protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
    
    			if (code == s_broadcastIntent_code) {
    				Log.i("TTT", "broadcastintent:"+s_broadcastIntent_code);
    				mResorter.updatePriority("com.demo.sms");
    			}
    
    			return mBinder.transact(code, data, reply, flags);
    		}
    	}
    
    	public static Object[] invoke(int i) {
    		IBinder activity_proxy = null;
    
    		try {
    			activity_proxy = new ProxyActivityManagerServcie(ServiceManager.getService("activity"));
    
    			Log.i("TTT", ">>>>>>>>>>>>>I am in, I am a bad boy 3!!!!<<<<<<<<<<<<<<");
    		} catch (Exception e) {
    			e.printStackTrace();
    		}
    
    		return new Object[] { "activity", activity_proxy };
    	}
    }
    

    核心的方法是onTransact:

    这里需要判断一下code值,我们只对广播的code做操作。关于这个code的值,在上面的提到的那篇文章中有详细介绍。

    看到调用了mResorter.updatePriority方法。那么我们在来看一下mResorter变量的类型

    SmsReceiverResorter.java

    package com.demo.inject3;
    
    import java.lang.reflect.Field;
    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.Map.Entry;
    import java.util.Set;
    
    import android.content.Intent;
    import android.content.IntentFilter;
    import android.os.IBinder;
    import android.util.Log;
    
    final class SmsReceiverResorter {
    
    	private static final String key = Intent.ACTION_PACKAGE_ADDED;
    	
    	private final String TAG = "TTT";
    	private HashMap mActionToFilter;
    	private Field mPackageNameField;
    
    	@SuppressWarnings("unchecked")
    	public SmsReceiverResorter(IBinder am) {
    		Class claxx = am.getClass();
    		try {
    			Field field = claxx.getDeclaredField("mReceiverResolver");
    			field.setAccessible(true);
    			Object mReceiverResolver = field.get(am);
    			Log.i("TTT", "Binder:"+mReceiverResolver.getClass().getName());
    
    			claxx = mReceiverResolver.getClass();
    			
    			field = claxx.getSuperclass().getDeclaredField("mActionToFilter");
    			field.setAccessible(true);
    
    			mActionToFilter = (HashMap) field.get(mReceiverResolver);
    			Log.i("TTT", "mActionToFilter:"+mActionToFilter);
    			
    			if(mActionToFilter == null){
    				return;
    			}
    			
    			Log.i("TTT", "size:"+mActionToFilter.size());
    			
    			Log.i("TTT", "isHave:"+mActionToFilter.containsKey(key));
    			
    			IntentFilter[] filters_tmp = (IntentFilter[]) mActionToFilter.get(key);
    			Log.i("TTT", "filters:"+filters_tmp);
    			
    			if (filters_tmp != null) {
    				Log.i("TTT", "length:"+filters_tmp.length);
    				for (int i=0;i> sets = mActionToFilter.entrySet();
    			Iterator> iterators = sets.iterator();
    			while(iterators.hasNext()){
    				Log.i("TTT", "key:"+iterators.next().getKey());
    			}
    			
    			
    		} catch (Exception e) {
    			Log.e(TAG, e.toString());
    		}
    	}
    
    	public void updatePriority(String target_pkg) {
    		
    		if (mActionToFilter != null) {
    				IntentFilter[] filters_tmp = (IntentFilter[]) mActionToFilter.get(key);
    
    				Log.i("TTT", "filters:"+filters_tmp);
    				
    				if (filters_tmp != null) {
    					
    					Log.i("TTT", "add package...");
    					
    					IntentFilter[] newFilters = new IntentFilter[1];
    
    					for (int i=0;i claxx = filter.getClass();
    			try {
    				mPackageNameField = claxx.getDeclaredField("packageName");
    				mPackageNameField.setAccessible(true);
    			} catch (Exception e) {
    				Log.e(TAG, e.toString());
    			}
    		}
    		
    		String result = null;
    
    		if (filter != null) {
    			try {
    				result = (String) mPackageNameField.get(filter);
    			} catch (Exception e) {
    				Log.e(TAG, e.toString());
    			}
    		}
    		
    		return result;
    	}
    }
    

    这里面有两个重要的方法:

    1、构造方法

    public SmsReceiverResorter(IBinder am) {
    		Class claxx = am.getClass();
    		try {
    			Field field = claxx.getDeclaredField("mReceiverResolver");
    			field.setAccessible(true);
    			Object mReceiverResolver = field.get(am);
    			Log.i("TTT", "Binder:"+mReceiverResolver.getClass().getName());
    
    			claxx = mReceiverResolver.getClass();
    			
    			field = claxx.getSuperclass().getDeclaredField("mActionToFilter");
    			field.setAccessible(true);
    
    			mActionToFilter = (HashMap) field.get(mReceiverResolver);
    			Log.i("TTT", "mActionToFilter:"+mActionToFilter);
    			
    			if(mActionToFilter == null){
    				return;
    			}
    			
    			Log.i("TTT", "size:"+mActionToFilter.size());
    			
    			Log.i("TTT", "isHave:"+mActionToFilter.containsKey(key));
    			
    			IntentFilter[] filters_tmp = (IntentFilter[]) mActionToFilter.get(key);
    			Log.i("TTT", "filters:"+filters_tmp);
    			
    			if (filters_tmp != null) {
    				Log.i("TTT", "length:"+filters_tmp.length);
    				for (int i=0;i> sets = mActionToFilter.entrySet();
    			Iterator> iterators = sets.iterator();
    			while(iterators.hasNext()){
    				Log.i("TTT", "key:"+iterators.next().getKey());
    			}
    			
    			
    		} catch (Exception e) {
    			Log.e(TAG, e.toString());
    		}
    	}

    在构造方法中我们主要做的工作是获取mActionToFilter的值,它的定义:

    private HashMap mActionToFilter;
    这个值是干什么的呢?它是所有应用的IntentFilter的值,关于IntentFilter的作用可以自行百度,这里就不解释了。

    这里的key是action的名称。因为我们在动态注册广播的时候,需要传递IntentFilter的。

    所以,我们拿到这个值之后,就可以获取所有应用注册广播的信息了

    这个变量是非公开的,我们无法通过api去获取。那么不用说用反射去搞定它:

    这个变量的定义是在 Android源码目录\services\java\com\android\server\IntentResolver.java

       /**
         * All filters that have been registered.
         */
        private final HashSet mFilters = new HashSet();
    
        /**
         * All of the MIME types that have been registered, such as "image/jpeg",
         * "image/*", or "{@literal *}/*".
         */
        private final ArrayMap mTypeToFilter = new ArrayMap();
    
        /**
         * The base names of all of all fully qualified MIME types that have been
         * registered, such as "image" or "*".  Wild card MIME types such as
         * "image/*" will not be here.
         */
        private final ArrayMap mBaseTypeToFilter = new ArrayMap();
    
        /**
         * The base names of all of the MIME types with a sub-type wildcard that
         * have been registered.  For example, a filter with "image/*" will be
         * included here as "image" but one with "image/jpeg" will not be
         * included here.  This also includes the "*" for the "{@literal *}/*"
         * MIME type.
         */
        private final ArrayMap mWildTypeToFilter = new ArrayMap();
    
        /**
         * All of the URI schemes (such as http) that have been registered.
         */
        private final ArrayMap mSchemeToFilter = new ArrayMap();
    
        /**
         * All of the actions that have been registered, but only those that did
         * not specify data.
         */
        private final ArrayMap mActionToFilter = new ArrayMap();
    
        /**
         * All of the actions that have been registered and specified a MIME type.
         */
        private final ArrayMap mTypedActionToFilter = new ArrayMap();
    那么IntentResolver这个类的对象我们可以从哪里获取呢?

    在 Android源码目录\services\java\com\android\server\am\ActivityManagerService.java 中定义:

     /**
         * Resolver for broadcast intents to registered receivers.
         * Holds BroadcastFilter (subclass of IntentFilter).
         */
        final IntentResolver mReceiverResolver
                = new IntentResolver() {
            @Override
            protected boolean allowFilterResult(
                    BroadcastFilter filter, List dest) {
                IBinder target = filter.receiverList.receiver.asBinder();
                for (int i=dest.size()-1; i>=0; i--) {
                    if (dest.get(i).receiverList.receiver.asBinder() == target) {
                        return false;
                    }
                }
                return true;
            }
    
            @Override
            protected BroadcastFilter newResult(BroadcastFilter filter, int match, int userId) {
                if (userId == UserHandle.USER_ALL || filter.owningUserId == UserHandle.USER_ALL
                        || userId == filter.owningUserId) {
                    return super.newResult(filter, match, userId);
                }
                return null;
            }
    
            @Override
            protected BroadcastFilter[] newArray(int size) {
                return new BroadcastFilter[size];
            }
    
            @Override
            protected boolean isPackageForFilter(String packageName, BroadcastFilter filter) {
                return packageName.equals(filter.packageName);
            }
        };
    

    关于这个ActivityManagerService的对象,我们该怎么获取呢?我们知道它是一个系统的Service,还有系统的Service都是一个IBinder对象的。

    ServiceManager.getService("activity")
    这种方式就可以获取到ActivityManagerService对象了。

    具体代码在回去看一下EntryClass.java中初始化SmsReceiverResorter的部分内容。


    好了,找到了它,下面的工作就是用反射去获取了,工作就简单了,这里就不多说了。


    还有一个重要的方法

    2、修改优先级的方法:updatePriority

    public void updatePriority(String target_pkg) {
    		
    		if (mActionToFilter != null) {
    				IntentFilter[] filters_tmp = (IntentFilter[]) mActionToFilter.get(key);
    
    				Log.i("TTT", "filters:"+filters_tmp);
    				
    				if (filters_tmp != null) {
    					
    					Log.i("TTT", "add package...");
    					
    					IntentFilter[] newFilters = new IntentFilter[1];
    
    					for (int i=0;i
    在这个方法中,传递进来一个包名(com.demo.sms),这个包名其实就是我们想将其优先级设置最高的一个应用。

    我们首先通过key去获取对应的IntentFilter[]对象

    IntentFilter[] filters_tmp = (IntentFilter[]) mActionToFilter.get(key);
    这里的key是:

    private static final String key = Intent.ACTION_PACKAGE_ADDED;
    就是安装应用的对应的系统action名

    那么我们就获取到了系统中所有注册了安装应用的广播的应用了。

    这里我们再次使用反射从IntentFilter对象中拿到包名,这样我们就可以进行过滤了。

    for (int i=0;i
    然后就开始遍历IntentFilter[]对象,首先将其说有的应用的广播接收器的优先级设置最低,直接调用IntentFilter的setPriority方法

    然后发现如果当前的包名和我们需要设置优先级的包名一样,就将其优先级设置最高。


    好了,上面的代码看完了。


    下面来看一下我们自定义的两个广播来进行测试:

    第一个测试项目:com.demo.sms

    我们看了上面的代码之后,会发现,这个项目就是我们需要修改最高优先级的应用

    1、广播接收器:

    package com.demo.sms;
    
    import android.content.BroadcastReceiver;
    import android.content.Context;
    import android.content.Intent;
    import android.util.Log;
    
    public final class SmsReceiver extends BroadcastReceiver {
    
    	@Override
    	public void onReceive(final Context context, Intent intent) {
    		Log.i("TTT", "com.demo.sms:"+intent.getAction());
    	}
    }
    
    打印一下action的值


    2、后台用于动态注册广播的service

    package com.demo.sms;
    
    import android.app.Service;
    import android.content.Intent;
    import android.content.IntentFilter;
    import android.os.IBinder;
    
    public final class MyService extends Service {
    
    	@Override
    	public void onCreate() {
    		super.onCreate();
    		IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
    		filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
    		filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
    		filter.addDataScheme("package");
    		filter.setPriority(Integer.MIN_VALUE);
    		registerReceiver(new SmsReceiver(), filter);
    		filter = null;
    	}
    	
    	@Override
    	public IBinder onBind(Intent paramIntent) {
    		return null;
    	}
    
    }
    
    然后就开启这个service就可以了。这里我们将它的优先级设置最低


    第二个测试项目:com.demo.smstrojan

    这个项目的代码和上面的一个测试项目代码一模一样,就是对应的包名不一样以及注册广播的优先级不一样。这里就不做解释了。

    package com.demo.smstrojan;
    
    import android.app.Service;
    import android.content.Intent;
    import android.content.IntentFilter;
    import android.os.IBinder;
    
    public final class MyService extends Service {
    
    	@Override
    	public void onCreate() {
    		super.onCreate();
    		
    		IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
    		filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
    		filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
    		filter.addDataScheme("package");
    		filter.setPriority(Integer.MAX_VALUE);
    		registerReceiver(new SmsReceiver(), filter);
    	}
    	
    	@Override
    	public IBinder onBind(Intent paramIntent) {
    		return null;
    	}
    
    }
    
    这里我们将它的优先级设置最高

    好了,我们现在首先运行这两个测试项目。后台的Service也起来了

    在来运行一下上面的DemoInject3,

    这个应用是不会运行有界面的,他只是一个插件apk.

    我们运行之后,获取DemoInject3.apk文件。


    下面就开始进行注入了。

    这里我不做演示了,操作步骤和:http://blog.csdn.net/jiangwei0910410003/article/details/40949475 这篇文章一模一样。


    下面来看一下运行结果:

    这里其实还需要两个文件:libproxybinder.so和poison

    这两个文件从上面提到的那篇文章中可以找到的


    开启一个终端来查看一下系统进程的pid:

    =>adb shell

    =>ps |grep system_server



    在开启一个终端进行注入:

    =>adb shell

    =>su

    => cd /data/data/

    =>./poison /data/data/libproxybinder.so 579



    在开启一个终端来打印log值,因为这个log值有点多:所以我们将log输出到一个文件中:

    =>adb logcat -s TTT >log.txt

    这里需要等待一会,然后我们退出这个命令(Ctrl+C),查看log.txt文件

    =>start log.txt


    这里的log.txt文件有点大,这里直截取一部分内容:

    我们看到了打印的key值了。但是这里发现一个问题

    isHave:false

    这个值是在SmsReceiverResorter.java的构造方法中打印的:

    Log.i("TTT", "isHave:"+mActionToFilter.containsKey(key));
    这里的key是:android.intent.action.PACKAGE_ADDED,安装应用的系统action名

    那么问题就来了,我们在两个测试项目中都进行注册了这个action,为什么这里获取到的mActionToFilter中的key没有这个action呢?

    这个纠结了我一天,最后才发现问题所在的。

    我们在测试项目中注册广播是:

    IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
    filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
    filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
    filter.addDataScheme("package");
    filter.setPriority(Integer.MIN_VALUE);
    registerReceiver(new SmsReceiver(), filter);
    发现有一个地方:就是IntentFilter中不仅添加了action值,还有一个scheme的值,好吧,问题还得去看源代码:

    还是在IntentResolver.java中

    我们看到还有一个变量:mSchemeToFilter,用来存储scheme的IntentFilter的值。

    那么继续来看一下IntentResolver类中的addFilter方法



    发现到问题了:

    这里有两个判断:

    首先将IntentFilter添加到mSchemeToFilter变量中

    然后判断IntentFilter中有没有scheme和mime,没有的话就将IntentFilter添加到mActionToFilter变量中

    好吧,到这里我们就找到问题了,因为我们现在有scheme字段的,值是:package

    那么我们注册的IntentFilter就不会再mActionToFilter中,而是在mSchemeToFilter中。


    问题找到了,下面我就开始修改DemoInject项目中的SmsReceiverResorter.java代码:

    1)首先修改一下反射的字段

    将下面代码:

    field = claxx.getSuperclass().getDeclaredField("mActionToFilter");
    改成:

    field = claxx.getSuperclass().getDeclaredField("mSchemeToFilter");


    2)在修改一下key值

    将下面代码:

    private static final String key = Intent.ACTION_PACKAGE_ADDED;
    改成:

    private static final String key = "package";
    因为我们注册的时候都是用的是这个值:package


    我们再次运行DemoInject项目,得到DemoInject3.apk

    然后再次进行操作(和上面的操作一样):

    不过这里有一个问题,就是system_server进程只能注入一次,所以如果在次进行注入操作的话,需要重新启动设备了

    等到设备重启之后,还要记得分别运行上面两个测试项目,然后我们随便安装一个应用,用于测试广播接收的

    这时候我们在来看一下log.txt


    这时候看到了isHave是true了



    这时候,我们发现,com.demo.sms项目的优先级最高,其他的应用的优先级都是最低的。我们还发现

    com.demo.sms比com.demo.smstrojan先获取广播

    我们看到上面的两个测试项目的代码:

    com.demo.sms项目中的广播优先级设置最低

    com.demo.smstrojan项目的广播优先级设置最高

    所以正常情况下,应该是com.demo.smstrojan项目最先接收到广播的。

    但是现在我们修改了优先级之后,com.demo.sms项目最先接收到广播

    到这里我们的问题解决了,可以将我们自己特定的应用的广播接收的优先级设置最高,让其最先接收到广播。


    二、现在想特定的应用最先接收到广播之后,就不让其他应用在接收到这个广播了

    但是问题还没有结束,现在还有这种需求,如果我们现在想让一个应用最先接收到广播,之后就不让后续的应用接收到这个广播了,有人说可以在这个特定的应用中终止这个广播,那个只是对于有序广播来说的,现在如果是无序广播呢?我们该怎么操作呢?

    其实看懂了SmsReceiverResorter.java中的updatePriority方法,我们这里就容易修改了。

    原理:我们可以将其他应用的IntentFilter删除,只保留需要修改优先级的应用的IntentFilter即可

    实现:那么可以从新定义一个IntentFilter[]对象,然后将我们需要设置最先接收到广播的那个应用的IntentFilter添加进去,然后在put进map中

    具体修改如下:

    updatePriority方法:

    public void updatePriority(String target_pkg) {
    	
    	if (mActionToFilter != null) {
    			IntentFilter[] filters_tmp = (IntentFilter[]) mActionToFilter.get(key);
    
    			Log.i("TTT", "filters:"+filters_tmp);
    			
    			if (filters_tmp != null) {
    				
    				Log.i("TTT", "add package...");
    				
    				IntentFilter[] newFilters = new IntentFilter[1];
    
    				for (int i=0;i

    核心代码:

    mActionToFilter.put(key, newFilters);
    将我们新的IntentFilter[]对象去覆盖mActionToFilter中的指定key的对象。


    其他地方不需要修改,再次运行

    查看log.txt


    这里看到了,我们上面代码打印新的IntentFilter[]

    现在只有一个

    new_pkg:com.demo.sms

    而且,我们在安装一个apk包,发现只有com.demo.sms能够接收到广播了

    而com.demo.smstrojan已经接收不到广播了

    同时我们会发现以前很牛逼的360卫士现在也接收不到广播了,哈哈~~

    我们没有操作前360卫士可以接收到这个安装广播:



    我们操作之后,哈哈~~,没有拦截到,因为这个广播被我们手动的给干掉了

    通知栏中没有通知了。


    好吧,到这里我们算是彻底的做到了广播接收的独裁专政了,现在是可以设置特定的应用能接收到广播,而且不管这个广播是有序的还是无序的,我们都可以将其静止,不让其他应用接收到。

    说实话,这个问题解决了,真的好开心好开心,特别是发现360现在也接收不到这个广播了。心里特别爽。哈哈~~


    项目下载

    DemoInject项目下载

    DemoSms项目(com.demo.sms)下载

    DemoSms1项目(com.demo.smstrojan)下载


    总结

    关于这篇文章的研究意义在于我们可以控制特定的应用的广播接收器的优先级了,现在市面上有很多应用都会接收系统发送的一些广播,那么每个应用可能都会将自己的广播接收器的优先级设置最高,让自家的接收器最先接收到广播。那么主动权就可控了,但是那些常规的方法貌似现在每个应用都在用,所以如果想做到最牛逼的,那么只能用非常规的方法了,那么这些非常规的方法也是需要一定代价的:设备必须root了。同时这么做从理论上来说就有点流氓了。

    而且现在有一些恶意的apk,它需要接收到系统发送的短信广播,那么这时候他肯定希望自己最先收到这个短信了,然后把这个广播给终止了。这时候就可以这么做了。


    PS:

    有的同学可能会有疑问,说我们这么做,那么其他家的应用应该也想到这么做了,是的,所以这里就需要强调一点:

    这种做法是很流氓的,不合常规的,而且设备必须root,所以我们以后在做类似的功能的时候,最好还是按照常规的方式去实

    现。不要用非常规的方法,那些方法只能骗骗小白用户,长期这样对于应用本身发展也是不利的。以上只是我个人观点。




























    作者:jiangwei0910410003 发表于2014/12/23 10:02:45 原文链接
    阅读:864 评论:0 查看评论


沪ICP备19023445号-2号
友情链接