标签归档:android

散列搜索 & HashMap

搜索算法

包含:顺序搜索、二分搜索、散列搜索。

顺序搜索的性能为 O(N);
二分搜索的性能为 O(logN),但是二分搜索需要集合有序;
散列搜索的性能为 O(1)。

二分搜索

二分搜索涉及到元素的存储结构,包含一般的数组和二叉树。而二叉树又引出了平衡二叉树、红黑二叉树等数据结构。

二叉树结构对于频繁要求集合更新的情况有很大的性能提升。

平衡二叉树的左子树和右子树高度差小于1,避免了单边子树的高度过长,影响搜索性能。

红黑二叉树的左子树和右子树的高度差小于2倍,是为了在平衡二叉树结构对于搜索过程提升的性能和维护该结构的调整消耗间取得平衡。

散列搜索

散列搜索的性能在拥有高效的散列算法时性能为 O(1)。
我这样理解:把散列后形成的集合比作一个把源数据当成下标的数组,搜索性能相当于根据数组下标取数据。

散列搜索的劣势在于无法做到数据的有序性。

HashMap

用 Hash 实现的 Map。

Hash

  1. 合理设计的 Hash 算法可以把源数据均匀地分布到一个固定范围内。
容量超过一定比例后需要扩容。
  2. Hash 冲突可以通过开放地址法和链地址法解决。
Java 采用了链地址法,且在链表长度大于8时会转为红黑树结构。

Map

HashMap: 非线程安全,无序,数组
HashTable: 遗留类,不建议使用
LinkedHashMap: 保留插入顺序的 HashMap
TreeMap: 有序的 Map,红黑树
ConcurrentHashMap: 线程安全的 HashMap

SparseArray/ArrayMap: Android 版的 HashMap,通过双数组实现,适合数据量小的时候用。

参考

  1. Java 8系列之重新认识HashMap
  2. 《算法技术手册》第5章 搜索算法

Android 接口破解实践

记一下这段时间破解 app 接口的心得吧。

获取 apk 文件

如果是系统 app,无需 root 权限即可在 system/app、system/partner-app、system/priv-app 下取到。

apk 文件处理

如果是系统 app,那么取出来的 apk 会是进行过 odex 优化过的,需要把 odex 合入 apk。

可以参考Android反编译odex然后重新打包

反编译 Java

工具

  1. jadx
https://github.com/skylot/jadx
使用 “control + T” 快捷键,利用类结构快速定位调用关系
  2. JD-GUI、Luyten 等
感觉都不如 jadx 反编的效果好
  3. http://www.javadecompilers.com/
在线反编译,非常不错。jadx 反编错误的时候可以试试这个

抓包

使用 Charles 等工具获取接口的网络请求包,筛选有特点的字符串。

定位

  1. 搜索
在 jadx 中搜索抓包中筛选出的字符串,寻找网络请求拼接的地方。
  2. 查找 Service
如果是通过 AIDL Service 对外提供服务的,那么可以从 AIDL 的 Binder 处快速定位接口信息。

解密

  1. 分析使用的 HTTP 库
大部分代码混淆过后很难查看,可以先确认下使用的 HTTP 库,去除 HTTP 库内部的逻辑干扰。可以 Google 有特点的字符串来搜索是哪个库。
  2. 加密方法
可以直接复制 jadx 反编译的结果。
  3. 加密密钥
藏在 Java 代码中的很好找,如果藏在 so 包中就需要模拟调用 so 包中的接口了。

密钥

  1. 新建一个 AndroidStudio 项目
保持和破解对象的包名一致。
  2. 引入破解对象中的 so 文件
  3. 复制 jadx 反编后 load jni 的类
  4. 模拟调用,获取结果
  5. 如果 so 包中没有加入“签名校验”,或只是简单校验了包名等可以伪装的信息,就可以获取密钥了。
  6. 如果 so 包中加入了“签名校验”,需要用 ida pro 反编 so 包具体分析了,请参考参考链接2。

参考链接

  1. Android逆向之旅—Android应用的安全的攻防之战
  2. 逆向札记-利用IDA简单过so签名校验

安卓性能分析

磁盘

工具:STRICTMODE
问题:主线程 I/O

问题1

存在未关闭的Closable对象

