基于安卓高仿how-old.net应用实现人脸识别估算年龄与性别

  前几段微软推出的大数据人脸识别年龄应用how-old.net在微博火了一把,它可以通过照片快速获得照片上人物的年龄,系统会对瞳孔、眼角、鼻子等27个“面部地标点"展开分析,进而得出你的“颜龄"。

来看下关于这款应用的截图:

 

 昨晚闲着没事,在网上查阅了点资料仿写了一款类似功能的APP,看下截图:

  

  关于人脸识别技术本想去使用微软给开发人员提供的SDK,但由于天朝巨坑的网络,我连How-old.net官网都登不上,只能绕道去找找其他地方有没类似功能的SDK。后来想起之前在搞O2O的时候,看到过一则关于支付宝"刷脸支付"功能的新闻,查找了相关资料发现他们的"刷脸技术"是Face++提供的,也就这样找到了个好东西。

  这是Face++的官方网站:http://www.faceplusplus.com.cn/,在网站里可以找到它为开发者提供了一部分功能的SDK(需要注册),其中就有人脸识别,判断年龄性别种族这个功能。

  我们注册个账号,然后创建个应用就可以得到官方给我们提供的APIKey和APISecret,记录下来,然后到到开发者中心(http://www.faceplusplus.com.cn/dev-tools-sdks/)就可以下载到对应版本的SDK,就一个Jar包直接导入项目就可以,这是官方给我们提供的API参考文档(http://www.faceplusplus.com.cn/api-overview/),这样子准备工作就做好了,可以开始进入软件编码阶段了。

先来看下布局文件:

很简单的布局文件,这里就直接贴代码了:

 1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5     android:background="#ffffff"
 6     android:orientation="vertical" >
 7
 8     <LinearLayout
 9         android:layout_width="match_parent"
10         android:layout_height="wrap_content"
11         android:layout_margin="5dp"
12         android:orientation="horizontal" >
13
14         <TextView
15             android:id="@+id/tv_tip"
16             android:layout_width="0dp"
17             android:layout_height="wrap_content"
18             android:layout_weight="1"
19             android:text="请选择图片"
20             android:textColor="#000000"
21             android:textSize="14dp" />
22
23         <Button
24             android:id="@+id/bt_getImage"
25             android:layout_width="0dp"
26             android:layout_height="wrap_content"
27             android:layout_weight="1"
28             android:text="选择图片"
29             android:textSize="16dp" />
30
31         <Button
32             android:id="@+id/bt_doAction"
33             android:layout_width="0dp"
34             android:layout_height="wrap_content"
35             android:layout_weight="1"
36             android:enabled="false"
37             android:text="识别操作"
38             android:textSize="16dp" />
39     </LinearLayout>
40
41     <ImageView
42         android:id="@+id/iv_image"
43         android:layout_width="match_parent"
44         android:layout_height="match_parent"
45         android:layout_marginBottom="5dp"
46         android:src="@drawable/ic_launcher" />
47
48     <FrameLayout
49         android:id="@+id/fl_view"
50         android:layout_width="wrap_content"
51         android:layout_height="wrap_content"
52         android:visibility="invisible" >
53
54         <TextView
55             android:id="@+id/tv_info"
56             android:layout_width="wrap_content"
57             android:layout_height="wrap_content"
58             android:background="@drawable/hint"
59             android:drawableLeft="@drawable/female"
60             android:gravity="center"
61             android:text="18"
62             android:textColor="#ff0044"
63             android:textSize="22sp"
64             android:visibility="invisible" />
65     </FrameLayout>
66
67 </LinearLayout>

再来说下主程序类,关于程序的实现,基本可以分为这几步:

1、进入程序,点击按钮跳转相册选取一张图片,并在程序主界面显示。

这里要注意的一些地方:

根据开发者API的/detection/detect(http://www.faceplusplus.com.cn/detection_detect/),我们可以知道,在设定APIKEY和APISECRERT的同时,我们需要指定一张图片的Url地址或者是把图片转为二进制数据向服务端进行POST提交,这里需要注意的是图片的大小不能超过1M,而现在智能手机的像素很高,随便拍一张照片都会超出这个限制范围,所以我们在获取到图片的同时需要对图片进行压缩处理。

2、封装所需要的参数,并把图片转为二进制数据提交到服务端获取识别结果(Json数据)。

3、根据服务端所返回的数据,设置显示到图像上。

大概实现就是这样子,下面直接上的代码吧,大部分注释都很详细,我不一个个讲了,会挑一些重点出来说。

这是一个网络请求工具类:

 1 package com.lcw.rabbit.face;
 2
 3 import java.io.ByteArrayOutputStream;
 4
 5 import org.json.JSONObject;
 6
 7 import android.graphics.Bitmap;
 8
 9 import com.facepp.error.FaceppParseException;
10 import com.facepp.http.HttpRequests;
11 import com.facepp.http.PostParameters;
12
13 /**
14  * Face++ 帮助类,执行网络请求耗时操作
15  *
16  * @author Rabbit_Lee
17  *
18  */
19 public class FaceHelper {
20
21     private static final String TAG = FaceHelper.class.getName();
22
23     /**
24      * 创建网络
25      *
26      * @param bitmap
27      * @param callBack
28      */
29     public static void uploadFaces(final Bitmap bitmap, final CallBack callBack) {
30         new Thread(new Runnable() {
31
32             @Override
33             public void run() {
34                 try {
35                     // 将Bitmap对象转换成byte数组
36                     ByteArrayOutputStream stream = new ByteArrayOutputStream();
37                     bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
38                     byte[] data = stream.toByteArray();
39
40                     // 创建连接请求
41                     HttpRequests requests = new HttpRequests(MainActivity.APIKey, MainActivity.APISecret, true, true);
42                     // 封装参数
43                     PostParameters params = new PostParameters();
44                     params.setImg(data);
45                     // 提交网络请求
46                     JSONObject jsonObject = requests.detectionDetect(params);
47                     // 设置回调数据
48                     callBack.success(jsonObject);
49                 } catch (FaceppParseException e) {
50                     // 设置回调数据
51                     callBack.error(e);
52                     e.printStackTrace();
53                 }
54
55             }
56         }).start();
57
58     }
59
60     /**
61      * 数据回调接口,方便主类获取数据
62      *
63      * @author Rabbit_Lee
64      *
65      */
66     public interface CallBack {
67
68         void success(JSONObject jsonObject);
69
70         void error(FaceppParseException exception);
71     }
72 }

既然是网络请求,那肯定属于耗时操作,那么我们需要在子线程里去完成,然后官方给我们提供的SDK里帮我们封装了一些工具类

例如:

HttpRequests类,它帮我们封装好了HTTP请求,我们可以直接设置参数去访问服务端。

打开源码我们可以发现除了无参构造,它还有2个构造方法,分别是2个参数和4个参数的,其实我们仔细看下源码便可以很轻松的发现,这些参数只不过是用来提交服务端的URL

分别是:(无疑我们是要选择CN+HTTP),所以后两个参数我们直接置为TRUE就可以了。

1     static final private String WEBSITE_CN = "https://apicn.faceplusplus.com/v2/";
2     static final private String DWEBSITE_CN = "http://apicn.faceplusplus.com/v2/";
3     static final private String WEBSITE_US = "https://apius.faceplusplus.com/v2/";
4     static final private String DWEBSITE_US = "http://apius.faceplusplus.com/v2/";

PostParameters类,用来设置参数的具体值的,这里提供了一个setImg方法,我们只需要把我们图片转为字节数组的变量直接存入即可。

为了方便主程序类方便获取到返回数据,这里采用了接口回调方法CallBack,设置了success(当数据正常返回时),error(当数据不正常返回时)。

这是主程序类:

  1 package com.lcw.rabbit.face;
  2
  3 import org.json.JSONArray;
  4 import org.json.JSONException;
  5 import org.json.JSONObject;
  6
  7 import android.app.Activity;
  8 import android.app.ProgressDialog;
  9 import android.content.Intent;
 10 import android.database.Cursor;
 11 import android.graphics.Bitmap;
 12 import android.graphics.BitmapFactory;
 13 import android.graphics.Canvas;
 14 import android.graphics.Paint;
 15 import android.net.Uri;
 16 import android.os.Bundle;
 17 import android.os.Handler;
 18 import android.os.Message;
 19 import android.provider.MediaStore;
 20 import android.view.View;
 21 import android.view.View.MeasureSpec;
 22 import android.view.View.OnClickListener;
 23 import android.widget.Button;
 24 import android.widget.ImageView;
 25 import android.widget.TextView;
 26 import android.widget.Toast;
 27
 28 import com.facepp.error.FaceppParseException;
 29
 30 public class MainActivity extends Activity implements OnClickListener {
 31
 32     // 声明控件
 33     private TextView mTip;
 34     private Button mGetImage;
 35     private Button mDoAction;
 36     private ImageView mImageView;
 37     private View mView;
 38     private ProgressDialog mDialog;
 39
 40     // Face++关键数据
 41     public static final String APIKey = "你的APPKEY";
 42     public static final String APISecret = "你的APPSERCRET";
 43
 44     // 标志变量
 45     private static final int REQUEST_CODE = 89757;
 46     private static final int SUCCESS = 1;
 47     private static final int ERROR = 0;
 48
 49     // 图片路径
 50     private String mPicStr;
 51     // Bitmap对象
 52     private Bitmap mBitmap;
 53
 54     private Handler handler = new Handler() {
 55         public void handleMessage(android.os.Message msg) {
 56             switch (msg.what) {
 57             case SUCCESS:
 58                 // 成功
 59                 JSONObject object = (JSONObject) msg.obj;
 60                 // 解析Json数据,重构Bitmap对象
 61                 reMakeBitmap(object);
 62                 mImageView.setImageBitmap(mBitmap);
 63                 break;
 64             case ERROR:
 65                 // 失败
 66                 String errorInfo = (String) msg.obj;
 67                 if (errorInfo == null || "".equals(errorInfo)) {
 68                     Toast.makeText(MainActivity.this, "Error", Toast.LENGTH_LONG).show();
 69                 } else {
 70                     Toast.makeText(MainActivity.this, errorInfo, Toast.LENGTH_LONG).show();
 71                 }
 72                 break;
 73
 74             default:
 75                 break;
 76             }
 77         }
 78
 79         private void reMakeBitmap(JSONObject json) {
 80
 81             mDialog.dismiss();
 82
 83             // 拷贝原Bitmap对象
 84             Bitmap bitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), mBitmap.getConfig());
 85             Canvas canvas = new Canvas(bitmap);
 86             canvas.drawBitmap(mBitmap, 0, 0, null);
 87
 88             Paint paint = new Paint();
 89             paint.setColor(0xffffffff);
 90             paint.setStrokeWidth(3);
 91
 92             try {
 93                 JSONArray faces = json.getJSONArray("face");
 94                 // 检测照片有多少张人脸
 95                 int facesCount = faces.length();
 96                 mTip.setText("识别到"+facesCount+"张人脸");
 97                 for (int i = 0; i < facesCount; i++) {
 98                     JSONObject position = faces.getJSONObject(i).getJSONObject("position");
 99                     // position属性下的所需数据,定位人脸位置
100                     Float x = (float) position.getJSONObject("center").getDouble("x");
101                     Float y = (float) position.getJSONObject("center").getDouble("y");
102                     Float width = (float) position.getDouble("width");
103                     Float height = (float) position.getDouble("height");
104
105                     // 把百分比转化为实际像素值
106                     x = x / 100 * bitmap.getWidth();
107                     y = y / 100 * bitmap.getHeight();
108                     width = width / 100 * bitmap.getWidth();
109                     height = height / 100 * bitmap.getHeight();
110                     // 绘制矩形人脸识别框
111                     canvas.drawLine(x - width / 2, y - height / 2, x - width / 2, y + height / 2, paint);
112                     canvas.drawLine(x - width / 2, y - height / 2, x + width / 2, y - height / 2, paint);
113                     canvas.drawLine(x + width / 2, y - height / 2, x + width / 2, y + height / 2, paint);
114                     canvas.drawLine(x - width / 2, y + height / 2, x + width / 2, y + height / 2, paint);
115
116                     // 获取年龄,性别
117                     JSONObject attribute = faces.getJSONObject(i).getJSONObject("attribute");
118                     Integer age = attribute.getJSONObject("age").getInt("value");
119                     String sex = attribute.getJSONObject("gender").getString("value");
120
121                     // 获取显示年龄性别的Bitmap对象
122                     Bitmap infoBm = makeView(age, sex);
123
124                     canvas.drawBitmap(infoBm, x - infoBm.getWidth() / 2, y - height / 2-infoBm.getHeight(), null);
125
126                     mBitmap = bitmap;
127
128                 }
129
130             } catch (JSONException e) {
131                 e.printStackTrace();
132             }
133
134         }
135
136         /**
137          * 构建一个显示年龄,性别的Bitmap
138          *
139          * @param age
140          * @param sex
141          */
142         private Bitmap makeView(Integer age, String sex) {
143             mView.setVisibility(View.VISIBLE);
144             TextView tv_info = (TextView) mView.findViewById(R.id.tv_info);
145             tv_info.setText(age.toString());
146
147             if (sex.equals("Female")) {
148                 //女性
149                 tv_info.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.female), null, null, null);
150             } else {
151                 //男性
152                 tv_info.setCompoundDrawablesWithIntrinsicBounds(getResources().getDrawable(R.drawable.male), null, null, null);
153             }
154             // 通过cache机制将View保存为Bitmap
155             tv_info.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
156             tv_info.layout(0, 0, tv_info.getMeasuredWidth(), tv_info.getMeasuredHeight());
157             tv_info.buildDrawingCache();
158             tv_info.setDrawingCacheEnabled(true);
159             Bitmap bitmap = Bitmap.createBitmap(tv_info.getDrawingCache());
160             tv_info.destroyDrawingCache();
161
162
163             return bitmap;
164         }
165     };
166
167     @Override
168     protected void onCreate(Bundle savedInstanceState) {
169         super.onCreate(savedInstanceState);
170         setContentView(R.layout.activity_main);
171         // 对控件进行初始化操作
172         initViews();
173         // 对控件进行监听操作
174         initActions();
175     }
176
177     private void initActions() {
178         mGetImage.setOnClickListener(this);
179         mDoAction.setOnClickListener(this);
180     }
181
182     private void initViews() {
183         mTip = (TextView) findViewById(R.id.tv_tip);
184         mGetImage = (Button) findViewById(R.id.bt_getImage);
185         mDoAction = (Button) findViewById(R.id.bt_doAction);
186         mImageView = (ImageView) findViewById(R.id.iv_image);
187         mView=findViewById(R.id.fl_view);
188         mDialog = new ProgressDialog(MainActivity.this);
189         mDialog.setMessage("系统检测识别中,请稍后..");
190
191     }
192
193     @Override
194     protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
195         super.onActivityResult(requestCode, resultCode, intent);
196         if (requestCode == REQUEST_CODE) {
197             if (intent != null) {
198                 Uri uri = intent.getData();
199                 Cursor cursor = getContentResolver().query(uri, null, null, null, null);
200                 if (cursor.moveToFirst()) {
201                     int index = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
202                     mPicStr = cursor.getString(index);
203                     cursor.close();
204
205                     // 根据获取到的图片路径,获取图片并进行图片压缩
206                     BitmapFactory.Options options = new BitmapFactory.Options();
207                     // 当Options的inJustDecodeBounds属性设置为true时,不会显示图片,只会返回该图片的具体数据
208                     options.inJustDecodeBounds = true;
209
210                     // 根据所选实际的宽高计算压缩比例并将图片压缩
211                     BitmapFactory.decodeFile(mPicStr, options);
212                     Double reSize = Math.max(options.outWidth * 1.0 / 1024f, options.outHeight * 1.0 / 1024f);
213                     options.inSampleSize = (int) Math.ceil(reSize);
214                     options.inJustDecodeBounds = false;
215
216                     // 创建Bitmap
217                     mBitmap = BitmapFactory.decodeFile(mPicStr, options);
218                     mImageView.setImageBitmap(mBitmap);
219
220                     mTip.setText("点击识别==》");
221                     mDoAction.setEnabled(true);
222
223                 }
224             }
225
226         }
227     }
228
229     @Override
230     public void onClick(View v) {
231         switch (v.getId()) {
232         case R.id.bt_getImage:
233             // 点击获取图片按钮跳转相册选取图片
234             Intent intent = new Intent(Intent.ACTION_PICK);
235             // 获取手机图库信息
236             intent.setType("image/*");
237             startActivityForResult(intent, REQUEST_CODE);
238             break;
239         case R.id.bt_doAction:
240             // 点击识别按钮进行图片识别操作
241             mDialog.show();
242             FaceHelper.uploadFaces(mBitmap, new FaceHelper.CallBack() {
243
244                 @Override
245                 public void success(JSONObject result) {
246                     // 人脸识别成功,回调获取数据
247                     Message msg = Message.obtain();
248                     msg.what = SUCCESS;
249                     msg.obj = result;
250                     handler.sendMessage(msg);
251                 }
252
253                 @Override
254                 public void error(FaceppParseException e) {
255                     // 人脸识别失败,回调获取错误信息
256                     Message msg = Message.obtain();
257                     msg.what = ERROR;
258                     msg.obj = e.getErrorMessage();
259                     handler.sendMessage(msg);
260                 }
261             });
262             break;
263
264         default:
265             break;
266         }
267
268     }
269
270 }

