開源組件DoraemonKit之Android版本技術實現(二)

1、引言

​ DoraemonKit是滴滴開源的研發助手組件,目前支持iOS和Android兩個平臺。經過接入DoraemonKit組件,能夠方便支持以下所示的多種調試工具:php

device-2019-01-27-231951

​ 本文是DoraemonKit之Android版本技術實現系列文章的第二篇,主要介紹各個經常使用工具的技術實現細節。java

2、技術實現

2.1 app基本信息

​ 不少時候,咱們在開發或者調試的過程當中須要查看一些手機或者app相關的參數,這些參數相似手機型號、操做系統版本和應用包名等。正由於有這樣的須要,DoraemonKit提供了彙總的app基本信息展現功能。android

device-2019-01-30-140714

如何獲取信息

​ 信息主要分兩大類,一類是手機信息,一類是App信息。手機信息主要經過Build類獲取,App信息主要經過Context及其相關類獲取。git

經過Build類獲取信息

​ 下面是Build類能夠獲取到的經常使用信息:github

字段 含義 示例
Build.BRAND 品牌 Meizu
Build.MANUFACTURER 廠商 Meizu
Build.DEVICE 型號 mx3
Build.VERSION.SDK_INT SDK版本 19
Build.CPU_ABI CPU ABI armeabi-v7a

​ Build類主要是經過讀取/system/build.prop文件中的配置,好比Build.MANUFACTURER就是其中ro.product.manufacturer對應的值,Build類中的值是系統預先讀取在內存中的,也能夠不經過Build類直接讀取build.prop文件。數據庫

經過Context類獲取信息

​ Context類是Android系統中最重要的類,是App和系統之間的紐帶,經過App的Context能夠獲取App相關的信息,如App的包名:瀏覽器

String packageName = context.getPackageName();
複製代碼

​ 獲取應用圖標:緩存

Drawable icon = context.getResources().getDrawable(context.getApplicationInfo().icon);
複製代碼

​ 獲取應用名:bash

String label = context.getString(context.getApplicationInfo().labelRes);
複製代碼

​ 判斷權限:app

if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
        || ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
    LogHelper.d(TAG, "No Location Permission");
    return;
}
複製代碼

2.2 文件瀏覽

​ 在開發和調試過程當中常常須要查看一些App自有目錄的文件內容,雖然Android系統一般會提供文件瀏覽器的系統應用,可是由於App自有目錄中大多數屬於私有目錄,因此若是App能夠集成一個本身的文件瀏覽功能就能夠很方便地查看私有目錄中的文件,好比sharedprefs配置。

device-2019-02-24-222854

​ 經過context獲取私有目錄的文件信息:

fileInfos.add(new FileInfo(context.getFilesDir().getParentFile()));
fileInfos.add(new FileInfo(context.getExternalCacheDir()));
fileInfos.add(new FileInfo(context.getExternalFilesDir(null)));
複製代碼

​ 而後就能夠根據File信息展現當前文件夾的信息,同時也能夠拿到子文件的信息,填充列表的Adapter就能夠展現如上圖所示的文件瀏覽器。

​ 哆啦A夢目前支持圖片查看和文本查看,默認的查看方式是文本查看,判斷文件種類的方式是根據文件後綴。

public static String getSuffix(File file) {
    if (file == null || !file.exists()) {
        return "";
    }
    return file.getName()
            .substring(file.getName().lastIndexOf(".") + 1)
            .toLowerCase(Locale.getDefault());
}
複製代碼

​ 哆啦A夢也支持分享到第三方應用查看,是經過FileProvider對外分享的,只有經過FileProvider才能將私有目錄中的文件分享出去:

Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri;
uri = FileProvider.getUriForFile(context, context.getPackageName() + ".debugfileprovider", file);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(uri, type);
if (intent.resolveActivity(context.getPackageManager()) == null) {
    intent.setDataAndType(uri, DATA_TYPE_ALL);
}
context.startActivity(intent);
複製代碼

​ 同時在FileProvider的path中須要聲明root-path,這樣才能包含全部的私有目錄。

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <root-path name="name" path="" />
</paths>
複製代碼

2.3 位置模擬

​ 位置模擬是地圖類應用十分經常使用的調試功能,哆啦A夢在實現位置模擬功能時主要嘗試了兩種方案。

​ 第一個方案是Android系統提供的LocationManager類下面TestProvider相關API,這個方案的實現很是容易,只須要調用相關的系統API:

mLocationManager.addTestProvider(name,
        provider.requiresNetwork(),
        provider.requiresSatellite(),
        provider.requiresCell(),
        provider.hasMonetaryCost(),
        provider.supportsAltitude(),
        provider.supportsSpeed(),
        provider.supportsBearing(),
        provider.getPowerRequirement(),
        provider.getAccuracy());
mLocationManager.setTestProviderEnabled(name, true);
mLocationManager.setTestProviderStatus(name, LocationProvider.AVAILABLE, null, System.currentTimeMillis());

複製代碼