StrictMode: A resource was acquired at attached stack trace but never released. See java.io.Closeable for information on avoiding resource leaks.
StrictMode: java.lang.Throwable: Explicit termination method 'close' not called
StrictMode:    at dalvik.system.CloseGuard.open(CloseGuard.java:180)
StrictMode:    at android.database.CursorWindow.<init>(CursorWindow.java:111)
StrictMode:    at android.database.AbstractWindowedCursor.clearOrCreateWindow(AbstractWindowedCursor.java:198)
StrictMode:    at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:138)
StrictMode:    at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:132)
StrictMode:    at com.yulore.basic.provider.db.handler.CityProDBHandler.a(CityProDBHandler.java:84)
StrictMode:    at com.yulore.basic.provider.db.handler.AbsDBHandler.querySync(AbsDBHandler.java:160)
StrictMode:    at com.yulore.basic.provider.db.handler.CityProDBHandler.querySync(CityProDBHandler.java:61)
StrictMode:    at com.yulore.basic.location.CityDataBizManager.getCityByName(CityDataBizManager.java:154)
StrictMode:    at com.yulore.basic.location.a$a.onReceiveLocation(BDLocationImpl.java:83)
StrictMode:    at com.baidu.location.LocationClient.callListeners(Unknown Source)
StrictMode:    at com.baidu.location.LocationClient.onNewLocation(Unknown Source)
StrictMode:    at com.baidu.location.LocationClient.access$2900(Unknown Source)
StrictMode:    at com.baidu.location.LocationClient$a.handleMessage(Unknown Source)
StrictMode:    at android.os.Handler.dispatchMessage(Handler.java:102)
StrictMode:    at android.os.Looper.loop(Looper.java:154)
StrictMode:    at android.app.ActivityThread.main(ActivityThread.java:6186)
StrictMode:    at java.lang.reflect.Method.invoke(Native Method)
StrictMode:    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:889)
StrictMode:    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779)
StrictMode: Finalizing a Cursor that has not been deactivated or closed. database = /data/user/0/com.huawei.yellowpagetest/databases/YULORE_DB, table = city, query = SELECT province.*, city.* FROM city left outer join province on pro_id = province_id WHERE city_name like ? AND city.pro_id = province.province_id
StrictMode: android.database.sqlite.DatabaseObjectNotClosedException: Application did not close the cursor or database object that was opened here
StrictMode:    at android.database.sqlite.SQLiteCursor.<init>(SQLiteCursor.java:98)
StrictMode:    at android.database.sqlite.SQLiteDirectCursorDriver.query(SQLiteDirectCursorDriver.java:50)
StrictMode:    at android.database.sqlite.SQLiteDatabase.rawQueryWithFactory(SQLiteDatabase.java:1318)
StrictMode:    at android.database.sqlite.SQLiteDatabase.queryWithFactory(SQLiteDatabase.java:1165)
StrictMode:    at android.database.sqlite.SQLiteDatabase.query(SQLiteDatabase.java:1036)
StrictMode:    at android.database.sqlite.SQLiteDatabase.query(SQLiteDatabase.java:1242)
StrictMode:    at com.yulore.basic.provider.db.c.a(DatabaseDAO.java:76)
StrictMode:    at com.yulore.basic.provider.db.c.a(DatabaseDAO.java:68)
StrictMode:    at com.yulore.basic.provider.db.c.a(DatabaseDAO.java:63)
StrictMode:    at com.yulore.basic.provider.db.c.a(DatabaseDAO.java:58)
StrictMode:    at com.yulore.basic.provider.db.c.a(DatabaseDAO.java:53)
StrictMode:    at com.yulore.basic.provider.db.d.a(DatabaseDAOProxy.java:43)
StrictMode:    at com.yulore.basic.provider.db.handler.AbsDBHandler.querySync(AbsDBHandler.java:160)
StrictMode:    at com.yulore.basic.provider.db.handler.CityProDBHandler.querySync(CityProDBHandler.java:61)
StrictMode:    at com.yulore.basic.location.CityDataBizManager.getCityByName(CityDataBizManager.java:154)
StrictMode:    at com.yulore.basic.location.a$a.onReceiveLocation(BDLocationImpl.java:83)
StrictMode:    at com.baidu.location.LocationClient.callListeners(Unknown Source)
StrictMode:    at com.baidu.location.LocationClient.onNewLocation(Unknown Source)
StrictMode:    at com.baidu.location.LocationClient.access$2900(Unknown Source)
StrictMode:    at com.baidu.location.LocationClient$a.handleMessage(Unknown Source)
StrictMode:    at android.os.Handler.dispatchMessage(Handler.java:102)
StrictMode:    at android.os.Looper.loop(Looper.java:154)
StrictMode:    at android.app.ActivityThread.main(ActivityThread.java:6186)
StrictMode:    at java.lang.reflect.Method.invoke(Native Method)
StrictMode:    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:889)
StrictMode:    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779)

定位到 CityProDBHandler.java

@Override
protected List<City> parseResult(Cursor cursor) {
    if (cursor == null || cursor.getCount() == 0) {
        return null;
    }
    List<City> cityList = new ArrayList<>();
    try {
        String cityId = null;
        cursor.moveToPosition(-1);
        while (cursor.moveToNext()) {
            String tmpCityId = cursor.getString(cursor.getColumnIndex(ProvinceCityContract.City.CITY_ID));
            if (TextUtils.isEmpty(cityId) || !cityId.equals(tmpCityId)) {
                cityId = tmpCityId;
                City city = cursorToBean(cursor, null);
                cityList.add(city);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }
    return cityList;
}

问题2

存在未关闭的Closable对象

StrictMode: A resource was acquired at attached stack trace but never released. See java.io.Closeable for information on avoiding resource leaks.
StrictMode: java.lang.Throwable: Explicit termination method 'close' not called
StrictMode:    at dalvik.system.CloseGuard.open(CloseGuard.java:180)
StrictMode:    at android.database.CursorWindow.<init>(CursorWindow.java:111)
StrictMode:    at android.database.AbstractWindowedCursor.clearOrCreateWindow(AbstractWindowedCursor.java:198)
StrictMode:    at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:138)
StrictMode:    at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:132)
StrictMode:    at com.yulore.basic.provider.db.handler.FootMarkHandler.c(FootMarkHandler.java:81)
StrictMode:    at com.yulore.basic.provider.db.handler.FootMarkHandler.insertExec(FootMarkHandler.java:27)
StrictMode:    at com.yulore.basic.provider.db.handler.AbsDBHandler.insertSync(AbsDBHandler.java:205)
StrictMode:    at com.yulore.basic.provider.db.handler.AbsDBHandler$InsertTask.run(AbsDBHandler.java:427)
StrictMode:    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
StrictMode:    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
StrictMode:    at java.lang.Thread.run(Thread.java:761)
StrictMode: Finalizing a Cursor that has not been deactivated or closed. database = /data/user/0/com.huawei.yellowpagetest/databases/YULORE_DB, table = footmark, query = SELECT footmark.* FROM footmark WHERE title = ?
StrictMode: android.database.sqlite.DatabaseObjectNotClosedException: Application did not close the cursor or database object that was opened here
StrictMode:    at android.database.sqlite.SQLiteCursor.<init>(SQLiteCursor.java:98)
StrictMode:    at android.database.sqlite.SQLiteDirectCursorDriver.query(SQLiteDirectCursorDriver.java:50)
StrictMode:    at android.database.sqlite.SQLiteDatabase.rawQueryWithFactory(SQLiteDatabase.java:1318)
StrictMode:    at android.database.sqlite.SQLiteDatabase.queryWithFactory(SQLiteDatabase.java:1165)
StrictMode:    at android.database.sqlite.SQLiteDatabase.query(SQLiteDatabase.java:1036)
StrictMode:    at android.database.sqlite.SQLiteDatabase.query(SQLiteDatabase.java:1242)
StrictMode:    at com.yulore.basic.provider.db.c.a(DatabaseDAO.java:76)
StrictMode:    at com.yulore.basic.provider.db.c.a(DatabaseDAO.java:68)
StrictMode:    at com.yulore.basic.provider.db.c.a(DatabaseDAO.java:63)
StrictMode:    at com.yulore.basic.provider.db.c.a(DatabaseDAO.java:58)
StrictMode:    at com.yulore.basic.provider.db.c.a(DatabaseDAO.java:53)
StrictMode:    at com.yulore.basic.provider.db.d.a(DatabaseDAOProxy.java:43)
StrictMode:    at com.yulore.basic.provider.db.handler.FootMarkHandler.a(FootMarkHandler.java:41)
StrictMode:    at com.yulore.basic.provider.db.handler.FootMarkHandler.c(FootMarkHandler.java:80)
StrictMode:    at com.yulore.basic.provider.db.handler.FootMarkHandler.insertExec(FootMarkHandler.java:27)
StrictMode:    at com.yulore.basic.provider.db.handler.AbsDBHandler.insertSync(AbsDBHandler.java:205)
StrictMode:    at com.yulore.basic.provider.db.handler.AbsDBHandler$InsertTask.run(AbsDBHandler.java:427)
StrictMode:    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
StrictMode:    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
StrictMode:    at java.lang.Thread.run(Thread.java:761)