由于注释很全,这里我就挑几个地方出来讲,有其他不清楚的朋友可以在文章评论里留言,我会答复的。

1、当点击选择图片按钮时,通过Intent去访问系统图片,并根据返回的图片进行图片压缩,因为官方文档很明确的告诉我们图片大小不能超过1MB。

压缩图片有2种方式,一种是通过压缩图片的质量大小,另一种则是根据比例来压缩图片大小(这里我选择第二种压缩方式)。

根据得到的Bitmap对象,我们可以先将inJustDecodeBounds先设置成true,这样我们就不会得到具体的图片,只会得到该图片的获取到真实图片的宽和高,然后我们让其去除以1024,取较大的一个数作为压缩比例,取整利用inSampleSize去对Bitmap进行压缩,然后再把inJustDecodeBounds设置为false。

2、当我们提交图片并且服务端向我们返回数据时,由于我们是在子线程中去执行网络请求的,所以这边我们通过Handler机制来传输数据,使得主线程可以拿到数据并更新UI。

服务端给我们返回的是一个Json的数据,所以我们需要在这里将它进行解析(关于Json解析,我这里用到的是安卓官方自带的Json帮助类,想用谷歌提供的Gson工具类也是可以的,这边是工具类使用方法:《Gson简要使用笔记》)拿到我们所需要的数据,这里我们需要的数据有