​ 而後向provider中設置須要模擬的Location就能夠實現系統全局模擬GPS,它mock的不只限於應用自己,也能夠影響到其餘應用,因此不少位置模擬軟件都是使用這個方案實現的。

​ 可是這個方案的缺點也很明顯,第一點是須要在開發者模式設置頁中開啓模擬定位權限,這個缺點還比較容易接受。第二點是不少地圖SDK存在反做弊機制,會判斷獲取的Location是否來自TestProvider,Android系統自己就提供了判斷方法:

location.isFromMockProvider();

複製代碼

​ 經過測試發現經常使用的地圖SDK中,騰訊地圖和百度地圖不能使用TestProvider模擬定位,高德地圖和Google地圖能夠模擬定位,這就致使這個方案在不少時候都不能生效,並且由於咱們的SDK是關注於應用自己的調試功能的,因此不須要具有影響其餘應用的能力。

​ 第二個方案是經過Hook系統Binder服務的方式,動態代理Location Service。

public class LocationHookHandler implements InvocationHandler {
    private static final String TAG = "LocationHookHandler";

    private Object mOriginService;

    @SuppressWarnings("unchecked")
    @SuppressLint("PrivateApi")
    public LocationHookHandler(IBinder binder) {
        try {
            Class iLocationManager$Stub = Class.forName("android.location.ILocationManager$Stub");
            Method asInterface = iLocationManager$Stub.getDeclaredMethod("asInterface", IBinder.class);
            this.mOriginService = asInterface.invoke(null, binder);
        } catch (Exception e) {
            LogHelper.e(TAG, e.toString());
        }
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        switch (method.getName()) {
            case "requestLocationUpdates":
                ...
                break;
            case "getLastLocation":
            	...
                return lastLocation;
            case "getLastKnownLocation":
            	...
                return lastKnownLocation;
            default:
                break;
        }
        return method.invoke(this.mOriginService, args);
    }
}

複製代碼

​ 上面的代碼就是Location服務的代理類,經過替換原有requestLocationUpdates、getLastLocation和getLastKnownLocation接口的實現就能夠實現模擬定位,返回咱們想要模擬的位置,主要利用的就是InvocationHandler動態代理機制。

​ Android對系統服務主要是經過ServiceManager去管理的,且服務的實例是保存在靜態全局變量中的。

public final class ServiceManager {
    private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>();

	...
	
    public static IBinder getService(String name) {
        try {
            IBinder service = sCache.get(name);
            if (service != null) {
                return service;
            } else {
                return Binder.allowBlocking(getIServiceManager().getService(name));
            }
        } catch (RemoteException e) {
            Log.e(TAG, "error in getService", e);
        }
        return null;
    }
    