定位 FootMarkHandler.java

@Override
protected long insertExec(Footmark footmark) {
    Cursor cursor = queryExec(footmark.getName());
    if (cursor != null && cursor.getCount() != 0) {
        return updateExec(footmark);
    }
    return mDatabaseDAOProxy.insert(footmark);
}

问题3

对SharedPreferences写入操作,建议优先调用apply而非commit

StrictMode: StrictMode policy violation; ~duration=21 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=65567 violation=2
StrictMode:    at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1293)
StrictMode:    at libcore.io.BlockGuardOs.stat(BlockGuardOs.java:298)
StrictMode:    at android.system.Os.stat(Os.java:501)
StrictMode:    at android.app.SharedPreferencesImpl.writeToFile(SharedPreferencesImpl.java:649)
StrictMode:    at android.app.SharedPreferencesImpl.-wrap2(SharedPreferencesImpl.java)
StrictMode:    at android.app.SharedPreferencesImpl$2.run(SharedPreferencesImpl.java:535)
StrictMode:    at android.app.SharedPreferencesImpl.enqueueDiskWrite(SharedPreferencesImpl.java:554)
StrictMode:    at android.app.SharedPreferencesImpl.-wrap0(SharedPreferencesImpl.java)
StrictMode:    at android.app.SharedPreferencesImpl$EditorImpl.commit(SharedPreferencesImpl.java:476)
StrictMode:    at com.yulore.basic.utils.SharedPreferencesUtil.putString(SharedPreferencesUtil.java:107)
StrictMode:    at com.yulore.basic.utils.SystemUtil.getIMEI(SystemUtil.java:82)
StrictMode:    at com.yulore.basic.YuloreEngine.register(YuloreEngine.java:48)
StrictMode:    at com.yulore.framework.FrameworkApplication.onCreate(FrameworkApplication.java:31)
StrictMode:    at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1025)
StrictMode:    at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5448)
StrictMode:    at android.app.ActivityThread.-wrap2(ActivityThread.java)
StrictMode:    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1564)
StrictMode:    at android.os.Handler.dispatchMessage(Handler.java:102)
StrictMode:    at android.os.Looper.loop(Looper.java:154)
StrictMode:    at android.app.ActivityThread.main(ActivityThread.java:6186)
StrictMode:    at java.lang.reflect.Method.invoke(Native Method)
StrictMode:    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:889)
StrictMode:    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779)

YuloreEngine 中在主线程获取 IMEI,最终调用至
SharedPrefrencesUtil.java

public void putString(String key, String value) {
    SharedPreferences.Editor editor = this.sp.edit();
    editor.putString(key, value);
    editor.commit();
}

问题4

主线程中出现文件读写违例,建议使用工作线程(必要时结合Handler)完成

