老司机种菜


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 公益404

  • 搜索

Android插件化(一)技术调研

发表于 2018-01-12 | 分类于 Android

前言

有关APK更新的技术比较多,例如:增量更新、插件式开发、热修复、RN、静默安装。 下面简单介绍一下:

更新方式 签名
增量更新 旧版本Apk(v1.0)和新(v2.0)、旧版本Apk(v1.0)生成的差分包(apk.patch 质量小)合并成为新版本Apk(v2.0)安装。
插件式开发 给宿主APK提供插件,扩展(需要的时候再下载),可以动态地替换。主要技术是动态代理的知识。
热修复 通过NDK底层去修复,也是C/C++的技术。
RN 通过JS脚本去修复APK。
静默安装 需要root权限,适配不同手机ROM很麻烦。

插件化、热修复(思想)的发展历程

  • 2012年7月,AndroidDynamicLoader,大众点评,陶毅敏:思想是通过Fragment以及schema的方式实现的,这是一种可行的技术方案,但是还有限制太多,这意味这你的activity必须通过Fragment去实现,这在activity跳转和灵活性上有一定的不便,在实际的使用中会有一些很奇怪的bug不好解决,总之,这还是一种不是特别完备的动态加载技术。
  • 2013年,23Code,自定义控件的动态下载:主要利用 Java ClassLoader 的原理,可动态加载的内容包括 apk、dex、jar等。
  • 2014年初,Altas,阿里伯奎的技术分享:提出了插件化的思想以及一些思考的问题,相关资料比较少。
  • 2014年底,Dynamic-load-apk,任玉刚:动态加载APK,通过Activity代理的方式给插件Activity添加生命周期。
  • 2015年4月,OpenAltas/ACCD:Altas的开源项目,一款强大的Android非代理动态部署框架,目前已经处于稳定状态。
  • 2015年8月,DroidPlugin,360的张勇:DroidPlugin 是360手机助手在 Android 系统上实现了一种新的插件机制:通过Hook思想来实现,它可以在无需安装、修改的情况下运行APK文件,此机制对改进大型APP的架构,实现多团队协作开发具有一定的好处。
  • 2015年9月,AndFix,阿里:通过NDK的Hook来实现热修复。
  • 2015年11月,Nuwa,大众点评:通过dex分包方案实现热修复。
  • 2015年底,Small,林光亮:打通了宿主与插件之间的资源与代码共享。
  • 2016年4月,ZeusPlugin,掌阅:ZeusPlugin最大特点是:简单易懂,核心类只有6个,类总数只有13个。

1.增量更新

增量更新就是原有app的基础上只更新发生变化的地方,其余保持原样。 与原来每次更新都要下载完整apk包的做法相比,这样做的好处显而易见:每次变化的地方总是比较少,因此更新包的体积就会小很多。

1.1增量更新的流程

  1. APP检测最新版本:把当前版本告诉服务端,服务端进行判断。 如果有新版本,服务端需要对当前版本的APK与最新版本的APK进行一次差分,产生patch差分文件。(或者新版本的APK上传到服务端的时候就已经差分好了)
  2. APP在后台下载差分文件,进行文件的MD5校验,在本地进行合并(跟本地的data目录下面的APK文件合并),合并出最新的APK之后,提示用户安装。
  3. 增量更新的最终目的:省流量地更新宿主APK。

差分的处理比较麻烦的地方就是要针对不同的应用市场渠道和众多不同版本进行差分。 注意:新版本有可能比旧版本小,差分只是把变化的部分记录下来。

1.2服务器端行为(后台工程师操作)

1.2.1下载拆分和合并要用的第三方库(bsdiff、bzip2)

我们使用到的第三方库是:Binary diff,简称bsdiff,这个库专门用来实现文件的差分和合并的,它的官网如下:http://www.daemonology.net/bsdiff/

1.2.2Java代码调用:

创建Web项目,用来做APP的服务端。创建工具类专门用于产生差分包:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BsDiff {
/**
* 差分
* @param oldfile
* @param newfile
* @param patchfile
*/
public native static void diff(String oldfile,String newfile,String patchfile);

static {
System.loadLibrary("bsdiff");
}
}

其中JNI的实现如下(该实现写在bsdiff.cpp中):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
JNIEXPORT void JNICALL Java_com_haocai_bsdiff_BsDiff_diff
(JNIEnv *env, jclass jcls, jstring oldfile_jstr, jstring newfile_jstr, jstring patchfile_jstr) {
int argc = 4;
char* oldfile = (char*)env->GetStringUTFChars(oldfile_jstr, NULL);
char* newfile = (char*)env->GetStringUTFChars(newfile_jstr, NULL);
char* patchfile = (char*)env->GetStringUTFChars(patchfile_jstr, NULL);

//参数(第一个参数无效)
char *argv[4];
argv[0] = { "bsdiff" };
argv[1] = oldfile;
argv[2] = newfile;
argv[3] = patchfile;

bsdiff_main(argc, argv);

env->ReleaseStringUTFChars(oldfile_jstr, oldfile);
env->ReleaseStringUTFChars(newfile_jstr, newfile);
env->ReleaseStringUTFChars(patchfile_jstr, patchfile);
};

通过研究bsdiff的源码,我们发现bsdiff.cpp里面的main函数就是入口函数,避免歧义把函数名main改为bsdiff_main,然后通过JNI去调用。根据bsdiff.cpp中bsdiff_main函数方法中有以下关键语句

1
if (argc != 4) errx(1, "usage: %s oldfile newfile patchfile\n", argv[0]);

根据提示需要传入4个参数:

1
2
3
4
argv[0] = "bsdiff";//这个参数没用
argv[1] = oldPath;//旧APK文件路径
argv[2] = newPath;/新APK文件路径
argv[3] = patchPath;//APK差分文件路径

然后我们准备两个APK文件,不同版本的,最好Java代码、资源都不一样。

写一个Java测试类生成差分包:

1
2
3
4
5
6
7
8
9
10
11
package com.haocai.bsdiff;

public class ConstantsWin {

//路径不能包含中文
public static final String OLD_APK_PATH = "D:/android_apks/test_old.apk";

public static final String NEW_APK_PATH = "D:/android_apks/test_new.apk";

public static final String PATCH_PATH = "D:/android_apks/apk.patch";
}
1
2
3
4
5
6
7
8
9
10
11
package com.haocai.bsdiff;

/**
* Created by Administrator on 2017/11/14.
*/
public class BsDiffTest {
public static void main(String[] args){
//得到差分包
BsDiff.diff(ConstantsWin.OLD_APK_PATH,ConstantsWin.NEW_APK_PATH,ConstantsWin.PATCH_PATH);
}
}

注意:

  • test_new.apk、test_old.apk 要先放在目标目录
  • bsdiff.cpp中生成差分包的程序方法是异步的,所以生成完整的apk.patch可能要等一下。apk.patch体积大小停止增长,表示生成结束。
    1.2.3简单搭建后台JavaWeb供Android前端下载apk.patch差分包

1.3Android客户端行为

1.3.1编译合并要用的第三方库(bsdiff、bzip2)

对应的Java代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.haocai.app.update;

/**
* Created by Xionghu on 2017/11/14.
* Desc:
*/

public class BsPatch {
/**
* 合并
* @param oldfile
* @param newfile
* @param patchfile
*/
public native static void patch(String oldfile,String newfile,String patchfile);

static {
System.loadLibrary("bspatch");
}
}

在Android端,我们需要把bzip2以及bsdiff的文件拷贝到jni目录里面,同样的,我们只需要编译一个bspatch.c源文件即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//合并
JNIEXPORT void JNICALL Java_com_haocai_app_update_BsPatch_patch
(JNIEnv *env, jclass jcls, jstring oldfile_jstr, jstring newfile_jstr, jstring patchfile_jstr){
int argc = 4;
char* oldfile = (char*)(*env)->GetStringUTFChars(env,oldfile_jstr, NULL);
char* newfile = (char*)(*env)->GetStringUTFChars(env,newfile_jstr, NULL);
char* patchfile = (char*)(*env)->GetStringUTFChars(env,patchfile_jstr, NULL);

//参数(第一个参数无效)
char *argv[4];
argv[0] = "bspatch";
argv[1] = oldfile;
argv[2] = newfile;
argv[3] = patchfile;

bspatch_main(argc,argv);

(*env)->ReleaseStringUTFChars(env,oldfile_jstr, oldfile);
(*env)->ReleaseStringUTFChars(env,newfile_jstr, newfile);
(*env)->ReleaseStringUTFChars(env,patchfile_jstr, patchfile);

}