    ...

複製代碼

​ 服務實例保存在HashMap中,key是Context中定義的常量。

public static final String LOCATION_SERVICE = "location";

複製代碼

​ 因此能夠在應用初始化的時候提早替換掉sCache中的實例,這樣後面經過context.getSystemService獲取到的Service實例就是被動態代理的實例。

Class serviceManager = Class.forName("android.os.ServiceManager");
Method getService = serviceManager.getDeclaredMethod("getService", String.class);
IBinder binder = (IBinder) getService.invoke(null, Context.LOCATION_SERVICE);

ClassLoader classLoader = binder.getClass().getClassLoader();
Class[] interfaces = {IBinder.class};
BinderHookHandler handler = new BinderHookHandler(binder);
IBinder proxy = (IBinder) Proxy.newProxyInstance(classLoader, interfaces, handler);

Field sCache = serviceManager.getDeclaredField("sCache");
sCache.setAccessible(true);
Map<String, IBinder> cache = (Map<String, IBinder>) sCache.get(null);

cache.put(Context.LOCATION_SERVICE, proxy);
sCache.setAccessible(false);

複製代碼

​ 替換實例的時機須要儘量早,這樣才能保證在context.getSystemService前替換掉對應實例,因此在應用初始化的時機執行替換是比較推薦的。

2.4 Crash查看

​ 哆啦A夢目前只支持捕獲Java異常,後續會擴展到支持捕獲jni異常,捕獲Java異常主要經過設置UncaughtExceptionHandler,系統會在發生異常時通知到UncaughtExceptionHandler。

@Override
public void uncaughtException(Thread thread, Throwable ex) {
	...
}

複製代碼

​ 回調會返回發生異常的thread和異常信息。

2.5 日誌查看

device-2019-02-25-145354

​ 日誌查看功能主要是在手機端集成Logcat的相關功能,能夠過濾Log關鍵字,或者Log級別,核心邏輯是在手機端打印Logcat而後將獲取到的Log信息進行展現。

​ 打印Logcat能夠經過Runtime的exec函數實現。

Runtime.getRuntime().exec("logcat -c");
Process process = Runtime.getRuntime().exec("logcat -v time");
InputStream is = process.getInputStream();
InputStreamReader reader = new InputStreamReader(is);
BufferedReader br = new BufferedReader(reader);

String log;
while ((log = br.readLine()) != null && isRunning) {
    Message message = Message.obtain();
    message.what = MESSAGE_PUBLISH_LOG;
    message.obj = log;
    internalHandler.sendMessage(message);
}

br.close();
reader.close();
is.close();

複製代碼

​ 每一條Runtime.getRuntime().exec(…)表示在命令行中執行的一條命令,就和咱們再terminal中輸入命令是同樣的,返回值是執行命令行的Process,而後從Process中獲取InputStream,後面就能夠持續從Process中獲取Log信息了。

​ Log信息的解析代碼以下,能夠獲取level,packagePriority,message,date和time等多種屬性,後續能夠根據不一樣維度去作過濾和分類。

public LogInfoItem(String log) {
    orginalLog = log;
    if (log.contains("V/")) {
        level = Log.VERBOSE;
    } else if (log.contains("D/")) {
        level = Log.DEBUG;
    } else if (log.contains("I/")) {
        level = Log.INFO;
    } else if (log.contains("W/")) {
        level = Log.WARN;
    } else if (log.contains("E/")) {
        level = Log.ERROR;
    } else if (log.contains("A/")) {
        level = Log.ASSERT;
    }
    int beginIndex = log.indexOf(": ");
    if (beginIndex == -1) {
        meseage = log;
    } else {
        meseage = log.substring(beginIndex + 2);
    }
    beginIndex = log.indexOf("/");
    int endIndex = log.indexOf("/", beginIndex + 1);
    if (beginIndex != -1 && endIndex != -1) {
        packagePriority = log.substring(beginIndex + 1, endIndex - 3);
    }
    endIndex = log.indexOf(" ");
    if (endIndex != -1) {
        date = log.substring(0, endIndex);
    }
    beginIndex = endIndex;
    endIndex = log.indexOf(" ", beginIndex + 1);
    if (endIndex != -1 && beginIndex != -1) {
        time = log.substring(beginIndex, endIndex);
    }
}

複製代碼

2.6 緩存清理

​ 不少時候須要恢復APP到新安裝狀態,能夠經過系統中的應用設置頁實現,可是這樣須要不少步操做,因此哆啦A夢SDK集成了集成緩存的功能。

​ 基礎方法是清除某個文件夾的全部內容。

private static void deleteFilesByDirectory(File directory) {
    if (directory != null && directory.exists() && directory.isDirectory()) {
        for (File item : directory.listFiles()) {
            item.delete();
        }
    }
}

複製代碼

​ 刪除內部緩存。

public static void cleanInternalCache(Context context) {
    deleteFilesByDirectory(context.getCacheDir());
}

複製代碼

​ 刪除內部文件。

public static void cleanFiles(Context context) {
    deleteFilesByDirectory(context.getFilesDir());
}

複製代碼

​ 刪除SharedPrefs文件。

public static void cleanSharedPreference(Context context) {
    deleteFilesByDirectory(new File(context.getFilesDir().getParent() + "/shared_prefs"));
}

複製代碼

​ 刪除數據庫文件。

public static void cleanDatabases(Context context) {
    deleteFilesByDirectory(new File(context.getFilesDir().getParent() + "/databases"));
}

複製代碼

​ 清除外部緩存。

public static void cleanExternalCache(Context context) {
    if (Environment.getExternalStorageState().equals(
            Environment.MEDIA_MOUNTED)) {
        deleteFilesByDirectory(context.getExternalCacheDir());
    }
}

複製代碼

​ 由於SharedPrefs是讀取到內存的,因此生效必須重啓APP。

2.7 H5任意門

​ 實現很是簡單,就是經過註冊回調。

DoraemonKit.setWebDoorCallback(new WebDoorManager.WebDoorCallback() {
    @Override
    public void overrideUrlLoading(Context context, String url) {
        Intent intent = new Intent(App.this, WebViewActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.putExtra(WebViewActivity.KEY_URL, url);
        startActivity(intent);
    }
});

複製代碼

​ 在哆啦A夢的頁面輸入H5地址後,哆啦A夢會回調地址和Context,通知接入App調起H5容器跳轉到對應頁面。

3、總結

​ App基本信息的獲取主要經過Build類和Context及其相關類。

​ 文件瀏覽功能的關鍵是經過FileProvider把私有文件分享出去。

​ 位置模擬功能利用了InvocationHandler動態代理機制,代理了Location Service的接口實現。

​ Crash查看經過設置UncaughtExceptionHandler實現獲取發生線程和異常。

​ 日誌查看是經過命令行運行Logcat來獲取Log信息並展現的。

​ 緩存清理須要刪除內部緩存、內部文件、SharedPrefs、數據庫文件和外部緩存。

​ H5任意門只需註冊回調就能夠得到透傳的H5地址。

​ 經過這篇文章主要是但願你們可以對DoraemonKit經常使用工具的技術實現有一個瞭解,若是有好的想法也能夠參與到DoraemonKit開源項目的建設中來,在項目頁面提交Issues或者提交Pull Requests,相信DoraemonKit項目在你們的努力下會愈來愈完善。

​ DoraemonKit項目地址:github.com/didi/Doraem…,以爲不錯的話就給項目點個star吧。

4、交流羣

20190127231730
相關文章
相關標籤/搜索