StrictMode: StrictMode policy violation; ~duration=24 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=65567 violation=2
StrictMode:    at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1293)
StrictMode:    at java.io.UnixFileSystem.checkAccess(UnixFileSystem.java:249)
StrictMode:    at java.io.File.exists(File.java:780)
StrictMode:    at android.app.ContextImpl.getDataDir(ContextImpl.java:1938)
StrictMode:    at android.app.ContextImpl.getCacheDir(ContextImpl.java:582)
StrictMode:    at android.content.ContextWrapper.getCacheDir(ContextWrapper.java:253)
StrictMode:    at com.yulore.volley.toolbox.Volley.newRequestQueue(Volley.java:43)
StrictMode:    at com.yulore.volley.toolbox.Volley.newRequestQueue(Volley.java:78)
StrictMode:    at com.yulore.yellowpage.base.integration.VolleyUrlLoader$Factory.getInternalQueue(VolleyUrlLoader.java:32)
StrictMode:    at com.yulore.yellowpage.base.integration.VolleyUrlLoader$Factory.<init>(VolleyUrlLoader.java:43)
StrictMode:    at com.yulore.yellowpage.base.integration.VolleyGlideModule.registerComponents(VolleyGlideModule.java:41)
StrictMode:    at com.bumptech.glide.Glide.get(Glide.java:157)
StrictMode:    at com.yulore.yellowpage.YulorePageEngine.register(YulorePageEngine.java:30)
StrictMode:    at com.yulore.framework.FrameworkApplication.onCreate(FrameworkApplication.java:32)
StrictMode:    at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1025)
StrictMode:    at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5448)
StrictMode:    at android.app.ActivityThread.-wrap2(ActivityThread.java)
StrictMode:    at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1564)
StrictMode:    at android.os.Handler.dispatchMessage(Handler.java:102)
StrictMode:    at android.os.Looper.loop(Looper.java:154)
StrictMode:    at android.app.ActivityThread.main(ActivityThread.java:6186)
StrictMode:    at java.lang.reflect.Method.invoke(Native Method)
StrictMode:    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:889)
StrictMode:    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:779)

YulorePageEngine 中在主线程配置 Glide,最终调用至
Volley.java

public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
    File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

    String userAgent = "volley/0";
    try {
        String packageName = context.getPackageName();
        PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
        userAgent = packageName + "/" + info.versionCode;
    } catch (NameNotFoundException e) {
    }

    if (stack == null) {
        if (Build.VERSION.SDK_INT >= 9) {
            stack = new HurlStack();
        } else {
            // Prior to Gingerbread, HttpUrlConnection was unreliable.
            // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
            stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
        }
    }

    Network network = new BasicNetwork(stack);

    RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
    queue.start();

    return queue;
}

内存

工具:LeakCanary
问题:Activity 内存泄漏

问题1

LeakCanary 捕捉到 SearchListActivity 内存泄漏,概率高。

Screenshot_20171127-170017-w540

问题2

Activity 中放入线程池中的任务如果不能及时执行完毕,在 Activity 销毁时很容易导致内存泄漏。

比如 DetailActivity 在 onDestroy 时就针对该情况作了处理:

@Override
protected void onDestroy() {
    super.onDestroy();
    RequestManager.cancelAll(TAG);
    if (mQueryInfoTask != null) {
        ThreadManager.getInstance().cancel(mQueryInfoTask);
    }
}

但是其他的 Activity 中并未做处理,包括:

  1. CategoryListActivity 中请求分类数据的任务
    private void requestData() {
        int cityId = mSharedPreferencesUtil.getInt(Constant.LOCATION_CITY_ID, 0);
        mCategoryListManager.queryData(categoryLink, cityId, 0.0, 0.0, mCategoryListener);
    }
    
  2. SelectCityActivity 中获取并处理城市数据的 AsyncTask
    new AsyncTask<Void, Void, Void>() {
    
        @Override
        protected Void doInBackground(Void... params) {
    
            //加载定位固定项
            SortCityModel locationCity = new SortCityModel();
            locationCity.setCityName(mLocationCityTitle);
            locationCity.setSortLetters(SideBar.STAR_STR);
            locationCity.setPinyinFull(mLocationCityTitle);
            locationCity.setCityId(-1);
            //加载热门固定项
            SortCityModel hotCity = new SortCityModel();
            hotCity.setCityName(mHotCityTitle);
            hotCity.setSortLetters(SideBar.POUND_STR);
            hotCity.setPinyinFull(mHotCityTitle);
            hotCity.setCityId(-1);
    
            mAllCityList.add(locationCity);
            mAllCityList.add(hotCity);
            //加载数据库所有城市列表
            List<City> allCityList = mCityManager.getAllCityListSync();
            List<City> hotCityList = new ArrayList<>();
            if (allCityList == null || allCityList.size() == 0) {
                return null;
            }
            //加载热门城市列表
            for (City city : allCityList) {
                int hot = city.getHot();
                if (hot == CITY_HOT) {
                    hotCityList.add(city);
                }
            }
            List<SortCityModel> tmpAllCityList = filledData(allCityList);
            Collections.sort(tmpAllCityList, mPinyiComparator);
            mAllCityList.addAll(tmpAllCityList);
            if (hotCityList != null && hotCityList.size() > 0) {
                mHotCityList = filledData(hotCityList);
                Collections.sort(mHotCityList, mPinyiComparator);
            }
            return null;
        }
    
        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            mLoadView.setVisibility(View.GONE);
            if (mAllCityList != null && mAllCityList.size() > 0) {
                mCityListAdapter = new CityListAdapter(mContext, mAllCityList, mHotCityList);
                mCityListAdapter.setOnHotCityListItemClickListener(SelectCityActivity.this);
                mCityListView.setAdapter(mCityListAdapter);
                mCityListView.setOnScrollListener(SelectCityActivity.this);
            } else {
                Toast.makeText(mContext, R.string.select_city_content_empty, Toast.LENGTH_SHORT).show();
            }
        }
    }.execute();
    
  3. HomeFragment 中刷新数据的任务
    @Override
    public void onRefresh(PullToRefreshBase<ScrollView> refreshView) {
        RequestManager.addRequest(new HomeDataRequest(mActivity, 0, listener), TAG);
    }
    /*
    * 设置联网后activity刷新数据
    * */
    public void onRefresh(){
        RequestManager.addRequest(new HomeDataRequest(mActivity, 0, listener), TAG);
    }
    
  4. NearlyListActivity 中获取附近页面数据的任务
    private void requestNearlyData() {
        Logger.d(TAG,"requestNearlyData");
        if (NetworkUtil.isNetConnected(getApplicationContext())){
            NearbyListManager.getInstance().queryDataFromOnline(mURL, mCityId, mStart, mSize, mLat, mLng, mDataListener);
        }else {
            mNoNetworkFailure.setVisibility(View.VISIBLE);
            mLoadingLayout.setVisibility(View.GONE);
        }
    }
    