代码v1.0差分包合并核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package com.haocai.app.update;

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.format.Formatter;
import android.widget.Toast;
import com.lzy.okgo.OkGo;
import com.lzy.okgo.callback.FileCallback;
import com.lzy.okgo.model.Progress;
import com.lzy.okgo.model.Response;
import com.lzy.okgo.request.base.Request;
import java.io.File;
import java.text.NumberFormat;

public class MainActivity extends AppCompatActivity {

private static final int REQUEST_PERMISSION_STORAGE = 0x01;
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 0:
Toast.makeText(MainActivity.this, "您正在进行省流量更新", Toast.LENGTH_SHORT).show();
ApkUtils.installApk(MainActivity.this, Constants.NEW_APK_PATH);
break;
}
}
};
private NumberFormat numberFormat;


@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle("简单文件下载");

numberFormat = NumberFormat.getPercentInstance();
numberFormat.setMinimumFractionDigits(2);

checkSDCardPermission();

/**
* 因为后台没有写版本判断语句
* 在高版本下暂时先注释fileDownload(); 否则一直下载安装
*
* 低版本下运行fileDownload();
*/
fileDownload();


}


/**
* 检查SD卡权限
*/
protected void checkSDCardPermission() {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_PERMISSION_STORAGE);
}
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_PERMISSION_STORAGE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//获取权限
fileDownload();
} else {
Toast.makeText(getApplicationContext(), "权限被禁止,无法下载文件!", Toast.LENGTH_SHORT).show();
}
}
}

@Override
protected void onDestroy() {
super.onDestroy();
//Activity销毁时,取消网络请求
OkGo.getInstance().cancelTag(this);
}


