手机卫士-11
手機衛士-11
課1
看門狗WatchDogService程序優化
程序鎖不斷打開關閉打開關閉,有時還是有界面沒及時切換過來 有一瞬間還看見程序的界面,隱私還是保護得不夠好 原因是看門狗里WatchDogService.java里死循環,整個死循環的周期有一定的事件,所以會產生多次打開程序鎖而界面沒切換過來 那是因為應用程序還不夠優化
//該標志符用來控制是否不斷刷新 flag = true; new Thread() {//其實該service所做的事件就是創建一個死循環,不斷查看tempStopPacknames集合里的數據和新打開的app,從而進行比較操作public void run() {//如果為true就進入死循環while (flag) {// 獲取用戶的正在運行的應用程序任務棧的列表,最近使用的在集合的最前面List<RunningTaskInfo> taskinfos = am.getRunningTasks(100);String packname = taskinfos.get(0).topActivity.getPackageName();System.out.println(packname);//對比獲得的最新加載的app的名字是否存在于加鎖app數據庫中:當在加鎖數據庫中找到該packname,那么再比較該packname的app有沒有通過密碼校驗if (dao.find(packname)) {//有通過密碼校驗// 檢查主人是否發過臨時停止保護的指令if (tempStopPacknames.contains(packname)) {// 什么事情都不做,即不再彈出程序鎖界面//沒有通過密碼校驗} else {// 這個應用程序需要被鎖定// 跳出來看門狗,讓用戶輸入密碼。Intent intent = new Intent(WatchDogService.this,WatchDogEnterPwdActivity.class);//給該WatchDogEnterPwdActivity.class設置一個開啟任務的模式intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//用意圖開啟界面,并加上應用的信息intent.putExtra("packname", packname);startActivity(intent);}}try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}; }.start();我們給WatchDogService.java里死循環里加個計算器來計算循環一個的時間。
這是在效率比較高的模擬器結果 換成一個arm的模擬器來部署一下應用
看看其效率如何
- 如何優化?
處理細節: 在查詢數據庫的過程中不斷打開數據庫然后close,效率低,我們改造下ApplockDao.java,
ApplockDao.java
/*** 查詢全部的要鎖定的應用程序* @return 全部鎖定應用程序包名的集合*/ public List<String> findAll(){List<String> packnames = new ArrayList<String>();SQLiteDatabase db = helper.getWritableDatabase();Cursor cursor = db.query("lockinfo", null, null, null, null, null, null);while(cursor.moveToNext()){String packname = cursor.getString(cursor.getColumnIndex("packname"));packnames.add(packname);}cursor.close();db.close();//返回一個叫packnames的集合return packnames; }把所有的數據一次取出來,放在內存中,然后讓看門狗去查看內存,這樣效率大大改善。 經過以上的改善,
得到如下的改善?
但是這樣的死循環非常耗電,占據的cpu資源大多,怎么解決?
搜搜地圖(相當費電,后來版本來個重大改變后,在鎖屏后gps的定位會占時關閉掉,這樣的改變可可以省不少的電量)--->騰訊地圖
需求:那么我們可以考慮,我們能否鎖屏后我們的看門狗是否可以停掉,打開鎖后看門狗會打開。
把看門狗中死循環的代碼抽取到子線程中去跑。 把線程封裝成一個方法
startDogThread();
然后讓死循環加入一個成員變量標記flag,在自定義廣播接受者出接收開關屏幕的廣播事件處理
else if (Intent.ACTION_SCREEN_OFF.equals(action)) {tempStopPacknames.clear();// 臨時停止看門狗flag = false; } else if (Intent.ACTION_SCREEN_ON.equals(action)) {// 開啟看門狗if (!flag) {startDogThread();} }receiver = new InnerReceiver(); IntentFilter filter = new IntentFilter(); filter.addAction("com.itheima.doggoaway"); filter.addAction(Intent.ACTION_SCREEN_OFF); filter.addAction(Intent.ACTION_SCREEN_ON); registerReceiver(receiver, filter);這樣又大大減低了電量的消耗
優化程序后的小bug,上鎖效果消失了?
在提高看門狗效率的同時還有bug:我們改造了ApplockDao的findAll時,這時看門狗只看集合內存里保存的應用集合,這樣如果當操作一個應用上鎖之后,再去鎖上別的應用時這個后被鎖上的app的上鎖效果就失效了,因為之前看門狗里的死循環是檢查private List tempStopPacknames;集合,現在是看新定義的private List lockPacknames;集合,以實現緩存的效果,當在點擊listview里的加鎖和解鎖的按鈕時,它們操作的是數據庫dao的add和delete的方法,而方法是繼續調用數據庫的操作,看門狗根本不知道數據庫也發生了變化,那么如果我們怎么解決?
改造ApplockDao的add和delete,發送自定義廣播,當我們進行上鎖的動畫操作時,會對數據庫進行操作,那么我們讓ApplockDao的add和delete加上自定義廣播,讓看門狗接收這自定義的廣播,這就可以讓看門狗知道需要對數據庫進行增刪的操作了。在看門狗里這樣接收廣播
ApplockDao.java
/*** 添加一條鎖定的應用程序* @param packname 包名*/ public void add(String packname){SQLiteDatabase db = helper.getWritableDatabase();ContentValues values = new ContentValues();values.put("packname", packname);db.insert("lockinfo", null, values);db.close();//通知看門狗更新鎖定的應用程序集合Intent intent = new Intent();intent.setAction("com.itheima.mobilesafe.notifydogadd");intent.putExtra("packname", packname);context.sendBroadcast(intent); }/*** 刪除一條鎖定的應用程序* @param packname 包名*/ public void delete(String packname){SQLiteDatabase db = helper.getWritableDatabase();db.delete("lockinfo", "packname=?", new String[]{packname});db.close();//通知看門狗更新鎖定的包名集合Intent intent = new Intent();intent.setAction("com.itheima.mobilesafe.notifydogdelete");intent.putExtra("packname", packname);context.sendBroadcast(intent); }WatchDogService.java
else if("com.itheima.mobilesafe.notifydogadd".equals(action)){lockPacknames.add(intent.getStringExtra("packname")); }else if("com.itheima.mobilesafe.notifydogdelete".equals(action)){lockPacknames.remove(intent.getStringExtra("packname")); }receiver = new InnerReceiver(); IntentFilter filter = new IntentFilter(); filter.addAction("com.itheima.mobilesafe.notifydogdelete"); filter.addAction("com.itheima.mobilesafe.notifydogadd"); registerReceiver(receiver, filter);總結:廣播很靈活,這里使用了自定義廣播讓不同應用或組件之間發送消息接收消息實現通信!!! 改善后的看門狗WatchDogService和 ApplockDao的完整邏輯
WatchDogService.java
/*** 監視當前用戶操作的應用程序,如果這個應用程序需要保護,看門狗就蹦出來,讓用戶輸入密碼* //其實該service所做的事件就是創建一個死循環,不斷查看tempStopPacknames集合里的數據和新打開的app,從而進行比較操作* @author Administrator* */ public class WatchDogService extends Service {private Intent intent;private String packname;//應用管理器private ActivityManager am;private boolean flag;// 程序鎖的數據庫daoprivate ApplockDao dao;//內部廣播類private InnerReceiver receiver;/*** 臨時停止保護的包名集合*/private List<String> tempStopPacknames;private List<String> lockPacknames;@Overridepublic IBinder onBind(Intent intent) {return null;}//內部廣播類:該廣播類主要是操作tempStopPacknames集合,即操作從程序鎖界面傳來的應用的名字和操作鎖屏的狀態中//集合的清空,把集合提供給service去控制應用的加鎖效果//專門獲取程序界面中傳來的廣播信號,通知WatchDogService去解除程序鎖界面private class InnerReceiver extends BroadcastReceiver{@Overridepublic void onReceive(Context context, Intent intent) {//廣播一直接收所有的頻道信息String action = intent.getAction();//獲取WatchDogEnterPwdActivity傳來的StringExtraif("com.itheima.doggoaway".equals(action)){//StringExtra的keyString packname = intent.getStringExtra("packname");//看門狗把主人要求臨時停止保護的包名給記錄下來,即把WatchDogEnterPwdActicity傳來的//StringExtra:packname,把其app加入臨時停止保護的集合中tempStopPacknames.add(packname);}else if(Intent.ACTION_SCREEN_OFF.equals(action)){//當接收的廣播頻道是鎖屏,就把臨時停止保護的應用集合清零,即讓下次打開要保護的應用再次輸入密碼tempStopPacknames.clear();//停止看門狗flag = false;}else if(Intent.ACTION_SCREEN_ON.equals(action)){//當接收的廣播頻道是打開屏幕,if(!flag){startDogThread();}}else if("com.itheima.phonesafeguard.notifydogadd".endsWith(action)){lockPacknames.add(intent.getStringExtra("packname"));}else if("com.itheima.phonesafeguard.notifydogdelete".equals(action)){lockPacknames.remove(intent.getStringExtra("packname"));}}}@Overridepublic void onCreate() {//初始化臨時取消保護的app的集合tempStopPacknames = new ArrayList<String>();//獲取操作加鎖app的數據庫的接口dao = new ApplockDao(this);//一旦服務開啟集合的內容就不會再發生變化lockPacknames = dao.findAll();intent = new Intent(WatchDogService.this,WatchDogEnterPwdActivity.class);//給該WatchDogEnterPwdActivity.class設置一個開啟任務的模式intent.setFlags(intent.FLAG_ACTIVITY_NEW_TASK);receiver = new InnerReceiver();IntentFilter filter = new IntentFilter();filter.addAction("com.itheima.doggoaway");filter.addAction(Intent.ACTION_SCREEN_OFF);filter.addAction(Intent.ACTION_SCREEN_ON);filter.addAction("com.itheima.phonesafeguard.notifydogadd");filter.addAction("com.itheima.phonesafeguard.notifydogdelete");registerReceiver(receiver, filter);am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);startDogThread();super.onCreate();}private void startDogThread(){flag = true;new Thread(){//其實該service所做的事件就是創建一個死循環,不斷查看//tempStopPacknames集合里的數據和新打開的app,從而進行比較操作public void run() {//如果為true就進入死循環while(flag){//獲取用戶的正在運行的應用程序任務棧的列表,最近使用的在集合的最前面List<RunningTaskInfo> taskinfos = am.getRunningTasks(1);packname = taskinfos.get(0).topActivity.getPackageName();System.out.println(packname);//對比獲得的最新加載的app的名字是否存在于加鎖app數據庫中;//當在加鎖數據庫中找到該packname,那么再比較該packname的app有沒有通過密碼校驗 // if(dao.find(packname)){if(lockPacknames.contains(packname)){// 查詢內存//有通過密碼校驗//檢查主人是否發過臨時停止保護的指令if(tempStopPacknames.contains(packname)){//什么事情都不做,即不在彈出程序鎖界面//沒有通過密碼校驗}else{//用意圖開啟界面,并加上應用的消息intent.putExtra("packname", packname);startActivity(intent);}}try {Thread.sleep(30);} catch (InterruptedException e) {e.printStackTrace();}}};}.start();}@Overridepublic void onDestroy() {flag = false;//動態注銷廣播unregisterReceiver(receiver);receiver = null;super.onDestroy();}}ApplockDao.java
/*** 程序鎖增刪改查的業務類**/ public class ApplockDao {private ApplockDBHelper helper ;private Context context;public ApplockDao(Context context){helper = new ApplockDBHelper(context);this.context = context;}/*** 添加一條鎖定的應用程序* @param packname 包名*/public void add(String packname){SQLiteDatabase db = helper.getWritableDatabase();ContentValues values = new ContentValues();values.put("packname", packname);db.insert("lockinfo", null, values);db.close();//通知看門狗更新鎖定的應用程序集合Intent intent = new Intent();intent.setAction("com.itheima.phonesafeguard.notifydogadd");intent.putExtra("packname", packname);context.sendBroadcast(intent);}/*** 刪除一條鎖定的應用程序* @param packname 包名*/public void delete(String packname){SQLiteDatabase db = helper.getWritableDatabase();db.delete("lockinfo", "packname=?", new String[]{packname});db.close();//通知看門狗更新鎖定的包名集合Intent intent = new Intent();intent.setAction("com.itheima.phonesafeguard.notifydogdelete");intent.putExtra("packname", packname);context.sendBroadcast(intent);}/*** 查詢一條鎖定的應用程序* @param packname 包名* @return true 被鎖定false 沒有鎖定*/public boolean find(String packname){boolean result = false;SQLiteDatabase db = helper.getWritableDatabase();Cursor cursor = db.query("lockinfo", null, "packname=?", new String[]{packname}, null, null, null);if(cursor.moveToNext()){result = true;}cursor.close();db.close();return result;}/*** 查詢全部的要鎖定的應用程序* @return 全部鎖定應用程序包名的集合*/public List<String> findAll() {List<String> packnames = new ArrayList<String>();SQLiteDatabase db = helper.getWritableDatabase();Cursor cursor = db.query("lockinfo", null, null , null, null, null, null); while(cursor.moveToNext()){String packname = cursor.getString(cursor.getColumnIndex("packname"));packnames.add(packname);}cursor.close();db.close();//返回一個叫packnames的集合return packnames;} }課2
Activity的啟動模式重溫
standard 標準啟動模式
singletop 單一頂部模式
如果棧頂已經存在了activity,就不會重復的創建,而是復用棧頂已經存在的activity(瀏覽器的書簽)
singletask 單一任務棧模式
在任務棧里面只能存在一個實例,這個實例是存在在手機衛士默認的任務棧里面的
singleinstance 單例模式
在任務棧里面只能存在一個實例,這個實例是在自己單獨新建的任務棧里面 6
修改成7 其實這些啟動模式都是為了解決用戶的體驗感受的 程序鎖開發結束
流量統計
開始流量統計模塊 新建TrafficManagerActivity.java并配置清單文件
TrafficManagerActivity.java
public class TrafficManagerActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);<!-- 流量統計 --> <activity android:name="com.itheima.mobile47.TrafficManagerActivity"></activity>MainActivity加入節點case MainActivity.java
case 4://流量管理intent = new Intent(MainActivity.this,TrafficManagerActivity.class);startActivity(intent);break;安卓下如何獲取系統的流量信息?
類比:Windows-->網絡連接-->本地連接(網卡信息)可以統計出流量信息 安卓:TrafficStats類
Class that provides network traffic statistics. These statistics include bytes transmitted and received and network packets transmitted and received, over all interfaces, over the mobile interface, and on a per-UID basis. TrafficStats類的重要api
//獲取手機所有的網絡端口 , wifi , 2g/3g/4g(手機卡產生的流量) TrafficStats.getTotalRxBytes(); //r -->receive接收,獲取全部的接受的byte (下載) TrafficStats.getTotalTxBytes(); //t -->translate發送,獲取全部的發送的byte (上傳)TrafficStats.getMobileRxBytes();// 手機卡下載產生的流量 從開機開始到現在產出的流量 TrafficStats.getMobileTxBytes();// 手機卡上傳產生的流量
流量管理app由于TrafficStats類api的特點:
TrafficStats.getMobileRxBytes();// 手機卡下載產生的流量 從開機開始到現在產出的流量?
一般每5分鐘計算一次流量,一天下來就相加,來計算一個總的流量消耗量。
其實流量管理的app的程序邏輯無非是計時器和數據庫的操作
該方法就是獲取某個應用程序的流量,每個應用程序都有一個對應的uid用戶id,就因為有了內核uid,每個程序就有了不同的權限,換句話說,每個應用程序都有一個uid。
//在Android系統里面 ,給每一個應用程序都分配了一個用戶id //pid:進程id uid:用戶id TrafficStats.getUidRxBytes(10014); TrafficStats.getUidTxBytes(10014);
獲得3g/wifi產生了多少的流量
//分別列出來3g/wifi產生了多少的流量 //計時器。 ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); cm.getActiveNetworkInfo().getType();
//自動校驗流量 , 偷偷后臺給運營商發送短信。 //10010 10086 llcx
//聯網禁用 //linux平臺的防火墻軟件。必須手機要有root權限,修改iptable
實現騰訊的抽屜效果
TrafficManagerActivity.java的布局 activitytrafficmanager.xml 抽屜控件 要使用必須給該控件定義一個id TrafficManagerActivity.java
<SlidingDrawerandroid:orientation="horizontal"android:id="@+id/my_drawer"android:layout_width="match_parent"android:layout_height="match_parent"></SlidingDrawer>但是其實一個也不夠(設計界面報錯),還要定義很多個id:給把手handle也定義一個id,還有content內容也要定義一個id(還報錯),
<SlidingDrawerandroid:orientation="horizontal"android:id="@+id/my_drawer"android:layout_width="match_parent"android:layout_height="match_parent"android:content="@+id/my_content"android:handle="@+id/my_handle"> </SlidingDrawer>還需定義一個ImageView,然后imageview的id要引用SlidingDrawer的handle的id,
<SlidingDrawerandroid:orientation="horizontal"android:id="@+id/my_drawer"android:layout_width="match_parent"android:layout_height="match_parent"android:content="@+id/my_content"android:handle="@+id/my_handle" ><ImageViewandroid:id="@id/my_handle"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/ic_launcher" /></SlidingDrawer>還有LinearLayout里要引用SlidingDrawer的content的id,
<SlidingDrawerandroid:orientation="horizontal"android:id="@+id/my_drawer"android:layout_width="match_parent"android:layout_height="match_parent"android:content="@+id/my_content"android:handle="@+id/my_handle" ><ImageViewandroid:id="@id/my_handle"android:layout_width="wrap_content"android:layout_height="wrap_content"android:src="@drawable/ic_launcher" /><LinearLayoutandroid:background="#22000000"android:id="@id/my_content"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:orientation="vertical" ><TextView android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="我是抽屜里面的內容,哈哈哈"/></LinearLayout> </SlidingDrawer>
注意要把該布局文件復制過來! 這是從下往上拉 我們可以改變里orientation的屬性去改變拉的方向
課3
實現金山手機衛士小功能:常用手機號碼的效果
金山手機衛士小功能:常用手機號碼:
(listview嵌套這listview) 把金山apk里的數據庫文件拷貝過來?
觀察其數據庫 把數據庫信息展現到我們想實現的效果里 新建CommonNumberActivity.java和配置清單文件
CommonNumberActivity.java
public class CommonNumberActivity extends Activity {
private static final String path = "/data/data/com.itheima.mobile47/files/commonnum.db"; private ExpandableListView elv; private SQLiteDatabase db; @Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_common_num);清單文件
<!-- 常用號碼查詢 --> <activity android:name="com.itheima.mobile47.CommonNumberActivity"/>布局怎么寫呢? activitycommonnum.xml
<?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" ><TextViewstyle="@style/textview_title_style"android:text="常用號碼查詢" /><ExpandableListViewandroid:id="@+id/elv"android:layout_width="match_parent"android:layout_height="match_parent" ></ExpandableListView></LinearLayout>使用ExpandableListView控件,這樣的控件可以實現我們想要的效果 在CommonNumberActivity.java加載ExpandableListView控件。 ExpandableListView控件是Listview的子類。 在實現該控件的adapter的實現類時,如圖
這樣實現了接口中太多的方法,換成繼承BaseExpandableListAdapter 這靠譜 里面有方法我們需要的不多, 寫個界面加入到高級工具里 activity_tools.xml+ToolsActivity.java修改,進入到CommonNumberActivity.java
的效果如下
把數據庫的內容加載到ExpandableListView控件里
先把數據庫放到assets里 我們改造SplashActivcity-->copyDB--->data\data目錄下 SplashActivcity.java
try {// 拷貝數據庫到data/data目錄下面copyDB("address.db");copyDB("commonnum.db");新建CommonNumberDao.java:獲取到數據庫的路徑,然后就是操作數據庫獲取數據庫的信息
CommonNumberDao.java
/*** 獲取數據庫里面一共有多少個分組* @return*/ public static int getGroupCount(SQLiteDatabase db){Cursor cursor = db.rawQuery("select count(*) from classlist", null);cursor.moveToNext();int groupcount = cursor.getInt(0);cursor.close();return groupcount; }CommonNumberDao.java:
1、獲取數據庫里面一共有多少個分組 2、獲取數據庫里面某個分組有多少個孩子 3、獲取數據庫里面某個分組的名字 4、獲取數據庫里面某個分組里面某個孩子的名字和電話改造完CommonNumberDao.java后,回到CommonNumberActivity.java里的adapter進行改造。
CommonNumberActivity.java
//BaseXXX SimpleXXX DefaultXXX BaiscXXXX private class CommonNumAdapter extends BaseExpandableListAdapter{//獲取分組的數量@Overridepublic int getGroupCount() {return CommonNumberDao.getGroupCount(db);}//獲取孩子的數量@Overridepublic int getChildrenCount(int groupPosition) {return CommonNumberDao.getChildrenCount(db,groupPosition);}@Overridepublic Object getGroup(int groupPosition) {return null;}@Overridepublic Object getChild(int groupPosition, int childPosition) {return null;}@Overridepublic long getGroupId(int groupPosition) {return 0;}@Overridepublic long getChildId(int groupPosition, int childPosition) {return 0;}@Overridepublic boolean hasStableIds() {return false;}//獲取分組顯示的view@Overridepublic View getGroupView(int groupPosition, boolean isExpanded,View convertView, ViewGroup parent) {TextView tv;if(convertView!=null && convertView instanceof TextView){tv = (TextView) convertView;}else{tv= new TextView(getApplicationContext());}tv.setTextSize(20);tv.setTextColor(Color.RED);tv.setText(" "+CommonNumberDao.getGroupNameByPosition(db,groupPosition));return tv;}//獲取某個位置的孩子的view對象@Overridepublic View getChildView(int groupPosition, int childPosition,boolean isLastChild, View convertView, ViewGroup parent) {TextView tv;if(convertView!=null && convertView instanceof TextView){tv = (TextView) convertView;}else{tv= new TextView(getApplicationContext());}tv.setTextSize(16);tv.setTextColor(Color.BLACK);String info = CommonNumberDao.getChildInfoByPosition(db,groupPosition, childPosition);tv.setText(info.split("#")[0]+"\n"+info.split("#")[1]);return tv;}//指定孩子可以被點擊@Overridepublic boolean isChildSelectable(int groupPosition, int childPosition) {return true;}}運行:
但是不斷滑動時的最后app還是掛了,報出了內存溢出 改善程序:?
view復用 還有bug:展開所有,然后不斷拖動,也不會掛掉。但是如果拖動個幾千次上萬次,有可能數據打不開了,原因也是程序不斷打開關閉打開關閉的操作也讓數據庫最后垮了,
改造:數據庫改成在onCreate里打開,
@Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_common_num);db = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY);然后在onDestroy里關閉數據庫,同時要在CommonNumberDao.java里的增刪查改api把數據庫對象傳過來
@Override protected void onDestroy() {super.onDestroy();db.close(); }如果想讓孩子條目可以被點擊,首先要設置如下:
//指定孩子可以被點擊 @Override public boolean isChildSelectable(int groupPosition, int childPosition) {return true; } 然后在控件對象上設置:elv = (ExpandableListView) findViewById(R.id.elv); elv.setAdapter(new CommonNumAdapter()); elv.setOnChildClickListener(new OnChildClickListener() {@Overridepublic boolean onChildClick(ExpandableListView parent, View v,int groupPosition, int childPosition, long id) {System.out.println("被點擊了。"+CommonNumberDao.getChildInfoByPosition(db, groupPosition, childPosition));return false;} });
課4
緩存清理模塊:獲取緩存信息 (建議看多一次視頻:實用!)
緩存清理模塊介紹 什么是緩存?
data/data里的cache文件夾 每一個文件夾有可以創建一個cache的文件夾緩存文件
一個應用程序如何生成緩存?
新建工程:緩存測試
public class MainActivity extends Activity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);getFilesDir();//得到應用程序在文件系統上的路徑 /data/data/包名/filesgetCacheDir();//應用程序有臨時的數據需要保存 ,就可以存放在cache目錄,/data/data/包名/cachetry {File file = new File(getCacheDir(),"gaga.txt");FileOutputStream fos = new FileOutputStream(file);fos.write("sfdaf".getBytes());fos.close();} catch (Exception e) {e.printStackTrace();}}}
新建了工程后就可以使用金山手機位置使用緩存清理功能,可以搜出該工程產生的緩存文件。?
在手機設置里的應用欄目也會顯示:?
一般我們從網上下載了一些臨時的圖片,應用會自動放在cache里,當在手機設置里的應用欄目點擊清空緩存,可以把cache刪除。
新建工程:獲取緩存測試
劃紅線的數據谷歌怎么賦值的? 我們看系統上層所有的源碼中找到Setting 在工程里導入Setting,為了查找里面的源碼實現。 查找技巧,先搜界面,然后再搜java代碼,看源碼是怎么給劃紅線的數據賦值的 ctr+h--->全局搜索 緩存+*.xml?
然后根據線索一步一步的最后定位到了res--->cachesizetext--->InstalledAppDetails.java,獲取緩存的方法很明顯都在該java文件里
目標InstalledAppDetails.java,在該類里可以找到谷歌如何給劃紅線的數據賦值(緩存)
ctr+k快速搜索 下次嘗試操作尋找下。
源碼定位的邏輯:
我們的目標是劃紅線的緩存數據谷歌是怎么賦值
1、從布局出發:首先在Eclipse里使用ctr+h全局搜索:緩存 *.xml
2、從搜索到的信息
這是在setting源碼中布局文件xml中出現過的字段,打開文件可以找到該字段的來源
<string name="cache_header_label" msgid="1877197634162461830">"緩存"</string> <string name="clear_cache_btn_text" msgid="5756314834291116325">"清除緩存"</string> <string name="cache_size_label" msgid="7505481393108282913">"緩存"</string>3、根據這個來源再繼續全局搜索:cachesizelabel *.xml,這樣的目的是繼續搜索到設置中心的布局文件,并找到布局文件中cachesizelabel的id,搜索結果是installedappdetails.xml出現過cachesizelabel這個id
4、根據該布局文件可以分析出顯示緩存的數字的變量是:
<TextViewandroid:id="@+id/cache_size_text"android:textAppearance="?android:attr/textAppearanceMedium"android:paddingTop="6dip"android:paddingRight="6dip"android:layout_height="wrap_content"android:layout_width="wrap_content"android:maxLines="1" />5、繼續使用全局搜索:cachesizetext *.java,這樣可以搜到cachesizetext出自于哪個類文件:搜索結果是InstalledAppDetails.java里的一段代碼
// Cache sectionmCacheSize = (TextView) findViewById(R.id.cache_size_text);6、繼續順藤摸瓜:搜索: mCacheSize:找到一段關鍵代碼:
if (mLastCacheSize != mAppEntry.cacheSize) { mLastCacheSize = mAppEntry.cacheSize; mCacheSize.setText(getSizeStr(mAppEntry.cacheSize));7、我們繼續點進cacheSize觀察其來自于哪里:然后定位到了目標類ApplocationState.java,并發現了一段關鍵代碼:
public static class SizeInfo {long cacheSize;long codeSize;long dataSize; }8、然后繼續搜索,然后定位到了最關鍵的代碼塊:分析:
9、最后搜索遠程服務接口的實現類對象:mStatsObserver,最后定位到以下這段代碼:
10、然后我們在工程里使用PackageManager類來使用getPackageSizeInfo方法去獲取緩存值,但是遺憾的是,谷歌早就把getPackageSizeInfo方法給隱藏起來了:因此我們可以考慮使用反射的方法來獲取該方法
/*** Retrieve the size information for a package.* Since this may take a little while, the result will* be posted back to the given observer. The calling context* should have the {@link android.Manifest.permission#GET_PACKAGE_SIZE} permission.** @param packageName The name of the package whose size information is to be retrieved* @param userHandle The user whose size information should be retrieved.* @param observer An observer callback to get notified when the operation* is complete.* {@link android.content.pm.IPackageStatsObserver#onGetStatsCompleted(PackageStats, boolean)}* The observer's callback is invoked with a PackageStats object(containing the* code, data and cache sizes of the package) and a boolean value representing* the status of the operation. observer may be null to indicate that* no callback is desired.** @hide*/ public abstract void getPackageSizeInfo(String packageName, int userHandle,IPackageStatsObserver observer);繼續在工程里獲取緩存
技巧:如何觀看安卓源碼的某一個類中的某一個對象
在程序中打一個斷點。 我們使用debug窗口去查看我們要了解的一些比較難理解的類,配合搜索工具來找出其源碼父類的實現類。
code.google.com www.github.com 我們工作的好幫手使用反射機制拿出PackAgeManager類里被隱藏的方法:getPackageSizeInfo
然后還需調用遠程服務 實現遠程服務的接口 一系列工程完成后就可以獲取緩存。
還有權限要加上:
獲取緩存的邏輯(結合上面如何觀察源碼的方法和結論)
1、布局文件:需求:就是在輸入框里輸入一個應用的包名,然后點擊確定按鈕,就可以在控制臺返回緩存的size
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" ><EditTextandroid:hint="請輸入要獲取的應用程序的包名"android:id="@+id/et_packname"android:layout_width="match_parent"android:layout_height="wrap_content" /><Buttonandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerHorizontal="true"android:layout_centerVertical="true"android:onClick="click"android:text="獲取緩存信息" /></RelativeLayout>2、從之前源碼分析中知道我們可以從PackageManager類里獲取getPackageSizeInfo方法來獲取緩存size,因此我們獲取到PackageManager類對象pm,然后快捷鍵發現方法獲取不出來,查看PackageManager發現該方法是個抽象方法,也就是被谷歌隱藏了(hide)
3、那么我們就應該想辦法怎么獲得那個方法,方法就是(重要!)使用debug的方法來觀察其PackageManager的實現類是什么?我們隨便在程序中輸出些什么,然后在該輸出的地方打上一個斷點
public class MainActivity extends Activity { private EditText et_packname; @Override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);et_packname = (EditText) findViewById(R.id.et_packname); }public void click(View view){//獲取某個應用程序的緩存String packname = et_packname.getText().toString().trim();//使用該類可以獲取到緩存的方法getPackageSizeInfoPackageManager pm = getPackageManager();//但是該方法getPackageSizeInfo被谷歌隱藏,其在PackageManager類里只是一個抽象的方法,被隱藏了,那么技巧就是我們隨便在程序中輸出些東西,并打上斷點//以此來獲取PackageManager里的內容System.out.println("----");4、然后我們進入debug視圖觀察得到結果:其PackageManager的實現類就是:ApplicationPackageManager,找到其源碼發現:的確是繼承PackageManager并發現:
/*package*/ final class ApplicationPackageManager extends PackageManager {private static final String TAG = "ApplicationPackageManager";private final static boolean DEBUG = false;private final static boolean DEBUG_ICONS = false;并發現的確有方法 @Override public void getPackageSizeInfo(String packageName, int userHandle,IPackageStatsObserver observer) {try {mPM.getPackageSizeInfo(packageName, userHandle, observer);} catch (RemoteException e) {// Should never happen!} }5、那么我們就可以使用反射的機制把其方法反射出來了:
//pm.getPackageSizeInfo(); //這樣可以拿到PackageManager實現類的字節碼Method[] methods = PackageManager.class.getMethods();for(Method method : methods){if("getPackageSizeInfo".equals(method.getName())){try {method.invoke(pm, packname,new MyObserver());} catch (Exception e) {e.printStackTrace();}}}6、觀察源碼中的方法,其 mPM.getPackageSizeInfo(packageName, userHandle, observer);中的參數是一個遠程服務實現類:因為在之前的源碼分析中得到的結論,那么我們必須找到該遠程服務的aidl文件復制過來放在我們的工程目錄下(注意包名要和原文件里的包名一致)
7、把aidl遠程服務接口文件設置好后,就可以把其遠程服務的類定義出來并實現,并傳到反射出來的getPackageSizeInfo()方法里了。
private class MyObserver extends android.content.pm.IPackageStatsObserver.Stub{@Overridepublic void onGetStatsCompleted(PackageStats pStats, boolean succeeded)throws RemoteException {long size = pStats.cacheSize;System.out.println("緩存大小為:"+Formatter.formatFileSize(getApplicationContext(), size));} }8、最后使用該方法還需要獲取權限:
<uses-permission android:name="android.permission.GET_PACKAGE_SIZE"/>課5
手機衛士開發:緩存清理 (重看視頻如何引入源碼中的progressBar樣式然后如何自定義)
開發緩存清理 新建CleanCacheActivity.java+配置清單
在MainActivity.java增加點擊事件case 6
需求UI:
布局:activitycleancache.xml 在布局里引用了一個progressBar的特殊樣式。在源碼那里復制過來的。 在CleanCacheActivity.java加載自定義的progressBar,測試其效果。 需求:就是在進度條進行完后就顯示被進度條覆蓋的信息。 需求:模仿騰訊:每掃描一條cache,即顯示一行信息條目,動態的顯示(難) 繼續實現布局:activitycleancache.xml activitycleancache.xml
<?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" ><TextViewstyle="@style/textview_title_style"android:gravity="center"android:text="清理緩存" /><FrameLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content" ><ProgressBarandroid:id="@+id/pb_scan"android:layout_width="fill_parent"android:layout_height="15dip"android:indeterminateOnly="false"android:progressDrawable="@drawable/progress_horizontal" /><TextViewandroid:id="@+id/tv_scan_result"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="adfadsfa" /></FrameLayout><ScrollViewandroid:layout_width="match_parent"android:layout_height="0dip"android:layout_weight="1" ><LinearLayoutandroid:id="@+id/ll_container"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical" ></LinearLayout></ScrollView><Buttonandroid:onClick="cleanAll"android:layout_width="fill_parent"android:layout_height="wrap_content"android:text="清理全部" /></LinearLayout>(觀察源碼后學習的進度條樣式)progress_horizontal.xml
<?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2008 The Android Open Source ProjectLicensed under the Apache License, Version 2.0 (the "License");you may not use this file except in compliance with the License.You may obtain a copy of the License athttp://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, softwaredistributed under the License is distributed on an "AS IS" BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.See the License for the specific language governing permissions andlimitations under the License.--><layer-list xmlns:android="http://schemas.android.com/apk/res/android"><item android:id="@android:id/background" android:drawable="@drawable/security_progress_bg"></item><item android:id="@android:id/secondaryProgress" android:drawable="@drawable/security_progress"></item><item android:id="@android:id/progress" android:drawable="@drawable/security_progress"></item></layer-list>
- CleanCacheActivity.java編程
- 初始化控件
- 掃描所有的應用程序,查看他們的緩存信息;
- 掃描應用方法:scan(),耗時,所以放在子線程里。
- 獲取緩存getCacheInfo(String packname)
-
在掃描應用方法里去獲取應用的cache信息。
public class CleanCacheActivity extends Activity {protected static final int SCANNING = 1;protected static final int FINISHED = 2;public static final int ADD_CACHE_VIEW = 3;private TextView tv_scan_result;private ProgressBar pb_scan;private LinearLayout ll_container;private PackageManager pm;private Handler handler = new Handler() {public void handleMessage(android.os.Message msg) {switch (msg.what) {case SCANNING://這樣可以讓子線程中遍歷的應用信息滾動PackageInfo info = (PackageInfo) msg.obj;tv_scan_result.setText("正在掃描:"+ info.applicationInfo.loadLabel(pm));break;case ADD_CACHE_VIEW:break;case FINISHED://遍歷完應用接收到該信號就把UI中的TextView隱藏掉tv_scan_result.setText("掃描完畢!");pb_scan.setVisibility(View.INVISIBLE);break;}};}; @Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_clean_cache);//該類用來獲取緩存信息的pm = getPackageManager();tv_scan_result = (TextView) findViewById(R.id.tv_scan_result);pb_scan = (ProgressBar) findViewById(R.id.pb_scan);ll_container = (LinearLayout) findViewById(R.id.ll_container);// 掃描所有的應用程序,查看他們的緩存信息。scan();}private void scan() {new Thread() {public void run() {//從包管理者中獲取到所有已經安裝了的應用的包名集合List<PackageInfo> infos = pm.getInstalledPackages(0);//給進度條設置最大數pb_scan.setMax(infos.size());int progress = 0;for (PackageInfo info : infos) {//調用內部方法getCacheInfo(info);progress++;pb_scan.setProgress(progress);try {//模擬進度條運行耗時Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}//遍歷已經安裝的應用安裝包,并讓UI中的TextView顯示遍歷的效果,所以使用HandlerMessage msg = Message.obtain();msg.what = SCANNING;//把應用的對象發過去msg.obj = info;handler.sendMessage(msg);}//遍歷完就給Handler發信號把UI中的TextView隱藏Message msg = Message.obtain();msg.what = FINISHED;handler.sendMessage(msg);};}.start();}//該方法里使用反射技術獲取緩存public void getCacheInfo(PackageInfo info) {try {Method method = PackageManager.class.getMethod("getPackageSizeInfo", String.class,IPackageStatsObserver.class);method.invoke(pm, info.packageName, new MyObserver(info));} catch (Exception e) {e.printStackTrace();}}private class MyObserver extendsandroid.content.pm.IPackageStatsObserver.Stub {PackageInfo packinfo;public MyObserver(PackageInfo packinfo) {this.packinfo = packinfo;}@Overridepublic void onGetStatsCompleted(PackageStats pStats, boolean succeeded)throws RemoteException {// 這個方法不是運行在主線程中,所有不可以直接更新uilong size = pStats.cacheSize;System.out.println(packinfo.applicationInfo.loadLabel(pm)+ ",緩存大小為:"+ Formatter.formatFileSize(getApplicationContext(), size));}}}class CacheInfo {Drawable icon;String packname;String appname;long size; }
能找到數據。那么就是在scan給界面更新。 功能完成后 技巧:動態在布局文件里,例如LinearLayout里加載類似于listView樣式的條目顯示
我們需要把掃描到的信息條目怎么列在LinearLayout里?
ll_container.addView(xxx);
原因:
使用Handler解決就沒問題了。 但是使用Message發送緩存信息發現比較多元化,所以定義一個緩存信息的bean內部類CacheInfo,讓MEssage發給handler去處理。 測試的結果樣式比較簡單,
我們做的出專業些,定義一個條目布局:item_cacheinfo.xml
item_cacheinfo.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content" ><ImageViewandroid:id="@+id/iv_cache_icon"android:layout_width="40dip"android:layout_height="40dip"android:src="@drawable/ic_launcher" /><TextViewandroid:layout_centerVertical="true"android:layout_width="wrap_content"android:id="@+id/tv_cache_name"android:layout_height="wrap_content"android:layout_toRightOf="@id/iv_cache_icon"android:text="應用程序名稱"android:textColor="#000000"android:textSize="20sp" /><TextViewandroid:layout_centerVertical="true"android:layout_width="wrap_content"android:layout_alignParentRight="true"android:id="@+id/tv_cache_size"android:layout_height="wrap_content"android:text="緩存大小"android:textColor="#ff0000"android:textSize="14sp" /> </RelativeLayout>
然后在CleanCacheActivity.java的Handler里使用打氣筒inflate把條目布局給加載進去,在為條目布局控件賦值,賦值之后讓linearLayout的對象ll_container.addView(view)添加進來。
考慮到加載的條目比較多,我們在布局文件里再加上一個scrollBar控件,可以讓批量的條目支持滾動
CleanCacheActivity.java
public class CleanCacheActivity extends Activity {protected static final int SCANNING = 1;protected static final int FINISHED = 2;public static final int ADD_CACHE_VIEW = 3;private TextView tv_scan_result;private ProgressBar pb_scan;private LinearLayout ll_container;private PackageManager pm;private Handler handler = new Handler() {public void handleMessage(android.os.Message msg) {switch (msg.what) {case SCANNING://這樣可以讓子線程中遍歷的應用信息滾動PackageInfo info = (PackageInfo) msg.obj;tv_scan_result.setText("正在掃描:"+ info.applicationInfo.loadLabel(pm));break;case ADD_CACHE_VIEW:final CacheInfo cacheinfo = (CacheInfo) msg.obj;View view = View.inflate(getApplicationContext(),R.layout.item_cacheinfo, null);ImageView icon = (ImageView) view.findViewById(R.id.iv_cache_icon);TextView name = (TextView) view.findViewById(R.id.tv_cache_name);TextView size = (TextView) view.findViewById(R.id.tv_cache_size);icon.setImageDrawable(cacheinfo.icon);name.setText(cacheinfo.appname);size.setText(Formatter.formatFileSize(getApplicationContext(),cacheinfo.size));ll_container.addView(view);view.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {// pm.deleteApplicationCacheFiles(packageName,// mClearCacheObserver);try {Method method = PackageManager.class.getMethod("deleteApplicationCacheFiles",String.class, IPackageDataObserver.class);method.invoke(pm, cacheinfo.packname,new IPackageDataObserver.Stub() {@Overridepublic void onRemoveCompleted(String packageName, boolean succeeded)throws RemoteException {}});} catch (Exception e) {e.printStackTrace();}}});break;case FINISHED://遍歷完應用接收到該信號就把UI中的TextView隱藏掉tv_scan_result.setText("掃描完畢!");pb_scan.setVisibility(View.INVISIBLE);break;}};};@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_clean_cache);//該類用來獲取緩存信息的pm = getPackageManager();tv_scan_result = (TextView) findViewById(R.id.tv_scan_result);pb_scan = (ProgressBar) findViewById(R.id.pb_scan);ll_container = (LinearLayout) findViewById(R.id.ll_container);// 掃描所有的應用程序,查看他們的緩存信息。scan();}private void scan() {new Thread() {public void run() {//從包管理者中獲取到所有已經安裝了的應用的包名集合List<PackageInfo> infos = pm.getInstalledPackages(0);//給進度條設置最大數pb_scan.setMax(infos.size());int progress = 0;for (PackageInfo info : infos) {//調用內部方法getCacheInfo(info);progress++;pb_scan.setProgress(progress);try {//模擬進度條運行耗時Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}//遍歷已經安裝的應用安裝包,并讓UI中的TextView顯示遍歷的效果,所以使用HandlerMessage msg = Message.obtain();msg.what = SCANNING;//把應用的對象發過去msg.obj = info;handler.sendMessage(msg);}//遍歷完就給Handler發信號把UI中的TextView隱藏Message msg = Message.obtain();msg.what = FINISHED;handler.sendMessage(msg);};}.start();}//該方法里使用反射技術獲取緩存public void getCacheInfo(PackageInfo info) {try {Method method = PackageManager.class.getMethod("getPackageSizeInfo", String.class,IPackageStatsObserver.class);method.invoke(pm, info.packageName, new MyObserver(info));} catch (Exception e) {e.printStackTrace();}}private class MyObserver extendsandroid.content.pm.IPackageStatsObserver.Stub {PackageInfo packinfo;public MyObserver(PackageInfo packinfo) {this.packinfo = packinfo;}@Overridepublic void onGetStatsCompleted(PackageStats pStats, boolean succeeded)throws RemoteException {// 這個方法不是運行在主線程中,所有不可以直接更新uilong size = pStats.cacheSize;System.out.println(packinfo.applicationInfo.loadLabel(pm)+ ",緩存大小為:"+ Formatter.formatFileSize(getApplicationContext(), size));if (size > 0) {//在該遠程服務實現類里進行對緩存的數據處理,使用把數據通過Message發給Handler然后處理UIMessage msg = Message.obtain();msg.what = ADD_CACHE_VIEW;CacheInfo cacheinfo = new CacheInfo();cacheinfo.icon = packinfo.applicationInfo.loadIcon(pm);cacheinfo.packname = packinfo.packageName;cacheinfo.appname = packinfo.applicationInfo.loadLabel(pm).toString();cacheinfo.size = size;msg.obj = cacheinfo;handler.sendMessage(msg);}}}class CacheInfo {Drawable icon;String packname;String appname;long size;}/*** 清理全部的緩存* @param view*/public void cleanAll(View view){Method[] methods= PackageManager.class.getMethods();for(Method method : methods){if("freeStorageAndNotify".equals(method.getName())){try {method.invoke(pm, Integer.MAX_VALUE,new IPackageDataObserver.Stub() {@Overridepublic void onRemoveCompleted(String packageName, boolean succeeded)throws RemoteException {System.out.println("result:"+succeeded);}});} catch (Exception e) {e.printStackTrace();}return;}}}}課6
實現清理緩存信息(建議下午的觀察源碼的流程視頻看一遍)
接著清理緩存信息 需求:模仿金山:跳轉到設置中心的app設置中心 需求2:模仿騰訊:
找源碼,實現清除緩存的點擊功能
我們給條目設計一個點擊事件,把以上的源碼設計進來。 然后發現方法有不能直接調用,被谷歌工程師給隱藏了,所以又需要反射出來:?
CleanCacheActivity.java
view.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {// pm.deleteApplicationCacheFiles(packageName,// mClearCacheObserver);try {Method method = PackageManager.class.getMethod("deleteApplicationCacheFiles",String.class, IPackageDataObserver.class);method.invoke(pm, cacheinfo.packname,new IPackageDataObserver.Stub() {@Overridepublic void onRemoveCompleted(String packageName, boolean succeeded)throws RemoteException {}});} catch (Exception e) {e.printStackTrace();}}});然后點擊發現又報出異常信息:?
但是在加載權限是發現清單文件也報錯了?
那么我們強行刪除Problems中的信息,看是否可以運行。 發現不可行:那么按照之前的邏輯去反射谷歌隱藏的方法來實現功能不可行,那么有什么辦法清理應用緩存呢?
但是清理緩存的功能還是能實現的,那就是一鍵清理的安卓漏洞。
主要是安卓的一個服務會檢測系統的內存,如果發現內存不足,那么會自動把系統里的所有緩存給刪除,我們可以抓住此漏洞,模仿該特點去發送內存不足的信息給系統,一次達到一鍵清理緩存的功能實現。 在CleanCacheActivity里加入一鍵清理的功能cleanAll PackageManager-->freeStorageAndNotify
CleanCacheActivity.java
/*** 清理全部的緩存* @param view*/ public void cleanAll(View view){Method[] methods= PackageManager.class.getMethods();for(Method method : methods){if("freeStorageAndNotify".equals(method.getName())){try {method.invoke(pm, Integer.MAX_VALUE,new IPackageDataObserver.Stub() {@Overridepublic void onRemoveCompleted(String packageName, boolean succeeded)throws RemoteException {System.out.println("result:"+succeeded);}});} catch (Exception e) {e.printStackTrace();}return;}} }源碼解釋:?
使用反射來調用該方法。一次給系統發送信息。 點擊一鍵清理
雖然報出異常: 但是其實都已經刪除了 金山的SD卡清理
怎么實現?
SD卡不安全,每個應用程序都會在SD卡里寫一些內容,這些內容我們不可能去手工認知,但是金山已經把他們寫成數據庫文件了,我們就使用金山的數據庫來遍歷SD卡是否有一樣的文件,以此來找到并刪除。 金山的痕跡清理
沒什么好說的,呵呵
資料下載
總結
- 上一篇: ubuntu 14.04搭建wifi分享
- 下一篇: android7wifi怎么分享,教你如