问题3

Activity 中匿名 Handler 类对其的引用可能导致的内存泄漏,建议修改为静态内部类,通过 WeakReference 来引用外部 Activity。

TeleprompterFragment.java 中

private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        switch (msg.what) {
            case LOCAL_TIP:
                String key = (String) msg.obj;
                if (mKeyword != null && mKeyword.equals(key)) {
                    if (mKeywordTipsAdapter == null) {
                        List<String> list = new ArrayList<>();
                        list.add(key);
                        mKeywordTipsAdapter = new KeywordTipsAdapter(mActivity);
                        mTipsListView.setAdapter(mKeywordTipsAdapter);
                    } else {
                        mKeywordTipsAdapter.replaceByOne(key);
                    }
                    setHotAndRecordGone();
//                    if (shopAdapter != null){
//                        shopAdapter.clear();
//                    }
                }
                break;
        }
    }
};

问题4

对于系统服务来说它们的生命周期一般是跟随 APP 的完整生命周期,所以它们如果对外有引用,按照 Java 的声明周期延长法则,这些外部对象也都会被延长生命周期,进而产生内存泄漏。因此在使用系统服务时尽量避免使用界面的 Context。

比如:

// OpenWebFragment.java
mActivity.getSystemService(Context.LOCATION_SERVICE);

// 建议修改为
mActivity.getApplicationContext().getSystemService(Context.LOCATION_SERVICE)

网络

工具:Charles(抓包工具)
问题:无效流量消耗

问题1

CategoryListActivity & MoreCatListActivity

  1. Activity 中的数据为热门分类,应该是有预置的 logo,但在有网络的情况下会优先联网加载,没有充分利用离线资源;
  2. 每次进入都会到网上请求该分类的数据,但是这些数据更新很少,应该设置更新间隔或利用缓存,不要每次都下载数据,浪费用户流量。

问题2

DetailActivity

  1. 每次进入都会请求 Service 数据,请确认是否需要这部分数据,如果不需要请屏蔽该请求,如需要请考虑是否可以设置缓存。

问题3

SearchListActivity

  1. 请求热门搜索词的请求中会返回大量无关数据

-w508

如上图,有用的数据是 freqsearch 中的内容只有7个,而无用的 category 中的内容却有 292 个,且获取到的 category 数据被舍弃掉了。
应该请服务器不要返回 category 中的数据。

问题4

统计功能

在搜索结果页和详情页等地方会进行一些数据统计并上传,但是上传都失败了。如下图:

-w914

其他

权限


  1. apk 申请了很多无用权限,例如闪光灯、振动、通话等权限;

资源


  1. apk 中引入的第三发支付 SDK (微信、支付宝)共 374K,如无需要,请删除 jar 包、相关的 Java 类及 AndroidManifest.xml 中的配置;
  2. libdiff.so 文件共 485K,用于增量更新,该功能仅适用于黑白名单、归属地等文件,不包含热线文件,请考虑是否需要保留;
  3. gradle 中引入的依赖库,有部分是不需要的,比如:recyclerview、cardview、zxing 等,请删除。

数据库设计

需求分析

  1. 实体与实体之间的关系
  2. 实体所包含的属性
  3. 哪些属性或属性的组合可以唯一标识一个实体

逻辑设计

  1. 第一范式
数据库表中的所有字段都是单一属性,不可再分。
即要求数据表都是二维表。


  2. 第二范式
数据库表中不存在非关键字段对任一候选关键字段的部分函数依赖。
部分函数依赖是指存在着组合关键字中的某一关键字决定非关键字的情况。
即所有单关键字段的表都符合第二范式。



  3. 第三范式
第三范式是在第二范式的基础上定义的,如果数据表中不存在非关键字段对任意候选关键字段的传递函数依赖则符合第三范式。



  4. BC 范式
在第三范式的基础上,数据库表中如果不存在任何字段对任一候选关键字段的传递函数依赖则符合 BC 范式。
也就是说如果是复合关键字,则复合关键字之间也不能存在函数依赖关系。




物理设计

  1. 选用合适的数据库
  2. 反范式化
为了性能和读取效率的考虑而适当的对第三范式的要求进行违反,以空间换时间。


维护优化

  1. 批量操作 VS 逐条操作
  2. 禁止使用 Select * 这样的查询
  3. 控制使用用户自定义函数
  4. 不要使用数据库中的全文索引

参考

数据库设计那些事

Android 读取数据不完整的问题

问题描述

低概率性加载图片不完整,如下图:

日志

 D skia    : libjpeg error 105 <  Ss=0, Se=63, Ah=0, Al=0> from Incomplete image data 

分析

搜索

从日志看到大致是使用的图片数据是不完整的。上网搜索错误日志得到类似问题 Problem decoding into existing bitmap,useing V4 #1843
在该链接最后列举了两种可能导致数据不完整的例子:

OutputStream networkStream = ...;
BufferedOutputStream out = new BufferedOutputStream(networkStream);
for (some data) out.write(some data);
networkStream.close(); // this is wrong
// the end of some data may be still in memory of the buffered stream
// you need to close the outer stream (out) first, so it flushes to the underlying stream (networkStream).
byte[] buffer = new byte[2048];
while(in.read(buffer) == 2048) { // == 2048 should be > 0
     out.write(buffer); // and write should be `out.write(buffer, 0, length of last batch of bytes)`
} // the last batch of bytes read to the buffer will be less than 2048