具体数值各代表什么意思,这个大家查阅下官方给定的API文档就可以知道了,这里就不再详细去写了,然后根据所给的这些数值我们会圈出所对应人脸的位置,方法可以有很多,形状也无所谓,这里我给出的是矩形方案。

3、再来就是对隐藏布局TextView进行显示,由于我们可以在服务端给我们返回的数据里知道性别年龄等数据,这里就很好办了。

这里我通过通过cache机制将View转为Bitmap然后进行显示,当然不止是这种方法,用自定义View去进行布局也是可以的,这里大家灵活操作,我就不多说了。

到这里,文章就结束了,大家有什么疑问可以在文章评论下面给我留言。

作者:李晨玮
出处:http://www.cnblogs.com/lichenwei/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
正在看本人博客的这位童鞋,我看你气度不凡,谈吐间隐隐有王者之气,日后必有一番作为!旁边有“推荐”二字,你就顺手把它点了吧,相得准,我分文不收;相不准,你也好回来找我!

时间: 2024-10-12 12:16:37

基于安卓高仿how-old.net应用实现人脸识别估算年龄与性别的相关文章

Android 高仿微信实时聊天 基于百度云推送

转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38799363 ,本文出自:[张鸿洋的博客] 一直在仿微信界面,今天终于有幸利用百度云推送仿一仿微信聊天了~~~ 首先特别感谢:weidi1989分享的Android之基于百度云推送IM ,大家可以直接下载:省了很多事哈,本例中也使用了weidi的部分代码,凡是@author way的就是weidi1989的代码~~ 1.效果图 核心功能也就上面的两张图了~~~我拿着手机和模拟器

