散列搜索 & 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签名校验

Java 类成员执行顺序

  1. 如果类还没有被加载:
    1. 先执行父类的静态代码块和静态变量初始化,并且静态代码块和静态变量的执行顺序只跟代码中出现的顺序有关
    2. 执行子类的静态代码块和静态变量初始化
    3. 执行父类的实例变量初始化(例如:int a; 初始化就是0,引用类型就是null)
    4. 执行父类的构造函数
    5. 执行子类的实例变量初始化(例如:int a; 初始化就是0,引用类型就是null)
    6. 执行子类的构造函数
  2. 如果类已经被加载:
    1. 则静态代码块和静态变量就不用重复执行,再创建类对象时,只执行与实例相关的变量初始化和构造方法

参考 java 中类初始化,构造方法,静态成员变量,静态块的加载顺序

双拼

Kuai ing Liang uang Ruan Cao Zou T ue Qiu Yun Wei Jan Mian

快 迎 两 王 软 草 走, 特约 秋 云 为 见 面。

Xia ua Song iong shU chI zhVi Geng Dai Bin Niao Fen Pie Hang

夏 娃 怂 恿 书 痴 追, 更 待 滨 鸟 分 撇 航?

安卓性能分析

磁盘

工具: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. 不要使用数据库中的全文索引

参考

数据库设计那些事