举一反三

查看自己相应的代码:

private byte[] getShopLogoFromSDCard(String shopId) {
    Logger.d(TAG, "sdcard");
    byte[] b = null;
    try {
        File file = new File(Constant.APP_DB_PATH + "logo/" + shopId);
        if (file != null && file.exists()) {
            FileInputStream fis = new FileInputStream(file);
            try {
                b = new byte[fis.available()];
                fis.read(b);
                fis.close();

            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        } else {
            Logger.d(TAG, "sdcard no this image");
        }

    } catch (FileNotFoundException e) {
        e.printStackTrace();
        return null;
    }
    return b;
}

返回结果的 b 的大小是根据 fis.available 方法获取的,查看该方法的说明:

/**
 * Returns an estimate of the number of remaining bytes that can be read (or
 * skipped over) from this input stream without blocking by the next
 * invocation of a method for this input stream. Returns 0 when the file
 * position is beyond EOF. The next invocation might be the same thread
 * or another thread. A single read or skip of this many bytes will not
 * block, but may read or skip fewer bytes.
 *
 * <p> In some cases, a non-blocking read (or skip) may appear to be
 * blocked when it is merely slow, for example when reading large
 * files over slow networks.
 *
 * @return     an estimate of the number of remaining bytes that can be read
 *             (or skipped over) from this input stream without blocking.
 * @exception  IOException  if this file input stream has been closed by calling
 *             {@code close} or an I/O error occurs.
 */
public int available() throws IOException {
    if (closed) {
        throw new IOException("Stream Closed");
    }

    return available0();
}

该方法返回的是一个预估的大小,结果不保证正确。而且正好我们的问题就是概率性的,只有低概率的情况下会发生。那么就很可能是该方法导致的结果。

验证

开启两个线程,在每个线程中调用1000次该方法,在日志中打印出 fis.available 的结果。
测试结果真的就出现了 fis.available 结果不一致的情况,2个线程2000次调用中出现了两次。

11-16 11:32:29.180 18637 18657 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.180 18637 18649 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.202 18637 18654 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.203 18637 18657 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.227 18637 18657 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.227 18637 18654 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.245 18637 18657 D CacheManageApiImpl: fis.available: 8082
11-16 11:32:29.246 18637 18650 D CacheManageApiImpl: fis.available: 8082
11-16 11:32:29.272 18637 28268 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.313 18637 18657 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.315 18637 18654 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.338 18637 28268 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.347 18637 18654 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.358 18637 28268 D CacheManageApiImpl: fis.available: 8186
11-16 11:32:29.372 18637 18654 D CacheManageApiImpl: fis.available: 8186

修正

该方法中只需要正确处理将文件内容转换成 byte[] 即可。
该功能可以参考 File to byte[] in Java
既可以使用 Apache 提供的标准代码,在 Java 1.7 之后 JDK 也提供了库函数供使用。

最终代码如下:

private byte[] getShopLogoFromSDCard(String shopId) {
    Logger.d(TAG, "sdcard");
    byte[] b = null;

    File file = new File(Constant.APP_DB_PATH + "logo/" + shopId);
    if (file.exists()) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            b = IOUtils.toByteArray(fis);
        } catch (IOException e) {
            e.printStackTrace();
            Logger.d(TAG, "exception");
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    } else {
        Logger.d(TAG, "sdcard no this image");
    }

    return b;
}
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * https://commons.apache.org/proper/commons-io/javadocs/api-2.5/src-html/org/apache/commons/io/IOUtils.html
 */

public class IOUtils {

    private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
    public static final int EOF = -1;

    public static byte[] toByteArray(final InputStream input) throws IOException {
        final ByteArrayOutputStream output = new ByteArrayOutputStream();
        copy(input, output);
        return output.toByteArray();
    }

    public static int copy(final InputStream input, final OutputStream output) throws IOException {
        final long count = copyLarge(input, output);
        if (count > Integer.MAX_VALUE) {
            return -1;
        }
        return (int) count;
    }

    public static long copy(final InputStream input, final OutputStream output, final int bufferSize)
            throws IOException {
        return copyLarge(input, output, new byte[bufferSize]);
    }

    public static long copyLarge(final InputStream input, final OutputStream output)
            throws IOException {
        return copy(input, output, DEFAULT_BUFFER_SIZE);
    }

    public static long copyLarge(final InputStream input, final OutputStream output, final byte[] buffer)
            throws IOException {
        long count = 0;
        int n;
        while (EOF != (n = input.read(buffer))) {
            output.write(buffer, 0, n);
            count += n;
        }
        return count;
    }
}

JobScheduler 测试

JobScheduler 执行时间并不确定,其在满足预先设置的条件时会由系统来调用。

执行周期

通过 setPeriodic 方法可以设置任务执行的周期,在 Android N 及以后的版本中周期需大于15分钟,小于15分钟的按15分钟执行。

查看手机中的任务

adb shell dumpsys jobscheduler

该命令可以列出系统中的各种任务,包含:

  • Settings
  • Registered XX Jobs
  • Connectivity
  • Alarms
  • Idle
  • Battery
  • AppIdle
  • Content
  • Job history
  • Pending queue