安卓开发复习笔记——Fragment+ViewPager组件(高仿微信界面)

什么是ViewPager? 关于ViewPager的介绍和使用,在之前我写过一篇相关的文章<安卓开发复习笔记——ViewPager组件(仿微信引导界面)>,不清楚的朋友可以看看,这里就不再重复. 什么是Fragment? Fragment是Android3.0后新增的概念,Fragment名为碎片,不过却和Activity十分相似,具有自己的生命周期,它是用来描述一些行为或一部分用户界面在一个Activity中,我们可以合并多个Fragment在一个单独的activity中建立多个UI面板,或

高仿精仿安卓疯狂猜图游戏源代码

给大家分享一款不错的高仿精仿安卓疯狂猜图游戏源代码,喜欢的朋友能够下载看看. 游戏 <ignore_js_op> 源代码下载 http://code.662p.com/view/2960.html 具体说明:http://android.662p.com/thread-359-1-1.html

基于Node.js+MySQL开发的开源微信小程序B2C商城(页面高仿网易严选)

高仿网易严选的微信小程序商城(微信小程序客户端) 界面高仿网易严选商城(主要是2016年wap版) 测试数据采集自网易严选商城 功能和数据库参考ecshop 服务端api基于Node.js+ThinkJS+MySQL 计划添加基于Vue.js的后台管理系统.PC版.Wap版 GitHub: https://github.com/tumobi/nideshop-mini-program 项目截图 首页 专题 分类 商品列表 商品详情 购物车 订单中心 功能列表 首页 分类首页.分类商品.新品首发.