public void fileDownload() {

OkGo.<File>get(Constants.URL_PATCH_DOWNLOAD)//
.tag(this)//
.execute(new FileCallback(Constants.SD_CARD, Constants.PATCH_FILE) {

@Override
public void onStart(Request<File, ? extends Request> request) {
}

@Override
public void onSuccess(Response<File> response) {

new Thread(new Runnable() {
@Override
public void run() {

try {
// File patchFile = new File(Constants.SD_CARD, Constants.PATCH_FILE);
String oldfile = ApkUtils.getSourceApkPath(MainActivity.this, getPackageName());
String newfile = Constants.NEW_APK_PATH;
String patchfile = Constants.SD_CARD + File.separator + Constants.PATCH_FILE;
BsPatch.patch(oldfile, newfile, patchfile);

mHandler.sendEmptyMessage(0);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();


}

@Override
public void onError(Response<File> response) {

}

@Override
public void downloadProgress(Progress progress) {
System.out.println(progress);

String downloadLength = Formatter.formatFileSize(getApplicationContext(), progress.currentSize);
String totalLength = Formatter.formatFileSize(getApplicationContext(), progress.totalSize);
String speed = Formatter.formatFileSize(getApplicationContext(), progress.speed);
System.out.println(downloadLength);
}
});
}

}

注意:这里7.0可能会有问题,把路径暴露给别的app,需要FileProvider去实现(不难,这个留给大家去做吧)。

源码下载 作者: (简书)香沙小熊

2.插件化

插件化框架的一些对比,下面引用 https://github.com/wequick/Small/blob/master/Android/COMPARISION.md

特性 DynamicLoadApk DynamicAPK Small DroidPlugin VirtualAPK RePlugin
支持四大组件 只支持Activity 只支持Activity 只支持Activity 全支持 全支持 全支持
组件无需在宿主manifest中预注册 √ × √ √ √ √
插件可以依赖宿主 √ √ √ × √ √
支持PendingIntent × × × √ √ √
Android特性支持 大部分 大部分 大部分 几乎全部 几乎全部 几乎全部
兼容性适配 一般 一般 中等 高 高 高
插件构建 无 部署aapt Gradle插件 无 Gradle插件 Gradle插件
源码 https://github.com/singwhatiwanna/dynamic-load-apk https://github.com/CtripMobile/DynamicAPK https://github.com/wequick/Small https://github.com/DroidPluginTeam/DroidPlugin https://github.com/didi/VirtualAPK https://github.com/Qihoo360/RePlugin
开发者 singwhatiwanna CtripMobile Lody 滴滴 360

2.1DynamicLoadApk

基于静态代理的实现

2.2VirtualAPK

2.2.1特性
Feature Detail
Supported components Activity, Service, Receiver and Provider
Manually register components in AndroidManifest.xml No need
Access host app classes and resources Supported
PendingIntent Supported
Supported Android features Almost all features
Compatibility Almost all devices
Building system Gradle plugin
Supported Android versions API Level 15+
##### 2.2.2架构
image
2.2.3原理
2.2.3.1基本原理
  • 合并宿主和插件的ClassLoader 需要注意的是,插件中的类不可以和宿主重复
  • 合并插件和宿主的资源 重设插件资源的packageId,将插件资源和宿主资源合并
  • 去除插件包对宿主的引用 构建时通过Gradle插件去除插件对宿主的代码以及资源的引用
    2.2.3.2四大组件的实现原理
  • Activity 采用宿主manifest中占坑的方式来绕过系统校验,然后再加载真正的activity;
  • Service 动态代理AMS,拦截service相关的请求,将其中转给Service Runtime去处理,Service Runtime会接管系统的所有操作;
  • Receiver 将插件中静态注册的receiver重新注册一遍;
  • ContentProvider 动态代理IContentProvider,拦截provider相关的请求,将其中转给Provider Runtime去处理,Provider Runtime会接管系统的所有操作。

2.3RePlugin

2.3.1特性
特性 描述
组件 四大组件(含静态Receiver)
升级无需改主程序Manifest 完美支持
Android特性 支持近乎所有(包括SO库等)
TaskAffinity & 多进程 支持(坑位方案)
插件类型 支持自带插件(自识别)、外置插件
插件间耦合 支持Binder、Class Loader、资源等
进程间通讯 支持同步、异步、Binder、广播等
自定义Theme & AppComat 支持
DataBinding 支持
安全校验 支持
资源方案 独立资源 + Context传递(相对稳定)
Android 版本 API Level 9+ (2.3及以上)
2.3.2架构

image

模块化,组件化,插件化

在技术开发领域,模块化是指分拆代码,即当我们的代码特别臃肿的时候,用模块化将代码分而治之、解耦分层。具体到 android 领域,模块化的具体实施方法分为插件化和组件化。

一套完整的插件化或组件化都必须能够实现单独调试、集成编译、数据传输、UI 跳转、生命周期和代码边界这六大功能。

解耦思想: 控制反转是一种思想,依赖注入是一种设计模式,IoC框架使用依赖注入作为控制反转的方式

模块化粒度更小,更侧重于重用,而组件化粒度稍大于模块,更侧重于业务解耦。 组件化的核心是角色的转换。 在打包时, 是library; 在调试时, 是application。 组件化开发是纵向分层,模块化开发是横向分块。

组件化想要解决的问题:

  1. 实际业务变化非常快,但是工程之前的业务模块耦合度太高,牵一发而动全身.
  2. 对工程所做的任何修改都必须要编译整个工程
  3. 功能测试和系统测试每次都要进行.
  4. 团队协同开发存在较多的冲突.不得不花费更多的时间去沟通和协调,并且在开发过程中,任何一位成员没办法专注于自己的功能点,影响开发效率.
  5. 不能灵活的对工程进行配置和组装.比如今天产品经理说加上这个功能,明天又说去掉,后天在加上.

组件开发比较常见的问题是业务组件的相互引用,为此我们可以通过路由/总线的方式去处理,挂载到组件总线上的业务组件,都可以实现双向通信.而通信协议和HTTP通信协议类似,即基于URL的方式进行.

相对于组件化开发主要要解决的问题:

  1. 宿主和插件分开编译
  2. 并发开发
  3. 动态更新插件
  4. 按需下载模块
  5. 方法数或变量数爆棚

插件化组件化的区别:

  1. 组件化的单位是组件(module);插件化的单位是apk(一个完整的应用)。
  2. 组件化实现的是解耦与加快编译, 隔离不需要关注的部分;插件化实现的也是解耦与加快编译,同时实现热插拔也就是热更新。
  3. 组件化的灵活性在于按加载时机切换,分离出独立的业务组件,比如微信的朋友圈;插件化的灵活性在于是加载apk, 完全可以动态下载,动态更新,比组件化更灵活。
  4. 组件化能做的只是, 朋友圈已经有了,我想单独调试,维护,和别人不耦合,但是和整个项目还是有关联的;插件化可以说朋友圈就是一个app, 我需要整合了,把它整合进微信这个大的app里面

其实从框架名称就可以看出: 组 和 插。 组本来就是一个系统,你把微信分为朋友圈,聊天, 通讯录按意义上划为独立模块,但并不是真正意义上的独立模块。 插本来就是不同的apk, 你把微信的朋友圈,聊天,通讯录单独做一个完全独立的app, 需要微信的时候插在一起,就是一个大型的app了。 插件化的加载是动态的,这点很重要,也是灵活的根源。

所谓架构,无非两个方面: 分层和通信方式。 其实广义的架构也可以说是这两个方面:子模块(子系统)划分和通信。

子模块划分 除了大家公认的common部分, 业务模块的划分尤为重要,相比于狭义上的架构,广义上的子系统的划分的关注点,很考验技术经验以及对业务的理解。

通信方式 模块化的通信方式,无非是相互引入;我抽取了common, 其他模块使用自然要引入这个module 组件化的通信方式,按理说可以划分为多种,主流的是隐式和路由。隐式的存在使解耦与灵活大大降低,因此路由是主流 插件化的通信方式,不同插件本身就是不同的进程了。因此通信方式偏向于Binder机制类似的进程间通信 移动端目前的架构,差异化在于通信机制。通过以上说明,通信机制主要分为3种:

  • 对象持有
  • 接口持有
  • 路由 通信方式中,对象持有是比较原始的,解耦率最低,建议放弃; 接口持有是个不错的选择,极大程度上实现解耦的诉求,但是解耦不彻底,相互持有交互方的接口。 路由机制也是个不错的选择,可以实现完全解耦,就像组件化一样。但是路由机制的设计是个技术难点,怎么设计效率最高?更健壮?代码可查阅性更好?这些都是值得思考的问题。对于路由机制的优化,阿里的ARouter(用于组件通信)中,采用了分组的模式,我们可以采用;其次可以根据AnnotationProcessor的处理,为每一个注册接收器的组件实现一个SupportActions来确保消息只发送给注册了指定类型的模块,也是个不错的选择。

(转)聊一聊机器学习的MLE和MAP:最大似然估计和最大后验估计

发表于 2018-01-08 | 分类于 ml

TLDR (or the take away)

  • 概率学派 - Frequentist - Maximum Likelihood Estimation(MLE,最大似然估计)
  • 贝叶斯学派 - Baysesian - Maximum A Posteriori(MAP, 最大后验估计)

概述

有时候和别人聊天,对方会说自己有很多机器学习经验,深入一聊发现,对方竟然对MLE和MAP一知半解,至少在我看来,这位同学的机器学习基础并不扎实。难道在这个深度学习盛行的年代,不少同学都只注重调参数?

现代机器学习的终极问题都会转化为解目标函数的优化问题,MLE和MAP是生成这个函数的很基本的思想,因此我们对二者的认知是非常重要的。这次就和大家认真聊一聊MLE和MAP这两种estimator。

两大学派的争论

抽象一点来讲,频率学派和贝叶斯学派对世界的认知有本质不同:频率学派认为世界是确定的,有一个本体,这个本体的真值是不变的,我们的目标就是要找到这个真值或真值所在的范围;而贝叶斯学派认为世界是不确定的,人们对世界先有一个预判,而后通过观测数据对这个预判做调整,我们的目标是要找到最优的描述这个世界的概率分布。

在对事物建模时,用 θ 表示模型的参数,请注意,解决问题的本质就是求θ 。那么: (1) 频率学派: 存在唯一真值 θ 。举一个简单直观的例子–抛硬币,我们用 P(head) 来表示硬币的bias。抛一枚硬币100次,有20次正面朝上,要估计抛硬币正面朝上的bias P(head)=θ。在频率学派来看,θ = 20 / 100 = 0.2,很直观。当数据量趋于无穷时,这种方法能给出精准的估计;然而缺乏数据时则可能产生严重的偏差。例如,对于一枚均匀硬币,即 θ = 0.5,抛掷5次,出现5次正面 (这种情况出现的概率是1/2^5=3.125%),频率学派会直接估计这枚硬币 θ = 1,出现严重错误。

(2) 贝叶斯学派: θ 是一个随机变量,符合一定的概率分布。在贝叶斯学派里有两大输入和一大输出,输入是先验 (prior)和似然 (likelihood),输出是后验 (posterior)。先验,即 P(θ) ,指的是在没有观测到任何数据时对 θ 的预先判断,例如给我一个硬币,一种可行的先验是认为这个硬币有很大的概率是均匀的,有较小的概率是是不均匀的;似然,即 P(X|θ) ,是假设 θ 已知后我们观察到的数据应该是什么样子的;后验,即 P(θ|X) ,是最终的参数分布。贝叶斯估计的基础是贝叶斯公式,如下:

$P(\theta|X)=\frac{P(X|\theta) \times P(\theta)}{P(X)}$

同样是抛硬币的例子,对一枚均匀硬币抛5次得到5次正面,那么 P(head) ,即 P(θ|X) ,是一个distribution,最大值会介于0.5~1之间,而不是武断的 θ = 1。

这里有两点值得注意的地方:

随着数据量的增加,参数分布会越来越向数据靠拢,先验的影响力会越来越小 如果先验是uniform distribution,则贝叶斯方法等价于频率方法。因为直观上来讲,先验是uniform distribution本质上表示对事物没有任何预判

MLE - 最大似然估计

Maximum Likelihood Estimation, MLE是频率学派常用的估计方法!

假设数据 x_1, x_2, …, x_n 是i.i.d.的一组抽样,X = (x_1, x_2, …, x_n) 。其中i.i.d.表示Independent and identical distribution,独立同分布。那么MLE对 $\theta$ 的估计方法可以如下推导:

Maximum Likelihood Estimation, MLE是频率学派常用的估计方法!

假设数据 x_1, x_2, …, x_n 是i.i.d.的一组抽样,X = (x_1, x_2, …, x_n) 。其中i.i.d.表示Independent and identical distribution,独立同分布。那么MLE对 $\theta$ 的估计方法可以如下推导:

最后这一行所优化的函数被称为Negative Log Likelihood (NLL),这个概念和上面的推导是非常重要的!

我们经常在不经意间使用MLE,例如

  • 上文中关于频率学派求硬币概率的例子,其方法其实本质是由优化NLL得出。本文末尾附录中给出了具体的原因 :-)
  • 给定一些数据,求对应的高斯分布时,我们经常会算这些数据点的均值和方差然后带入到高斯分布的公式,其理论依据是优化NLL
  • 深度学习做分类任务时所用的cross entropy loss,其本质也是MLE

MAP - 最大后验估计

Maximum A Posteriori, MAP是贝叶斯学派常用的估计方法!

同样的,假设数据 x_1, x_2, …, x_n 是i.i.d.的一组抽样,X = (x_1, x_2, …, x_n) 。那么MLE对 $\theta$ 的估计方法可以如下推导: 其中,第二行到第三行使用了贝叶斯定理,第三行到第四行P(X) 可以丢掉因为与 $\theta$ 无关。注意 $-\log P(X|\theta )$ 其实就是NLL,所以MLE和MAP在优化时的不同就是在于先验项 - $\log P(\theta) $。好的,那现在我们来研究一下这个先验项,假定先验是一个高斯分布,即

$P(\theta) = \text{constant} \times e^{-\frac{\theta^2}{2\sigma^2}}$

那么, $-\log P(\theta) = \text{constant} + \frac{\theta^2}{2\sigma^2} $。至此,一件神奇的事情发生了 – 在MAP中使用一个高斯分布的先验等价于在MLE中采用L2的regularizaton!

再稍微补充几点:

  • 我们不少同学大学里学习概率论时,最主要的还是频率学派的思想,其实贝叶斯学派思想也非常流行,而且实战性很强
  • CMU的很多老师都喜欢用贝叶斯思想解决问题;我本科时的导师朱军老师也在做贝叶斯深度学习的工作,有兴趣可以关注一下。

后记

有的同学说:“了解这些没用,现在大家都不用了。”这种想法是不对的,因为这是大家常年在用的知识,是推导优化函数的核心,而优化函数又是机器学习 (包含深度学习) 的核心之一。这位同学有这样的看法,说明对机器学习的本质并没有足够的认识,而让我吃惊的是,竟然有不少其他同学为这种看法点赞。内心感到有点儿悲凉,也引发了我写这篇文章的动力,希望能帮到一些朋友 :-)

参考资料

  • [1] Bayesian Method Lecture, UT Dallas.

  • [2] MLE, MAP, Bayes classification Lecture, CMU.

附录

为什么说频率学派求硬币概率的算法本质是在优化NLL?

因为抛硬币可以表示为参数为 $\theta$ 的Bernoulli分布,即

$P(x_i; \theta) =\left{ \begin{array}{ll} \theta & x_i = 1 \ 1 - \theta & x_i = 0 \ \end{array} \right. \ = \theta^{x_i} (1- \theta)^{1-x_i}$

其中 x_i = 1 表示第 i 次抛出正面。那么,

$\text{NLL} = -\sum_{i=1}^n \log P(x_i; \theta) = -\sum_{i=1}^n \log \theta^{x_i} (1- \theta)^{1-x_i}$

求导数并使其等于零,得到

$\text{NLL}’ = -\sum_{i=1}^n\Big(\frac{x_i}{\theta} + (1-x_i)\frac{-1}{1-\theta}\Big) = 0$

即 $\hat{\theta} = \frac{\sum_{i=1}^n x_i}{n}$ ,也就是出现正面的次数除以总共的抛掷次数。

转自聊一聊机器学习的MLE和MAP:最大似然估计和最大后验估计

机器学习资源

发表于 2017-12-29 | 分类于 ml

deep-voice-conversion:Deep neural networks for voice conversion (voice style transfer) in Tensorflow

Prisma

Prisma shifts focus to b2b with an API for AI-powered mobile effects

Prisma 团队推出基于人工智能的贴纸制作应用 Sticky

Prisma launches a social feed to see if style can transfer into a platform

Manipulate audio with a simple and easy high level interface

FLV格式解析

发表于 2017-12-20 | 分类于 音视频封装

简介

FLV(Flash Video)是现在非常流行的流媒体格式,由于其视频文件体积轻巧、封装播放简单等特点,使其很适合在网络上进行应用,目前主流的视频网站无一例外地使用了FLV格式。另外由于当前浏览器与Flash Player紧密的结合,使得网页播放FLV视频轻而易举,也是FLV流行的原因之一。

FLV是流媒体封装格式,我们可以将其数据看为二进制字节流。总体上看,FLV包括文件头(File Header)和文件体(File Body)两部分,其中文件体由一系列的Tag及Tag Size对组成。 flv-struct

FLV格式解析

先来一张图,这是《科比退役演讲》下载)的一个FLV视频。我使用的是UltraEdit的二进制查看工具。 flv-head

header

头部分由一下几部分组成 Signature(3 Byte)+Version(1 Byte)+Flags(1 Bypte)+DataOffset(4 Byte)

  • signature 占3个字节 固定FLV三个字符作为标示。一般发现前三个字符为FLV时就认为他是flv文件。图中0x46 0x4C 0x56,代表FLV
  • Version 占1个字节 标示FLV的版本号。这里我们看到是1
  • Flags 占1个字节 内容标示。第0位和第2位,分别表示 video 与 audio 存在的情况.(1表示存在,0表示不存在)。截图看到是0x05,也就是00000101,代表既有视频,也有音频。
  • DataOffset 4个字节 表示FLV的header长度。这里可以看到固定是9

Body

FLV的body部分是一系列的back-pointers+tag构成的

  • back-pointers固定4个字节,表示前一个tag的size

  • tag分三种类型:video,audio,scripts.

    tag组成
    1
    tag type+tag data size+Timestamp+TimestampExtended+stream id+ tag data
  • type 1个字节。8为Audio,9为Video,18为scripts

  • tag data size 3个字节。表示tag data的长度。从streamd id 后算起。

  • Timestreamp 3个字节。时间戳

  • TimestampExtended 1个字节。时间戳扩展字段

  • stream id 3个字节。总是0

  • tag data 数据部分

图上第一个tag:

  • type=0x12=18,表示是一个scripts,FLV中,header后的第一个tag是script tag,script tag内容是amf格式数据,包含两个amf.
  • size=0x00 0x01 0x74 = 372
  • timpestreamp = 0x00 0x00 0x00
  • TimestampExtended=0x00
  • streamid=0x00 0x00 0x00
  • tag data部分: FLV-TAG
tag的划分

图中红色部分是我标出”(“与”)”前后的的两个back-pointers,都是4个字节。而括号中间就是第一个TAG。那是怎么计算的呢?我们就以这个做个示例。

  • 首先第一个back-pointers是0x00000000,那是因为后面是第一个TAG。所以他为0。
  • 然后根据我们我们前面格式获取到size是0x00 0x01 0x74 = 372。也就是说从stream id后面再加上372个字节就到了第一个TAG的末尾,我们数一下。tag header有11个字节。那么到第一个TAG,总共有372+11=383=0x17f。
  • 接下来我们找到0x17f的地址,从工具上很容易找到,正好就是后括号”)”的前面。红0x00 0x00 0x01 0x7F=372,这代表的是上一个TAG的大小。
  • 最后我们计算一下,上一个TAG数据部分是372个字节,前面type、stream id等字段占了11个字节。正好是匹配的。 上面我们已经知道了怎么取划分每个TAG。接下来我们就看TAG的具体内容:
tag的内容

前面已经提到tag分3种。我们一个个看

script

脚本Tag一般只有一个,是flv的第一个Tag,用于存放flv的信息,比如duration、audiodatarate、creator、width等。 首先介绍下脚本的数据类型。所有数据都是以数据类型+(数据长度)+数据的格式出现的,数据类型占1byte,数据长度看数据类型是否存在,后面才是数据。

一般来说,该Tag Data结构包含两个AMF包。AMF(Action Message Format)是Adobe设计的一种通用数据封装格式,在Adobe的很多产品中应用,简单来说,AMF将不同类型的数据用统一的格式来描述。第一个AMF包封装字符串类型数据,用来装入一个“onMetaData”标志,这个标志与Adobe的一些API调用有,在此不细述。第二个AMF包封装一个数组类型(srs返回为object类型),这个数组中包含了音视频信息项的名称和值。具体说明如下

值 类型 说明
0 Number type 8 Bypte Double
1 Boolean type 1 Bypte bool
2 String type 后面2个字节为长度
3 Object type
4 MovieClip type
5 Null type
6 Undefined type
7 Reference type
8 ECMA array type 数组,类似Map
10 Strict array type
11 Date type
12 Long string type 后面4个字节为长度

FLV-script-1.png 上图为第一个AMF包

  • type=0x02对应String
  • size=0A=10
  • value=onMetaData 正好是10个字节。

FLV_script-2.png 上图为第二个AMF

  • type=0x08 对应ECMA array type。

表示数组,类似Map。后面4个字节为数组的个数。然后是键值对,第一个为键,2个字节为长度。后面跟具体的内容。接着3个字节表示值的类型,然后根据类型判断长度。 上图我们可以判断,总共有13个键值对。 第一个长度为8个字节是duration。值类型是0x004073,第一个字节是00,所以是double,8个字节。 第二个长度5个字节是width。值也是double类型,8个字节。 依次解析下去…

Audio

flv-audio1 flv-audio2 flv-audio3 flv-audio4 视频中第二个tag为音频tag

stream-id之后:

  • 前4位为音频格式
值 类型
0 Linear PCM, platform endian
1 ADPCM
2 MP3
3 Linear PCM, little endian
4 Nellymoser 16-kHz mono
5 Nellymoser 8-kHz mono
6 Nellymoser
7 G.711 A-law logarithmic PCM
8 G.711 mu-law logarithmic PCM
9 reserved
10 AAC
11 Speex
14 MP3 8-Khz
15 Device-specific sound
- 接着2位为采样率(对于AAC总是3)
值 类型
0 5.5-kHz
1 11-kHz
2 22-kHz
3 44-kHz
  • 接着1位为采样的长度(压缩过的音视频都是16bit)
值 类型
0 snd8Bit
1 snd16Bit
  • 接着1位为音频类型(对于AAC总是1)
值 类型
0 sndMono
1 sndStereo
video

由于kobe视频音频编码是pcm,查找视频tag太难,使用<<东风破>> mv视频 flv-video1

  • type=0x09=9。这里应该是一个video。
  • size=0x000030=48。长度为48。
  • timestreamp=0x000000。
  • TimestampExtended =0x00。
  • stream id =0x000000

我们看到数据部分: 视频信息+数据 视频信息,1个字节。

StreamId之后的数据就表示是VideoTagHeader,如果是avc,VideoTagHeader会多出4个字节的信息就是AVCPacketType和CompositionTime

  • 前4位为帧类型Frame Type
值 类型
1 keyframe (for AVC, a seekable frame) 关键帧
2 inter frame (for AVC, a non-seekable frame)
3 disposable inter frame (H.263 only)
4 generated keyframe (reserved for server use only)
5 video info/command frame
  • 后4位为编码ID (CodecID)
值 类型
1 JPEG (currently unused)
2 Sorenson H.263
3 Screen video
4 On2 VP6
5 On2 VP6 with alpha channel
6 Screen video version 2
7 AVC

特殊情况 视频的格式(CodecID)是AVC(H.264)的话,VideoTagHeader会多出4个字节的信息,AVCPacketType 和CompositionTime。

  • AVCPacketType 占1个字节
值 类型
0 AVCDecoderConfigurationRecord(AVC sequence header)
1 AVC NALU
2 AVC end of sequence (lower level NALU sequence ender is not required or supported)

AVCDecoderConfigurationRecord.包含着是H.264解码相关比较重要的sps和pps信息,再给AVC解码器送数据流之前一定要把sps和pps信息送出,否则的话解码器不能正常解码。而且在解码器stop之后再次start之前,如seek、快进快退状态切换等,都需要重新送一遍sps和pps的信息.AVCDecoderConfigurationRecord在FLV文件中一般情况也是出现1次,也就是第一个video tag.

  • CompositionTime 占3个字节
条件 值
AVCPacketType ==1 Composition time offset
AVCPacketType !=1 0

再看到第二个video tag flv-video

我们看到 AVCPacketType =1,而后面三个字节为000043。这是一个视频帧数据。 解析到的数据完全符合上面的理论。

sps pps 前面我们提到第一个video 一般存放的是sps和pps。这里我们具体解析下sps和pps内容。先看下存储的格):

1
0x01+sps[1]+sps[2]+sps[3]+0xFF+0xE1+sps size+sps+01+pps size+pps

sps[1]=0x64 sps[2]=00 sps[3]=0D sps size=0x001B=27(占两个字节) 跳过27个字节后,是0x01 pps size=0x0005=118(占两个字节) 跳过5个字节,就到了back-pointers。

视频帧数据 解析出sps和pps tag后,后面的video tag就是真正的视频数据内容了 flv-video3 这是第二个video tag其实和之前图一样,只是我圈出来关键信息。先看下格式 frametype=0x17=00010111 AVCPacketType =1 Composition Time=0x000043 后面就是NALU DATA

引用:

flv格式详解+实例剖析

FLV视频封装格式详解

【总结】FLV(AAC/AVC)学习笔记

将h.264视频流封装成flv格式文件(一.flv格式)

将h.264视频流封装成flv格式文件(二.开始动手)

RTMP协议中的AMF数据

rtmp协议简单解析以及用其发送h264的flv文件

FLV 文件格式解析

(原)从mp4,flv文件中解析出h264和aac,送解码器解码失败:,avc1与H264区别在这里其实有人遇到了和我一样的问题:http://stackoverflow.com/questions/11330764/ffmpeg-cant-decode-h264-stream-frame-data

simplest_mediadata_test

rtmp_relay RtmpMindmap

(转)总结Android开发中必备的代码Review清单

发表于 2017-12-05 | 分类于 Android

前言

本文收集了我自己工作以来提交代码前的所有检查点。事实证明,这样能有效提高自己的代码质量和功能的稳定性。所以推荐大家以后每次提交代码前,都可以看下这份Review清单哈。

此外,可能还有些检查点我并没有发现,欢迎大家踊跃在评论区补充哈~

清理操作

  1. 页面退出时,是否完成必要的清理操作

    1. 是否调用Handler的removeCallbacksAndMessages(null)来清空Handler里的消息;
    2. 是否取消了还没完成的请求;
    3. 在页面里注册的监听,是否反注册;
    4. 假如自己用到观察者模式,是否反注册;
    5. 假如用了RxJava的话,是否解除订阅;
  2. 数据库的游标是否已经关闭 这个点一般人都知道,出问题一般在于,没有考虑到多线程并发时的情况下,Cursor没有被释放。 所以数据库的操作需要加上同步代码块 详细可参考:http://www.2cto.com/kf/201408/329574.html

  3. 打开过的文件流是否关闭

  4. Android 3.0以下的版本,使用完的Bitmap是否调用recycle(),否则会一直占用内存 而Android 3.0及以上的版本不需要调用recycle(),因为这些版本的Bitmap全部放到虚拟机的堆内存中,让GC自动回收。

  5. WebView使用完是否调用了其destory()函数

是否能进一步优化自己的代码

  1. 保存在内存中的图片,是否做过压缩处理再保存在内存里 否则可能由于图片质量太高,导致OOM

  2. Intent传递的数据太大,会导致页面跳转过慢。太大的数据可以通过持久化的形式传递,例如读写文件

  3. 频繁地操作同一个文件或者执行同一个数据库操作,是否考虑把它用静态变量或者局部变量的形式缓存在内存里。用空间换时间

  4. 放在主页面的控件,是否可以考虑用ViewStub来优化启动速度

要小心第三方包

  1. build.gradle远程依赖第三方包时,版本号建议写死,不要使用+号 避免由于新版本的第三方包引入了新的问题

  2. 导入第三方工程时,记得把编码转换成自己工程当前是用的编码

  3. 调用第三方的包或者JDK的方法时,要跳进他们的源码,看要不要加 try-catch 否则可能会导致自己应用的崩溃

  4. 使用第三方包时,是否加上其混淆规则 若漏掉加上第三方包的混淆规则,会导致第三方包不该混淆的代码被混淆。在Debug版本没有发现问题,但是Release版本就会出现问题

  5. 系统应用添加so时,是否在固件对应的Android.mk文件上加入新增的so,否则系统可能编译不过

    1
    2
    @lib/armeabi/libcommon.so \
    @lib/armeabi/libabcdefg.so \

注意要成对出现的地方

  1. 系统的、自己写的,注册和反注册的方法,是否成对出现

  2. 在生命周期的回调里,创建和销毁的代码是否对应起来 比如:onCreate()里面创建了Adapter,那么对应Adapter的退出处理操作(比如清空Image缓存),一般就要写在onDestory(),而不能写在onDestoryView()。

类似的生命周期对应的代码有: onStart()、onStop(); onCreate()、onDestory(); onResume()、onPause(); onCreateView()、onDestoryView()

  1. 若ListView的item复用了,对Item里View的操作是否成对出现 比如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    switch (type) {
    case ArticleListItem.TYPE_AD:
    ......
    mTitleView.setText(tencentAdBean.title);
    mGreenLabelView.setVisibility(VISIBLE);
    mRedLabelView.setText("");
    mRedLabelView.setVisibility(GONE);
    break;
    case ArticleListItem.TYPE_ARTICLE:
    ......
    mTitleView.setText(mzAdBean.adData.getTitle());
    mGreenLabelView.setVisibility(GONE);
    mRedLabelView.setText("ABC");
    mRedLabelView.setVisibility(VISIBLE);
    break;
    }

比如以上对mTitleView、mGreenLabelView和mRedLabelView的操作,都是成对出现。否则ListView可能会由于Item复用,导致Item显示错乱问题

防内存泄漏

  1. 内部类,比如Handler、Listener、Callback是否是成static class 因为非静态内部类会持有外部类的引用。

  2. 假如子线程持有了Activity,要用弱引用来持有 比如Request的Activity就应该用弱引用的形式,防止内存泄漏。

  3. 要求传入Activity作为参数的函数,是否可以改用getApplicationContext()来作为参数

Handler相关

  1. 使用View.post()是否会有问题 因为在View处于detached状态期间,post()里面的Runnable是不会被执行的。只有在此View处于attached状态时才会被执行。

如果想改Runnable每次肯定会被执行,那么应该是用Handler.post来替代

  1. 假如程序可能多次在同一个Handler里post同一个Runnable,每次post之前都应该先清空这个Handler中还没执行的该Runnable 如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    if (mCloudRun != null) {
    mHandler.removeCallbacks(mCloudRun);
    mCloudRun = null;
    }
    mCloudRun = new Runnable() {
    @Override
    public void run() {
    CloudAccelerateSwitchRequest request = new CloudAccelerateSwitchRequest();
    request.setPriority(RequestTask.PRIORITY_LOW);
    RequestQueue.getInstance().addRequest(request);
    }
    };
    mHandler.post(mCloudRun);

其他

  1. 多思考某些情况下,某变量是否会为空 而且在函数体内,处理参数前,必须加上判空语句

  2. 回调函数是否处理好 回调函数很容易出问题。比如网络请求的回调,需要判断此时的Aciivity等是否还存在,再进行调用。因为异步操作回来,Activity可能就消失不存在了。 而且还要对一些可能被回收的变量进行判空。

  3. 修改数据库后,是否把数据库的版本号+1

  4. 启动第三方的Activity时,是否判断了该Intent能否被解析

    1
    2
    3
    4
    5
    Intent sendIntent = new Intent(mContext, Demo.class);
    // 这种方式判断是否存在
    if (sendIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(sendIntent);
    }

若Activity不存在,会出现ActivityNotFoundException的异常

  1. 新注册的Activity、Service或Provider,若AndroidManifest.xml中exported属性为true,要考虑是否会引发安全性问题
    1
    2
    <activity android:name="com.inkenka.DemoActivity"
    android:exported="true"/>

因为exported属性为true时,外部应用就可以直接调用起该Activity。 可能导致的问题:

  • 若外部应用直接启动详情页,从而让某些验证页面直接被绕过
  • 若外部应用给该Activity传递乱七八糟的Intent,可能让该应用崩溃。也就是Android中的拒绝服务漏洞
  1. 除数是否做了非0判断

  2. 不要在Activity的onCreate里调用PopupWindow的showAsLoaction方法,由于Activity还没被加载完,会报错

功能完成后,自测时的检查点

  1. 思考某些情况下,某个变量是否会造成空指针问题
  2. 把手机横屏,检查布局是否有Bug
  3. 在不同分辨率的机型上,检查布局是否有Bug
  4. 切换到英文等外文字体下,检查外文是否能完整显示
  5. 从低版本升级上来,会不会有问题,比如可能会出现数据库不兼容的问题
  6. 按下Home再返回是否正常
  7. 熄灭屏幕再打开是否正常
  8. 切换成其它应用再切换回来会怎样
  9. 利用手机的开发者选项中的 “调试GPU过度绘制” ,“GPU呈现模式分析” 和 “显示FPS和功耗” 功能,看自己的新功能是否会导致过度绘制、是否会掉帧
  10. 测试看是否影响启动速度adb shell am start -W 包名/Activity
  11. 对比看APK大小是否有增大
  12. 跑1小时Monkey,测试其稳定性

转自: 良心推荐:总结Android开发中必备的代码Review清单

补充:总结工作中的Android内存泄漏问题

简单判断是否有内存泄漏

判断内存泄漏的定位的大单位是Activity。

可以通过反复进入退出一个Activity,然后用adb shell dumpsys meminfo + 包名 查看虚拟机的堆是否有不断地增长

定位内存泄漏

1.使用Leak Canary

在代码上加入Leak Canary,然后不断跑Monkey或者手动反复进出不同页面。若出现内存泄漏问题,会自动导出来,生成以下页面。

2.使用DDMS导出hprof,并用MAT工具进行分析

  • 强烈建议先跑30分钟Monkey测试
  • 使用eclipse的ddms找到对应的进程,触发一次gc后,dump出里面的内存快照hprof文件以分析当前应用内存的堆有什么东西
  • 使用Android SDK 里的platform-tools文件夹的 hprof-conv工具,对刚才 hprof 文件进行转换,以至于 后面MAT工具能正常打开
  • 使用MAT打开hprof文件,进入Histogram。输入自己猜测可能泄漏的Activity(项目中Activity不多时,可每个Activity都重复以下3、4、5步骤)
  • 键该其中一项,打开菜单选择list objects ->with incoming refs将列出该类的实例
  • 右健Path to GC Roots–>exclue all phantom/weak/soft etc. reference,找出这个实例GC后,还会存在什么对象的引用关系。

常见导致内存泄漏的几个点

生命周期的原因

比如:Activity中关联了一个生命周期超过Activity的Thread,这个Thread 若持有该Activity的引用,就会导致内存泄漏。

内部类的原因

因为内部类会隐式地持有外部类的引用,若内部类不被释放,外部类也是无法释放。常见的有内部的Listener、Callback、Handler等导致。

情景1:若外部类应该释放的时候,内部类还在执行里面的函数,会导致外部类无法释放。

情景2:若一个异步操作,会回调内部类的Listener、Callback、Handler。当外部类应该释放的时候,但是这个异步操作还存在,而这个异步操作类又持有了Listener、Callback、Handler,导致外部类无法被释放。PS:这个原因也属于生命周期的原因。

静态变量的原因

单例类里包含Activity

静态变量的类里引用到Activity

注册与反注册、打开与关闭没成对出现的原因

比如:注册广播接收器、注册观察者(典型的譬如数据库的监听)等。或者自己写的跟Activity引用有关的clear()函数没有成对出现

解决方法

解决内部类的问题(以Handler作为例子)

  1. onDestroy时候remove所有msgActivity finish后未处理的msg是问题根源,所以清空所有未被执行的msg
    1
    mHandler.removeCallbacksAndMessages(null);

PS:比如Listener、Callback等其他内部类的问题,页面退出的时候,应该完成必要的清理操作,比如Cancel 请求

  1. 使用静态内部类 + weakReference 静态内部类不会保留对外部类的引用,如果一定要引用外部类,使用weakReference
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    static class MyHandler extends Handler {
    WeakReference<Activity > mActivityReference;
    MyHandler(Activity activity) {
    mActivityReference= new WeakReference<Activity>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
    final Activity activity = mActivityReference.get();
    if (activity != null) {
    mImageView.setImageBitmap(mBitmap);
    }
    }
    }

PS:比如Listener、Callback等其他内部类的问题,也可以通过这个方法来解决

单例类里面尽量不要传入Activity,最好穿入ApplicationContext。假如传入了Activity,持有的时长也不能大于Activity的生命周期

对象的注册与反注册要成对出现

不使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存

因为View会持有Context,所以注意不要异步引用View,不要让静态对象持有View,不要在集合框架中存储View

android架构

发表于 2017-12-05 | 分类于 Android

Android官方架构组件介绍之LifeCycle,Android架构组件一共包括以下几个:

  • LifeCycle : 与Activity和Fragment的生命周期有关
  • LiveData :异步可订阅数据,也是生命周期感知
  • ViewModel :视图数据持有模型,也是生命周期感知
  • Room :SQLite抽象层,用于简化SQLite数据存储

官网

设计原则

Android应用程序的开发使用Java编写,在架构上使用MVC,鼓励组件之间的弱耦合。开发出编写可重用、可扩展、可维护、灵活性高的代码需要遵循以下原则。 ● “开—闭”原则(OCP):一个软件实体应当对扩展开放,对修改关闭。这个原则说的是,在设计一个模块时,应当使这个模块可以在不被修改的前提下被扩展。换言之,应当允许在不必修改源代码的情况下改变这个模块的行为。 ● 里氏代换原则(LSP):一个软件实体如果使用的是一个基类的话,那么一定使用于其子类,而且它根本不能察觉出基类对象和子类对象的区别。 ● 依赖倒转原则(DIP):要依赖于抽象,不要依赖于具体。 ● 接口隔离原则(ISP):使用多个专门的接口比使用单一的总接口要好。一个类对另外一个类的依赖性应当是建立在最小的接口上的。 ● 合成/聚合复用原则(CARP):又称合成复用原则(CRP),就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象的委派达到复用已有功能的目的。简而言之就是:要尽量使用合成/聚合,尽量不使用继承。 ● 迪米特法则(LoD):又称最少知识原则(LKP),是说一个对象应当对其他对象尽可能少的了解。狭义的迪米特法则是指如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类,可以通过第三者转发这个调用。广义的迪米特法则是指一个模块设计得好坏的一个重要的标志就是该模块在多大的程度上将自己的内部数据与实现有关的细节隐藏起来。信息的隐藏非常重要的原因在于,它可以使各个子系统之间脱耦,从而允许它们独立地被开发、优化、使用、阅读及修改。

设计模式

Android中最明显的简单工厂模式之一就是获取服务的系统方法“getSystemService”,例如,获得SensorManager的具体代码如下所示。

Android与工厂方法模式

在Android的Activity、Service等核心组件中,都定义了onCreate()方法,例如,当我们要显示用户界面时,一般都会在onCreate()使用setContentView这个方法,这个方法是在Activity中定义的,在编写具体的

Android与抽象工厂模式

在Android的应用程序开发中涉及到的IPC通信就是抽象工厂模式很好的

Android与单例模式

单例模式在Android中无所不在,例如,对服务的管理者ServiceManager就采用了单例模式,具体代码如下所示。

构造器模式

在Android中,AlertDialog的构造是建造者模式一个非常经典的实现,具体代码如下所示。

装饰器模式

在Java I/O库的设计和实现中就很好地使用了装饰模式。JDK提供的java.io包中使用了Decorator模式来实

外观模式

是一种得到广泛应用的模式,例如,我们熟知的MVC模式就采用了外观模式。在MVC架构模式中,每一层并不需要知道其他层次的细节,只是通过层与层之间的接口调用即可,这极大方便了应用开发

Facade设计模式

在JNI中的美妙应用,如下图所示。

享元模式

在Android中,享元模式非常明显的应用是在SQLite数据库数据查询时,当我们向SQLite发起查询时,SQL语句会被编译和缓存,此时即享元模式。

适配器模式

Android中的ListView是对适配器模式非常好的说明

代理模式

Android中两个进程间的通信,如 框架在支持Android应用程序间的跨进程通信时就是基于Binder的,要实现进程间通信的双方只需要遵循共同的AIDL接口即可,而在背后是使用了代理模式的,下面举具体的例子说明。

组合模式

Android中的View布局树是组合模式非常生动直观的说明

模板方式模式

Android中的四大组件的生命周期方法是模板方法模式的绝妙体现。

观察者模式

Android中观察者模式的实现可谓比比皆是,例如,一个Button的ButtonClickListener就是观察者,当用户单击该Button时,ButtonClickListener就会做出相应的响应。

状态模式

当Activity执行不同的方法时,会处于相应的状态中,这就是状态模式的应用。

Android与策略模式

Android中的ListView是策略模式非常好的说明,ListView汇总会有很多Items,每个处理会根据特定Item的ID可以选择不同的策略进行处理。我们在5.4节中已经进行了详细的阐述。

linux批量操作文件

发表于 2017-11-30 | 分类于 linux管理

一个批量将mp4文件转成gif文件的命令

1
find . -name "*.mp4" |sed 's/.mp4$//g'|xargs -i ffmpeg -i {}.mp4 {}.gif

sed 's/.mp4$//g'使用sed命令将mp4文件名的.mp4全部替换成空./g是全局替换.s是sed的替换命令,替换格式's/原文/要替换成的/'

或者:

1
for file in $(find . -name "*.mp4" -type f);do ffmpeg -i "$file" "${file%.*}.gif";done

找到所有.mp4文件进行循环,file是mp4文件全名,${file%.*}是剔除从右边最小匹配,即将.mp4去掉

mac下Core Dump文件的行程与分析

发表于 2017-11-17 | 分类于 env

mac下生成core dump

  1. 使用ulimit -c查看ulimit设置,显示unlimited表示开启,显示0表示关闭,通过ulimit -c unlimited打开设置; 但是这个只在当前窗口有效果。如果需要变成系统全局设置。 就需要去改/etc/profile文件,打开,然后加上ulimit -c unlimited就可以了,这样当产生Crash的时候就会自动产生dump文件。

  2. 之后需要配置一下dump产生的规则和路径:sudo sysctl kern.corefile=/cores/core.%N.%P,其中%N表示进程名字,%P表示进程id。Linux还有%S,%T分别表示最后一个信号和时间,在MAC上没找到对应的。(mac默认生成的core dump在/cores/下).

  3. 最后如何用lldb来查看一个core dump文件lldb -c core.xxx. 在lldb命令下输入bt查看报错代码.

生成太多core文件会占用电脑磁盘,可以关闭全局的core dump生成配置:

  1. 永久关闭,则在/etc/sysctl.conf中加入一行(如果存在,则将其值修改为0),重启后生效:kern.coredump=0
  2. 零时关闭,当前生效,重启后失效:sudo sysctl -w kern.coredump=0

android兼容性

发表于 2017-11-14 | 分类于 Android

oppo r9s无法浮层无法显示问题

oppo r9s,系统版本6.0.1,wmParams.type = WindowManager.LayoutParams.TYPE_TOAST;时无法正常弹出,改成wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;可显示.

在activity中弹出浮层后马上将activity movetoback导致oppo r9s 浮层无法显示,moveTaskToBack后延迟一秒显示浮层可解决问题.

1
2
3
4
5
//moveTaskToBack
val intent = Intent(Intent.ACTION_MAIN)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK// 注意
intent.addCategory(Intent.CATEGORY_HOME)
aty.startActivity(intent)

SQL查询案例

发表于 2017-11-08 | 分类于 db

如下数据库表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Student(S#,Sname,Sage,Ssex)学生表
S#:学号
Sname:学生姓名
Sage:学生年龄
Ssex:学生性别
Course(C#,Cname,T#)课程表
C#:课程编号
Cname:课程名称
T#:教师编号
SC(S#,C#,score)成绩表
S#:学号
C#:课程编号
score:成绩
Teacher(T#,Tname)教师表
T#:教师编号:
Tname:教师名字
  1. 查询“001”课程比“002”课程成绩高的所有学生的学号

    1
    select a.S# from (select S#,score from SC where C#='001')a, (select s#,score from SC where c#='002')b Where a.score>b.score
  2. 查询平均成绩大于60分的同学的学号和平均成绩

    1
    查询平均成绩大于60分的同学的学号和平均成绩
  3. 查询所有同学的学号、姓名、选课数、总成绩

    1
    select student.S#, student.Sname, count(sc.C#), sum(score) from student left outer join SC on student.S# = SC.S# group by S
  4. 查询姓‘李’的老师的个数

    1
    2
    3
    select count(distinct(Tname))
    from teacher
    where tname like '李%';
  5. 查询没有学过“叶平”老师可的同学的学号、姓名:

    1
    2
    3
    4
    select student.S#, student.Sname
    from Student
    where S# not in (select distinct(SC.S#) from SC,Course,Teacher
    where sc.c#=course.c# AND teacher.T#=course.T# AND Teahcer.Tname ='叶平');
  6. 查询学过“叶平”老师所教的所有课的同学的学号、姓名:

    1
    2
    3
    4
    5
    6
    select S#,Sname   from Student    
    where S# in (select S# from SC ,Course ,Teacher
    where SC.C#=Course.C# and Teacher.T#=Course.T#
    and Teacher.Tname='叶平' group by S#
    having count(SC.C#)=(select count(C#) from Course,Teacher
    where Teacher.T#=Course.T# and Tname='叶平'));
  7. 查询学过“011”并且也学过编号“002”课程的同学的学号、姓名:

    1
    2
    3
    4
    select Student.S#,Student.Sname
    from Student,SC where Student.S#=SC.S#
    and SC.C#='001'and
    exists( Select * from SC as SC_2 where SC_2.S#=SC.S# and SC_2.C#='002');
  8. 查询课程编号“002”的成绩比课程编号“001”课程低的所有同学的学号、姓名:

    1
    2
    3
    4
    5
    6
    Select S#,Sname
    from (select Student.S#,Student.Sname,score ,
    (select score from SC SC_2 where SC_2.S#=Student.S# and SC_2.C#='002') score2
    from Student,SC
    where Student.S#=SC.S# and C#='001') S_2
    where score2 < score;
  9. 查询所有课程成绩小于60的同学的学号、姓名:

    1
    2
    3
    4
    select S#, sname
    from student
    where s# not in
    (select student.s# from student, sc where s.s# = sc.s# and score>60);
  10. 查询没有学全所有课的同学的学号、姓名:

    1
    2
    3
    4
    5
    select student.s#, student.sname
    from student, sc
    where student.s#=sc.s#
    group by student.s#, student.sname
    having count(c#)<(select count(c#) from course);
  11. 查询至少有一门课与学号为“1001”同学所学相同的同学的学号和姓名:

    1
    2
    3
    4
    select s#, Sname
    from Student, SC
    where student.s# = sc.s#
    and c# in (select c# from SC where s#='1001');
  12. 查询至少学过学号为“001”同学所有一门课的其他同学学号和姓名;

    1
    2
    3
    4
    select distinct sc.s# , sname
    from student, sc
    where student.s#=sc.s#
    and c# in (select C# from sc where s#='001');
  13. 把“SC”表中“叶平”老师教的课的成绩都更改为此课程的平均成绩:

    1
    2
    3
    Update Sc Set Score=(Select Avg(s2_Score) From sc s2 Where s2.c#=sc.c#)  
    Where c# IN
    (Select c# From sc cs INNER JOIN Teacher tc ON cs.t#=tc.t# WHERE tname ='叶平')
  14. 查询和“1002”号的同学学习的课程完全相同的其他同学学号和姓名:

    1
    2
    3
    4
    select s# from sc where c#  in
    (select c# from sc where s#='1002')
    group by s# having count(*)=
    (select count(*) from sc where s#='1002');
  15. 删除学习“叶平”老师课的SC表记录:

    1
    2
    3
    4
    5
    delect sc
    from course, Teacher
    where course.c#=sc.c#
    and course.t#=teacher.t#
    and tname='叶平';
  16. 向SC表中插入一些记录,这些记录要求符合以下条件:没有上过编号“003”课程的同学学号、002号课的平均成绩:

    1
    2
    3
    Insert SC select S#,'002',
    (Select avg(score) from SC where C#='002')
    from Student where S# not in (Select S# from SC where C#='002');
  17. 按平均成绩从高到低显示所有学生的“数据库”、“企业管理”、“英语”三门的课程成绩,按如下形式显示:学生ID,数据库,企业管理,英语,有效课程数,有效平均分:

    1
    2
    3
    4
    5
    6
    7
    8
    select s# as 学生ID,
    (select score from sc where sc.s#=t.s# and c#='004') as 数据库,
    (select score from sc where sc.s#=t.s# and c#='001') as 企业管理,
    (select score from sc where sc.s#=t.s# and c#='006') as 英语,
    count(*) as 有效课程数, avg(t.score) as 平局成绩
    from sc as t
    group by s#
    order by avg(t.score)
  18. 查询各科成绩最高和最低的分: 以如下的形式显示:课程ID,最高分,最低分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    select L.c# as 课程ID, L.score as 最高分,
    R.score as 最低分
    from sc L, sc R
    where L.c# = R.c#
    and L.score = (select max(IL.score)
    from sc IL, student as IM
    where L.c#=IL.c# and IM.s#=IL.s#
    group by IL.c#)
    and R.score = (select min(IR.score)
    from sc as IR
    where R.c#=IR.c#
    group by IR.c#);
  19. 按各科平均成绩从低到高和及格率的百分数从高到低顺序:

    1
    2
    3
    4
    5
    6
    7
    8
    SELECT t.C# AS 课程号,
    max(course.Cname)AS 课程名,
    isnull(AVG(score),0) AS 平均成绩,
    100 * SUM(CASE WHEN isnull(score,0)>=60 THEN 1 ELSE 0 END)/COUNT(*) AS 及格百分数
    FROM SC T,Course
    where t.C#=course.C#
    GROUP BY t.C#
    ORDER BY 100 * SUM(CASE WHEN isnull(score,0)>=60 THEN 1 ELSE 0 END)/COUNT(*) DESC
  20. 查询如下课程平均成绩和及格率的百分数(用”1行”显示): 企业管理(001),马克思(002),OO&UML (003),数据库(004):

  21. 查询不同老师所教不同课程平均分从高到低显示:

    1
    2
    3
    4
    5
    6
    7
    8
    SELECT max(Z.T#) AS 教师ID,
    MAX(Z.Tname) AS 教师姓名,
    C.C# AS 课程ID,
    AVG(Score) AS 平均成绩
    FROM SC AS T,Course AS C ,Teacher AS Z
    where T.C#=C.C# and C.T#=Z.T#
    GROUP BY C.C#
    ORDER BY AVG(Score) DESC
  22. 查询如下课程成绩第3名到第6名的学生成绩单:企业管理(001),马克思(002),UML(003),数据库(004):

  23. 统计下列各科成绩,各分数段人数:课程ID,课程名称,[100-85],[85-70],[70-60],[ 小于60] :

    1
    2
    3
    4
    5
    6
    7
    8
    SELECT SC.C# as 课程ID, Cname as 课程名称,
    SUM(CASE WHEN score BETWEEN 85 AND 100 THEN 1 ELSE 0 END) AS [100 - 85] ,
    SUM(CASE WHEN score BETWEEN 70 AND 85 THEN 1 ELSE 0 END) AS [85 - 70],
    SUM(CASE WHEN score BETWEEN 60 AND 70 THEN 1 ELSE 0 END) AS [70 - 60],
    SUM(CASE WHEN score < 60 THEN 1 ELSE 0 END) AS [60 -]
    FROM SC,Course
    where SC.C#=Course.C#
    GROUP BY SC.C#,Cname;
  24. 查询学生平均成绩及其名次:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    SELECT 1+(SELECT COUNT( distinct 平均成绩)                
    FROM (SELECT S#,AVG(score) AS 平均成绩
    FROM SC
    GROUP BY S# ) AS T1 WHERE 平均成绩 > T2.平均成绩) as 名次,
    S# as 学生学号,平均成绩
    FROM (SELECT S#,AVG(score) 平均成绩
    FROM SC
    GROUP BY S# ) AS T2
    ORDER BY 平均成绩 desc;
  25. 查询各科成绩前三名的记录(不考虑成绩并列情况):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    SELECT t1.S# as 学生ID,t1.C# as 课程ID,Score as 分数       
    FROM SC t1
    WHERE score IN
    (SELECT TOP 3 score
    FROM SC
    WHERE t1.C#= C#
    ORDER BY score DESC)
    ```
    26. 查询每门课程被选修的学生数:

select c#, count(s#) from sc group by c#;

1
27. 查询出只选修一门课程的全部学生的学号和姓名:

select sc.s#, student.sname, count(c#) as 选课数 from sc,student where sc.s# =student.s# group by sc.s#,Student.sname having count(c#)=1;

1
28. 查询男生、女生人数:

select count(Ssex) as 男生人数 from student group by Ssex having Ssex=’男’; select count(Ssex) as 女生人数 from student group by Ssex having Ssex=’女’;

1
29. 查询姓“张”的学生名单:

select sname from student where sname like ‘张%’;

1
30. 查询同名同姓的学生名单,并统计同名人数:

select sanme,count() from student group by sname havang count()>1;

1
31. 1981年出生的学生名单(注:student表中sage列的类型是datetime):

select sname, convert(char(11),DATEPART(year,sage)) as age from student where convert(char(11),DATEPART(year,Sage))=’1981’;

1
32. 查询平均成绩大于85的所有学生的学号、姓名和平均成绩:

select Sname,SC.S# ,avg(score)
from Student,SC
where Student.S#=SC.S# group by SC.S#,Sname having avg(score)>85;

1
33. 查询每门课程的平均成绩,结果按平均成绩升序排序,平均成绩相同时,按课程号降序排列:

select C#, avg(score) from sc group by c# order by avg(score), c# desc;

1
34. 查询课程名称为“数据库”,且分数低于60的学生名字和分数:

select sname, isnull(score,0) from student, sc ,course where sc.s#=student.s# and sc.c#=course.c# and course.cname=’数据库’ and score<60;

1
35. 查询所有学生的选课情况:

select sc.s#,sc.c#,sname,cname from sc,student course where sc.s#=student.s# and sc.c#=course.c#;

1
36. 查询任何一门课程成绩在70分以上的姓名、课程名称和分数:

select distinct student.s#,student.sname,sc.c#,sc.score from student,sc where sc.score>=70 and sc.s#=student.s#;

1
37. 查询不及格的课程,并按课程号从大到小的排列:

select c# from sc where score<60 order by c#;

1
38. 查询课程编号为“003”且课程成绩在80分以上的学生的学号和姓名:

select sc.s#,student.sname from sc,student where sc.s#=student.s# and score>80 and c#=’003’;

1
39. 求选了课程的学生人数:

select count(*) from sc;

1
40. 查询选修“叶平”老师所授课程的学生中,成绩最高的学生姓名及其成绩:

select student.sname,score from student,sc,course c, teacher where student.s#=sc.S# and sc.c#=c.c# and c.T#=teacher.T# and teacher.tname=’叶平’ and sc.score=(select max(score) from sc where c#=c.c#);

1
41. 查询各个课程及相应的选修人数:

select count(*) from sc group by c#;

1
42. 查询不同课程成绩相同的学生和学号、课程号、学生成绩:

select distinct a.s#,b.score from sc a ,sc b where a.score=b.score and a.c#<>b.c#;

1
43. 查询每门课程成绩最好的前两名:

select t1.s# as 学生ID,t1.c# 课程ID, Score as 分数 from sc t1 where score in (select top 2 score from sc where t1.c#=c# order by score desc) order by t1.c#;

1
44. 统计每门课程的学生选修人数(超过10人的课程才统计)。要求输出课程号和选修人数,查询结果按人数降序排序,若人数相同,按课程号升序排序:

select c# as 课程号,count() as 人数 from sc group by c# order by count() desc c#;

1
45. 检索至少选修两门课程的学生学号:

select s# from sc group by s# having count(*)>=2;

1
46. 查询全部学生选修的课程和课程号和课程名:

select c# ,cname from course where c# in (select c# from sc group by c#);

1
47. 查询没学过”叶平”老师讲授的任一门课程的学生姓名:

select sname from student where s# not in (select s# from course,teacher,sc where course.t#=teacher.t# and sc.c#=course.c# and tname=’叶平’);

1
48. 查询两门以上不及格课程的同学的学号以及其平均成绩:

select s#,avg(isnull(score,0)) from sc where s# in (select s# from sc where score<60 group by s# having count(*)>2) group by s#;

1
49. 检索“004”课程分数小于60,按分数降序排列的同学学号:

select s# from sc where c#=’004’ and score<60 order by score desc;

1
50. 删除“002”同学的“001”课程的成绩:

delect from sc where s#=’002’ and c#=’001’;

1…111213…19
轻口味

轻口味

190 日志
27 分类
63 标签
RSS
GitHub 微博 豆瓣 知乎
友情链接
  • SRS
© 2015 - 2019 轻口味
京ICP备17018543号
本站访客数 人次 本站总访问量 次