等。其中 Registered XX Jobs 列出了系统中 JobScheduler。
例如:

 JOB #u0a7/-100: 88a17c2 com.android.providers.downloads/.DownloadIdleService
    u0a7 tag=*job*/com.android.providers.downloads/.DownloadIdleService
    Source: uid=u0a7 user=0 pkg=com.android.providers.downloads
    JobInfo:
      Service: com.android.providers.downloads/.DownloadIdleService
      PERIODIC: interval=+12h0m0s0ms flex=+12h0m0s0ms
      Requires: charging=true batteryNotLow=false deviceIdle=true
      Backoff: policy=1 initial=+30s0ms
      Has early constraint
      Has late constraint
    Required constraints: CHARGING TIMING_DELAY DEADLINE IDLE
    Satisfied constraints: BATTERY_NOT_LOW TIMING_DELAY APP_NOT_IDLE DEVICE_NOT_DOZING
    Unsatisfied constraints: CHARGING DEADLINE IDLE
    Tracking: BATTERY IDLE
    Enqueue time: -4h43m43s596ms
    Run time: earliest=-4h43m43s596ms, latest=+7h16m16s404ms
    Ready: false (job=false user=true !pending=true !active=true !backingup=true comp=true)

可以查看任务设置的条件,具体信息等。

Job history:
       -25m52s057ms START: u0a85 com.yulore.framework/com.yulore.superyellowpage.service.YuloreJobService
       -25m36s869ms  STOP: u0a85 com.yulore.framework/com.yulore.superyellowpage.service.YuloreJobService
       -15m51s422ms START: u0a46 DownloadManager:com.android.providers.downloads
       -15m50s704ms  STOP: u0a46 DownloadManager:com.android.providers.downloads
        -1m17s539ms START: u0a85 com.yulore.framework/com.yulore.superyellowpage.service.YuloreJobService
        -1m15s576ms  STOP: u0a85 com.yulore.framework/com.yulore.superyellowpage.service.YuloreJobService

Job History 可以查看 JobScheduler 的任务调度情况。

强制执行

adb shell cmd jobscheduler run -f package_name jobId

参考链接

What tools are available to test JobScheduler?

在运行时请求权限

背景资料

https://developer.android.com/training/permissions/requesting.html?hl=zh-cn

从 Android 6.0(API 级别 23)开始,用户开始在应用运行时向其授予权限,而不是在应用安装时授予。此方法可以简化应用安装过程,因为用户在安装或更新应用时不需要授予权限。它还让用户可以对应用的功能进行更多控制;例如,用户可以选择为相机应用提供相机访问权限,而不提供设备位置的访问权限。用户可以随时进入应用的“Settings”屏幕调用权限。

流程

下面是我根据自己的理解画的流程图。

在运行时请求权限流程图

详细的步骤具体说明在安卓开发者网站上 https://developer.android.com/training/permissions/requesting.html?hl=zh-cn#perm-request 已经有详细的中文说明。

PermissionsDispatcher

PermissionsDispatcher 是一个用来简化系统运行时权限逻辑的库。GitHub 地址是 https://github.com/hotchemi/PermissionsDispatcher

下面简单介绍下用法。

安装

在 build.gradle 中添加

dependencies {
  compile("com.github.hotchemi:permissionsdispatcher:${latest.version}") {
      // if you don't use android.app.Fragment you can exclude support for them
      exclude module: "support-v13"
  }
  annotationProcessor "com.github.hotchemi:permissionsdispatcher-processor:${latest.version}"
}

使用

PermissionsDispatcher 主要通过注解来简化运行时权限的逻辑处理。注解如下:

Annotation Required Description
@RuntimePermissions Register an Activity or Fragment(we support both) to handle permissions
@NeedsPermission Annotate a method which performs the action that requires one or more permissions
@OnShowRationale Annotate a method which explains why the permission/s is/are needed. It passes in a PermissionRequest object which can be used to continue or abort the current permission request upon user input
@OnPermissionDenied Annotate a method which is invoked if the user doesn’t grant the permissions
@OnNeverAskAgain Annotate a method which is invoked if the user chose to have the device “never ask again” about a permission

对应流程图的结果如下,使用的时候把相应逻辑处的代码声明为带注解的函数即可:

PermissionsDispatcher

shouldShowRequestPermissionRationale 的两次使用

这个方法用了两次,分别在请求权限之前和被拒绝授予权限之后。

  1. 请求权限之前
    为了判断是否需要向用户展示申请权限的理由。
    如果该值为 false,则是第一次向用户申请权限,没必要加多余的解释;
    而如果该值为 true,则表示用户曾经拒绝过授予,有必要向用户做一些相应的解释。
  2. 被拒绝授予权限之后
    为了判断是否需要手动引导用户到设置里授予权限。
    如果该值为 true,说明以后还有机会向用户申请,现在可以运行其他的功能;
    而如果该值为 false,则说明用户勾选了不再询问的复选框,你已经没机会向用户申请该权限了,你需要引导用户到设置里手动打开。

EasyPermissions

谷歌出品的权限库,也可以供参考。GitHub 地址是:https://github.com/googlesamples/easypermissions。

Android 双卡手机 sim 卡状态获取

总结一下 Android 双卡手机对于 sim 卡状态的获取方法。

判断是否插入了双卡

该方法判断是否两张 sim 卡都可用:

public static boolean isMultiSim(Context context) {
    int simCount = 0;
    SparseIntArray mActiveSubIds = getActiveSubscriptionIds(context);
    if (mActiveSubIds != null) {
        simCount = mActiveSubIds.size();
    }
    Log.d(TAG, "isMultiSim: " + (simCount > 1));
    return simCount > 1;
}

获取用户设置的主卡

该方法获取用户设置的主卡,分为三种情况:

  • 双卡
    一张主卡,一张副卡
  • 单卡
    一张主卡
  • 没有卡
    无主卡
private static final String TAG = "SimUtil";
public static final int MASTER_CARD_TYPE_FIRST = 1; // 卡1是主卡
public static final int MASTER_CARD_TYPE_SECOND = 2; // 卡2是主卡
public static final int MASTER_CARD_TYPE_NONE = 0; // 无卡
private static int MASTER_CARD = -1; // 主卡