高仿精仿安卓疯狂猜图游戏源码

给大家分享一款不错的高仿精仿安卓疯狂猜图游戏源码,喜欢的朋友可以下载看看.游戏<ignore_js_op> 源码下载 http://code.662p.com/view/2960.html 详细说明:http://android.662p.com/thread-359-1-1.html

基于百度云推送的高仿微信实时聊天Android源码

基于百度云推送的高仿微信实时聊天Android源码 使用服务:百度云推送    功能分类:社交     支持平台:Android 运行环境:Android       开发语言:Java     开发工具:Eclipse 下载地址:http://sina.lt/z84 源码简介 基于百度云推送的一款Android高仿微信的实时聊天app 运行动态图

基于vue2+nuxt构建的高仿饿了么(2018版)

前言 高仿饿了么,以nuxt作为vue的服务端渲染,适合刚接触或者准备上vue ssr的同学参考和学习 项目地址如遇网络不佳,请移步国内镜像加速节点 效果演示 查看demo请戳这里(请用chrome手机模式预览) 移动端扫描下方二维码 API接口文档 接口文档地址(基于apidoc) 技术栈 vue2 + vuex + vue-router + mint-ui + nuxt 项目运行 git clone [email protected]:EasyTuan/nuxt-elm.git # 国内镜像

基于Vue 2.0高仿 &lt;今日头条&gt; 单页应用。

这是用 vue.js 2.0 高仿 今日头条 的移动端项目,结合了原生app的部分功能以及网页版. 技术栈 vue.js 2.0全家桶(vue.vuex.vue-router) axios.jsonp element-ui.iview vue-lazyload.animate.css.moment.flexible.js 在线地址 线上地址(预览地址) GitHub源码地址 说明 项目内定死 账号: admin, 密码: admin. 因为数据原因,首页请求的数据接口来自网页版今日头条,修改了一

安卓源码_高仿茶百科

高仿茶百科                  功能分类:社交      支持平台:Android      运行环境:Eclipse   开发语言:Java     开发工具:Eclipse       源码大小:5.60MB 下载地址:http://t.cn/R7r75M2                     源码简介                                在网上看到的一个高仿茶百科的源码,仔细翻了翻,发现其中还有很对地方值得参考.特拿出来和大家分享一下. 源码运行