月度归档:2017年11月

安卓性能分析

磁盘

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

参考

数据库设计那些事

为 Terminal 配置 ShadowSocks 代理

为 Terminal 开启代理存在下述问题:

  1. Terminal 不会自动使用系统代理,需要手动配置;
  2. ShadowSocks 默认为 PAC 代理,Terminal 无法使用。

下面介绍下如何通过 ShadowSocks 来为 Terminal 提供代理

设置 ShadowSocks 为手动模式

这里使用 ShadowSocksX-NG 客户端来为系统配置代理:

该客户端拥有 PAC、Socks、HTTP 等多种代理方式。

自动模式

通过菜单栏下拉项配置代理模式:

平时一般使用 ShadowSocks 时用的是“PAC 自动模式”,

网络设置里的代理协议是自动代理配置。

手动模式

在 ShadowSocks 的偏好设置里确认 HTTP 代理地址。

  1. 通过菜单栏下拉选项配置 ShadowSocks 为手动模式;
  2. 在系统设置的网络里取消自动代理配置的设置;
  3. 在网络的代理设置里选择“网页代理(HTTP)”和 “安全网页代理(HTTPS)”,并且填入服务器地址。

为 Terminal 配置 HTTP 代理

在 Terminal 配置文件中增加如下内容, all_proxy 后填入 ShadowSocks 的 HTTP 代理地址:

# proxy list
alias proxy='export all_proxy=http://127.0.0.1:1087'
alias unproxy='unset all_proxy'

重新加载配置文件:

$ source .zshrc 或 重新打开 Terminal

开启代理

$ proxy 

关闭代理

$ unproxy

End

通过手动配置 Terminal 代理和配置 ShadowSocks 为手动模式即可成功在 Terminal 中启动代理了。

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?