public static int getMasterSimId(Context context) {
    // 获取主卡的 SubId
    int mDefaultSubId = -1;
    PhoneAccountHandle defAccount = null;
    TelecomManager telecomManager =
            (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
    try {
        Method m =                telecomManager.getClass().getDeclaredMethod("getUserSelectedOutgoingPhoneAccount");
        defAccount = (PhoneAccountHandle) m.invoke(telecomManager);
    } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
        Log.e(TAG, "getDeclaredMethod");
        e.printStackTrace();
    }
    Log.d(TAG, "defAccount: " + defAccount);
    if (defAccount != null) {
        try {
            Log.d(TAG, "defAccount.getId(): " + defAccount.getId());
            mDefaultSubId = Integer.parseInt(defAccount.getId());
        } catch (Exception e) {
            Log.e(TAG, "getSlotIndexBySubId");
            e.printStackTrace();
        }
    }

    Log.d(TAG, "mDefaultSubId: " + mDefaultSubId);

    int simCount = 0;
    int defaultSlotIndex = -1;

    SparseIntArray mActiveSubIds = getActiveSubscriptionIds(context);
    if (mActiveSubIds != null) {
        simCount = mActiveSubIds.size();
        // 获取主卡的 SlotId
        defaultSlotIndex = getSlotIndexBySubId(mDefaultSubId, context);
    } else {
        Log.d(TAG, "mActiveSubIds == null");
    }

    Log.d(TAG, "defaultSlotIndex: " + defaultSlotIndex);

    // 双卡,使用 defaultSlotId 来判断谁是主卡
    if (simCount == 2) {
        if (defaultSlotIndex == 1) {
            MASTER_CARD = MASTER_CARD_TYPE_SECOND;
        } else {
            MASTER_CARD = MASTER_CARD_TYPE_FIRST;
        }
    } else if (simCount == 1) { // 单卡,哪个卡能用,哪个就是主卡
        if (defaultSlotIndex == -1 && mActiveSubIds.size() > 0) {
            int size = mActiveSubIds.size();
            for (int i = 0; i < size; i++) {
                defaultSlotIndex = mActiveSubIds.valueAt(i);
            }
        }
        if (defaultSlotIndex == 0) {
            MASTER_CARD = MASTER_CARD_TYPE_FIRST;
        } else {
            MASTER_CARD = MASTER_CARD_TYPE_SECOND;
        }
    } else if (simCount == 0) { // 无卡,没有主卡
        MASTER_CARD = MASTER_CARD_TYPE_NONE;
    }

    Log.d(TAG, "simCount: " + simCount + " getMasterSimId = " + defaultSlotIndex + " MASTER_CARD: " + MASTER_CARD);
    return MASTER_CARD;
}

// 通过对应关系用 SubId 获取 SlotId
private static int getSlotIndexBySubId(int subId, Context context) {
    SparseIntArray map = getActiveSubscriptionIds(context);
    return getSlotIndexBySubId(map, subId);
}

private static int getSlotIndexBySubId(SparseIntArray map, int subId) {
    if (map != null) {
        return map.get(subId, -1);
    }
    return -1;
}

// 获取 sim 卡的 SubId 和 SlotId 的对应关系
private static SparseIntArray getActiveSubscriptionIds(Context context) {
    // 获取可用的 sim 卡
    List<SubscriptionInfo> list = SubscriptionManager.from(context).getActiveSubscriptionInfoList();
    return convertSubscriptionIds(list);
}

private static SparseIntArray convertSubscriptionIds(List<SubscriptionInfo> list) {
    SparseIntArray result = new SparseIntArray();
    if (list != null) {
        for (SubscriptionInfo item : list) {
            if (item == null) {
                continue;
            }

            int subId = item.getSubscriptionId();
            int slotIndex = item.getSimSlotIndex();
            result.put(subId, slotIndex);
        }
    }
    return result.size() > 0 ? result : null;
}

SubId 和 SlotId

说一下我对这两个值的理解:

  • SubId 是 SubscriptionInfo 这个类的 id 值,Android 记录的 sim 卡信息都保存为 SubscriptionInfo 对象,存在数据库里,每一张 sim 卡对应一个 SubId。系统操作 sim 卡状态,都以 SubId 为准。
  • SlotId 是手机卡槽的 id,更贴近人的理解,卡槽1的 SlotId 是0,卡槽2的 SlotId 是1。我们一般展示给用户的卡1和卡2分别代表卡槽1里的卡和卡槽2里的卡,所以需要用 SlotId 来获取 sim 卡信息。
  • SubscriptionInfo 类中同时包含 SubId 和 SlotId 两种信息,因此可以通过获取所有的 SubscriptionInfo 来建立一个 SubId 和 SlotId 的对应表。这样就可以彼此互相匹配了。

getSlotId(int subId)

上面是通过对应关系来使用 subId 获取 SlotId,其实 SubscriptionManager 中也提供了该方法,只是 hide了,想要使用的话需要通过反射。

public int getSlotIdUsingSubId(int subId, Context context) {
    int result = -1;
    try {
        String subscription_manager = "android.telephony.SubscriptionManager";
        Class<?> clz = Class.forName(subscription_manager);
        Object subSm;
        Constructor<?> constructor = clz.getDeclaredConstructor(Context.class);
        subSm = constructor.newInstance(context);
        Method mth = clz.getMethod("getSlotId", int.class);
        result = (int) mth.invoke(subSm, subId);
    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | IllegalArgumentException | NoSuchMethodException | InvocationTargetException e) {
        e.printStackTrace();
    }
    Logger.d(TAG, "subId = " + subId + " result = " + result);

    return result;
}

更多

想了解更多内容,请关注我的微信公众账号: