周记 | 2019.1.14-2019.1.18

前言:
  这周主要还是以学习、看代码、看书为主,就着《Android开发艺术探索》和《Effective Java》下饭,学习了一波关于注解的知识 ,以及主要使用注解加快开发速度的ButterKnife,另外写了几个简单的Demo。也算是回顾了之前所落下的安卓知识了,算是快速充电的一周,另外LeetCode也在刷刷刷。
以下是本周的主要学习记录点:

  • ButterKnife的使用
  • Android Context 的理解
  • Android手机验证码登录的实现
  • Android关于使用相机的点点滴滴
  • Android第三方登录的实现(未实现!要钱!)
  • SharedPreference的应用(登录维持)
  • 刷Leetcode
  • RadioGroup+Fragment实现带底部导航栏的界面切换(回顾:之前项目使用的是BottomBar实现)
  • RxJava从放弃到开始

坑有点多我们慢慢的一个个填

  • ButterKnife的使用

 ButterKnife是一个Android系统的View注入框架,以前总是要写很多findViewById来找到View对象,有了ButterKnife可以很轻松的省去这些步骤。。最重要的一点,使用ButterKnife对性能基本没有损失,因为ButterKnife用到的注解并不是在运行时反射的,而是在编译的时候生成新的class。项目集成起来也是特别方便,使用起来也是特别简单。
 ButterKnife项目地址:https://github.com/JakeWharton/butterknife

▲ ButterKnife的优势:

1、非常强大的View绑定和OnClick绑定,让你的代码瞬间整洁好多好多,再也看不见那么多的btn_function = (Button)findViewById(R.id.btn_func)了!简化代码也算是解放程序员了嘛,还提高了开发效率,何乐而不为呢。

改造后:

    // View绑定
    @BindView(R.id.pi_image)
    public CircleImageView mHeader_iv;
    @BindView(R.id.btn_personal_info_modify_image)
    public Button btn_personal_info_modify_image;
    @BindView(R.id.pi_name)
    public LinearLayout piLl_name;
    @BindView(R.id.pi_league_num)
    public LinearLayout piLl_league_num;
    @BindView(R.id.pi_gender)
    public LinearLayout piLl_gender;
...
    //OnClick绑定
    @OnClick(R.id.pi_name)
    public void pill_name_OnClick(){
        Toast.makeText(this,"点击",Toast.LENGTH_SHORT).show();
        startActivity(new Intent(personalInfo_Activity.this,pi_modifyName_Activity.class));
        Log.i(LOGTAG, "点击");
    }

2、方便的处理Adapter里的ViewHolder绑定问题

3、运行时不会影响APP效率,使用配置方便

4、代码清晰,可读性强

▲ 配置方式
在build.gradle中配置如下:

dependencies {
   ...
      implementation 'com.jakewharton:butterknife:8.8.1'
      annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}

其实最新的butterknife版本已经更新到

implementation 'com.jakewharton:butterknife:10.0.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.0.0'

但是呢,当我使用10.0.0的时候,发现了AndroidSupport与Androidx的冲突,报了如下错误:

anifest merger failed : Attribute application@appComponentFactory value=(android.support.v4.app.CoreComponentFactory) from [com.android.support:support-compat:28.0.0] AndroidManifest.xml:22:18-91
    is also present at [androidx.core:core:1.0.0] AndroidManifest.xml:22:18-86 value=(androidx.core.app.CoreComponentFactory).
    Suggestion: add 'tools:replace="android:appComponentFactory"' to <application> element at AndroidManifest.xml:36:5-364:19 to override.

按照Androidx和Android support库共存问题解决文章中的两种解决方式,第一是将build tools更新到3.2.0,gradle更新到4.6,依赖库统一更新到28.0.0,再把项目迁移至Androidx,具体操作看前提链接。第二是回退butterKnife版本,我将版本回退至8.8.1后使用正常。

▲使用体会:

1、在Activity 类中绑定 :ButterKnife.bind(this);必须在setContentView();之后绑定;且父类bind绑定后,子类不需要再bind。
2、在非Activity 类(eg:Fragment、ViewHold)中绑定: ButterKnife.bind(this,view);这里的this不能替换成getActivity()。
3、在Activity中不需要做解绑操作,在Fragment 中必须在onDestroyView()中做解绑操作。
4、使用ButterKnife修饰的方法和控件,不能用private or static 修饰,否则会报错。错误: @BindView fields must not be private or static.

▲主要使用:

@BindView—->绑定一个view;id为一个view 变量
@BindViews —-> 绑定多个view;id为一个view的list变量
@BindArray—-> 绑定string里面array数组;@BindArray(R.array.city ) String[] citys ;
@BindBitmap—->绑定图片资源为Bitmap;@BindBitmap( R.mipmap.wifi ) Bitmap bitmap;
@BindBool —->绑定boolean值
@BindColor —->绑定color;@BindColor(R.color.colorAccent) int black;
@BindDimen —->绑定Dimen;@BindDimen(R.dimen.borth_width) int mBorderWidth;
@BindDrawable —-> 绑定Drawable;@BindDrawable(R.drawable.test_pic) Drawable mTestPic;
@BindFloat —->绑定float
@BindInt —->绑定int
@BindString —->绑定一个String id为一个String变量;@BindString( R.string.app_name ) String meg;
@OnClick—->点击事件
@OnCheckedChanged —->选中,取消选中
@OnEditorAction —->软键盘的功能键
@OnFocusChange —->焦点改变
@OnItemClick item—->被点击(注意这里有坑,如果item里面有Button等这些有点击的控件事件的,需要设置这些控件属性focusable为false)
@OnItemLongClick item—->长按(返回真可以拦截onItemClick)
@OnItemSelected —->item被选择事件
@OnLongClick —->长按事件
@OnPageChange —->页面改变事件
@OnTextChanged —->EditText里面的文本变化事件
@OnTouch —->触摸事件
@Optional —->选择性注入,如果当前对象不存在,就会抛出一个异常,为了压制这个异常,可以在变量或者方法上加入一下注解,让注入变成选择性的,如果目标View存在,则注入, 不存在,则什么事情都不做

 我们一般将Context理解为“上下文”或者“场景”,如果你仍然觉得很抽象,不好理解。在这里我给出一个可能不是很恰当的比喻,希望有助于大家的理解:一个Android应用程序,可以理解为一部电影或者一部电视剧,Activity,Service,Broadcast Receiver,Content Provider这四大组件就好比是这部戏里的四个主角:胡歌,霍建华,诗诗,Baby。他们是由剧组(系统)一开始就定好了的,整部戏就是由这四位主演领衔担纲的,所以这四位主角并不是大街上随随便便拉个人(new 一个对象)都能演的。有了演员当然也得有摄像机拍摄啊,他们必须通过镜头(Context)才能将戏传递给观众,这也就正对应说四大组件(四位主角)必须工作在Context环境下(摄像机镜头)。那Button,TextView,LinearLayout这些控件呢,就好比是这部戏里的配角或者说群众演员,他们显然没有这么重用,随便一个路人甲路人乙都能演(可以new一个对象),但是他们也必须要面对镜头(工作在Context环境下),所以Button mButton=new Button(Context)是可以的。
 在应用程序中Context的具体实现子类就是:Activity,Service,Application。那么Context数量=Activity数量+Service数量+1。

Context作用域

正确使用Context
 一般Context造成的内存泄漏,几乎都是当Context销毁的时候,却因为被引用导致销毁失败,而Application的Context对象可以理解为随着进程存在的,所以我们总结出-使用Context的正确姿势:
1.当Application的Context能搞定的情况下,并且生命周期长的对象,优先使用Application的Context。
2.不要让生命周期长于Activity的对象持有到Activity的引用。
3.尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。

  • Android手机验证码登录的实现

login_Activity.java

public class login_Activity extends AppCompatActivity  {

    @BindView(R.id.tv_contract)
    public TextView tv_contract;
    @BindView(R.id.ev_phone_Number)
    public EditText ev_phone_Number;
    @BindView(R.id.ev_validator)
    public EditText ev_validator;
    @BindView(R.id.btn_login_confirm)
    public Button btn_login_confirm;
    @BindView(R.id.btn_get_validater)
    public Button btn_get_validater;

    private TimeCount time_count;
    private EventHandler eh; //事件接收器
    private SpUtil sp;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.login_activity);

        ButterKnife.bind(this);

        time_count = new TimeCount(60000, 1000); //设置验证码定时器
        sp = new SpUtil(this);
        tv_contractClick(); //点击服务协议和隐私政策的响应
        Handler_Validator_Event(); //处理点击确定按钮后的验证码回调工作
    }

    /**
     * 对同意协议和隐私政策进行特殊处理,使其可点击并处理相应逻辑
     */
    private void tv_contractClick() {
        final SpannableStringBuilder style = new SpannableStringBuilder();
        style.append("在点击“继续”时,我已同意服务协议和隐私政策");

        ClickableSpan clickableSpan = new ClickableSpan() {
            @Override
            public void onClick(View widget) {
                Toast.makeText(login_Activity.this, "触发点击事件:服务协议!", Toast.LENGTH_SHORT).show();
            }
        };

        ClickableSpan clickableSpan2 = new ClickableSpan() {
            @Override
            public void onClick(View widget) {
                Toast.makeText(login_Activity.this, "触发点击事件:隐私政策!", Toast.LENGTH_SHORT).show();
            }
        };

        style.setSpan(clickableSpan, 13, 17, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        style.setSpan(clickableSpan2,18,22,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        tv_contract.setText(style);

        //设置部分文字颜色
        ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(Color.parseColor("#40a9ff"));
        ForegroundColorSpan foregroundColorSpan2 = new ForegroundColorSpan(Color.parseColor("#40a9ff"));
        //设置需要特殊处理的文字
        style.setSpan(foregroundColorSpan, 13, 17, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        style.setSpan(foregroundColorSpan2, 18, 22, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        //配置给TextView
        tv_contract.setMovementMethod(LinkMovementMethod.getInstance());
        tv_contract.setText(style);
    }

    private void bindview() {

        time_count = new TimeCount(60000, 1000);
    }
    @OnClick(R.id.btn_login_confirm)
    public void btn_login_confirm_OnClick(){
      if (!ev_phone_Number.getText().toString().trim().equals("")) {
        if (Textutil.checkTel(ev_phone_Number.getText().toString().trim())) {
            if (!ev_validator.getText().toString().trim().equals("")) {
                SMSSDK.submitVerificationCode("+86",ev_phone_Number.getText().toString().trim(),ev_validator.getText().toString().trim());//提交验证
                Log.e("SMSSDK",ev_validator.getText().toString().trim());
            }else{
                Toast.makeText(login_Activity.this, "请输入验证码", Toast.LENGTH_SHORT).show();
            }
        }else{
            Toast.makeText(login_Activity.this, "请输入正确的手机号码", Toast.LENGTH_SHORT).show();
        }
    }else{
        Toast.makeText(login_Activity.this, "请输入手机号码", Toast.LENGTH_SHORT).show();
    }
}

    @OnClick(R.id.btn_get_validater)
    public void btn_get_validater_OnClick(){
        //SMSSDK.getSupportedCountries();//获取短信目前支持的国家列表
        if(!ev_phone_Number.getText().toString().trim().equals("")){
            if (Textutil.checkTel(ev_phone_Number.getText().toString().trim())) {
                SMSSDK.getVerificationCode("+86",ev_phone_Number.getText().toString());//获取验证码
                time_count.start();
            }else{
                Toast.makeText(login_Activity.this, "请输入正确的手机号码", Toast.LENGTH_SHORT).show();
            }
        }else{
            Toast.makeText(login_Activity.this, "请输入手机号码", Toast.LENGTH_SHORT).show();
        }
    }


    /**
     * 倒数计数器内部类
     */
    class TimeCount extends CountDownTimer {

        public TimeCount(long millisInFuture, long countDownInterval) {
            super(millisInFuture, countDownInterval);
        }
        /**
         * 每一次ontick都会更新UI
         * @param l
         */
        @Override
        public void onTick(long l) {
            btn_get_validater.setClickable(false);
            btn_get_validater.setText(l/1000 + "秒后重新获取");
        }

        /**
         * 当倒数计时器完成时回到获取验证码
         */
        @Override
        public void onFinish() {
            btn_get_validater.setClickable(true);
            btn_get_validater.setText("获取验证码");
        }
    }

    /**
     * 初始化事件接收器
     */
    private void Handler_Validator_Event(){
        eh = new EventHandler(){
            @Override
            public void afterEvent(int event, int result, Object data) {

                if (result == SMSSDK.RESULT_COMPLETE) { //回调完成

                    if (event == SMSSDK.EVENT_SUBMIT_VERIFICATION_CODE) { //提交验证码成功
                        Log.e("SMSSDK","verifyOK");
                        /**
                         * 这里应该是一个okhttp3请求,和服务器交互,并将用户名和头像信息写入sharePreference进行持久化
                         */
//                        String url = "http://这里写的是接口地址(具体接收格式要看后台怎么给)";
//                        Map<String, String> params = new HashMap<String, String>();
//                        params.put("number",ev_phone_Number.getText().toString().trim() );
//                        OkHttpUtil.postAsync(url, params, new OkHttpUtil.DataCallBack() {
//                            @Override
//                            public void requestFailure(Request request, IOException e) {
//                                Log.i("请求失败", "请求失败" + request.toString() + e.toString());
//                            }
//                            @Override
//                            public void requestSuccess(String result) throws Exception {
//                                Toast.makeText(login_Activity.this,"请求成功",Toast.LENGTH_SHORT).show();
//                                Log.i("请求成功", result);
//                            }
//                        });
                    } else if (event == SMSSDK.EVENT_GET_VERIFICATION_CODE){ //获取验证码成功


                    } else if (event ==SMSSDK.EVENT_GET_SUPPORTED_COUNTRIES){ //返回支持发送验证码的国家列表

                    }
                } else{
                    sp.saveString("username",ev_phone_Number.getText().toString().trim());
                    sp.saveString("username","机器人");
                    Log.e("SMSSDK","verifyERROR");
                    Looper.prepare();
                    Toast.makeText(login_Activity.this, "验证码输入错误,请重试", Toast.LENGTH_SHORT).show();
                    startActivity(new Intent(login_Activity.this, main_Activity.class)); //页面跳转
                    Looper.loop();
                    ((Throwable)data).printStackTrace();
                }
            }
        };
        SMSSDK.registerEventHandler(eh); //注册短信回调
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();
        SMSSDK.unregisterEventHandler(eh); //取消短信回调,销毁
    }
}

login_activity.xml

<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background ="#FFFFFF">
    <TextView
            android:id="@+id/tv_Reg_Login"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/reg_login"
            android:textSize="25sp"
            android:textColor="@color/lightDark"
            android:textStyle="bold"
            android:layout_marginTop="30dp"
            android:layout_marginLeft="20dp"
    />

    <LinearLayout
            android:layout_marginTop="30dp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/tv_Reg_Login"
            android:id ="@+id/ll_phone"
            android:orientation="vertical"
    >
        <TextView android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:layout_marginLeft = "20dp"
                  android:layout_marginTop="5dp"
                  android:text="@string/tv_phone"
                  android:textColor="@color/Dark"
                  android:textSize="14sp"/>
    <LinearLayout

            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
    >
        <EditText android:layout_width="280dp"
                  android:layout_marginTop="10dp"
                  android:layout_marginLeft="16dp"
                  android:layout_height="50dp"
                  android:id="@+id/ev_phone_Number"
                  android:hint = "@string/phone_hint"
                  android:inputType="number"
                  android:textSize="15sp"/>
        <Button
                android:id="@+id/btn_get_validater"
                android:layout_width="wrap_content"
                android:layout_height="30dp"
                android:background="@color/lightGray"
                android:textColor="@color/gray"
                android:layout_marginTop="5dp"
                android:layout_marginRight="10dp"
                android:textSize="10sp"
                android:text="@string/btn_get_verify"
        />
    </LinearLayout>


    </LinearLayout>

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id ="@+id/ll_verify"
            android:layout_below="@id/ll_phone"
            android:orientation="vertical"
    >
        <TextView android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:layout_marginLeft = "20dp"
                  android:layout_marginTop="5dp"
                  android:text="@string/tv_verify"
                  android:textColor="@color/Dark"
                  android:textSize="14sp"/>
        <EditText android:layout_width="match_parent"
                  android:layout_marginLeft="16dp"
                  android:layout_marginTop="10dp"
                  android:id="@+id/ev_validator"
                  android:layout_height="50dp"
                  android:hint = "@string/verify_hint"
                  android:textSize="15sp"/>

    </LinearLayout>

    <LinearLayout android:layout_width="match_parent"
                  android:layout_height="wrap_content"
                  android:layout_below="@id/ll_verify"
                  android:gravity="center"
                  android:layout_marginTop="30dp"
                  android:id="@+id/btn_login"
    >
        <Button android:layout_width="300dp"
                android:layout_height="40dp"
                android:background="@color/btn_blue"
                android:text="@string/btn_continue"
                android:id="@+id/btn_login_confirm"
                android:textColor = "#fff"

        />
    </LinearLayout>
    <TextView android:layout_width="match_parent"
              android:layout_height="30dp"
              android:layout_below="@id/btn_login"
              android:text="@string/tv_contract"
              android:id="@+id/tv_contract"
              android:layout_marginLeft="35dp"
              android:layout_marginTop="8dp"
              android:textSize="10sp"
    />

    <LinearLayout
            android:layout_below="@id/btn_login"
            android:layout_marginTop="140dp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
        <ImageView
                android:layout_marginLeft="85dp"
                android:layout_width="36dp"
                android:layout_height="36dp"
                android:background="@mipmap/iv_qq"/>
        <TextView
                android:id="@+id/tv_qq_login"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center"
                android:layout_marginLeft="5dp"
                android:text="@string/tv_qq_login"
                android:textColor="#272727"/>

        <ImageView
                android:layout_marginLeft="30dp"
                android:layout_width="36dp"
                android:layout_height="34dp"
                android:background="@mipmap/iv_wechat"/>

        <TextView
                android:id="@+id/tv_weixin_login"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center"
                android:layout_marginLeft="5dp"
                android:text="@string/tv_wechat_login"
                android:textColor="#272727"/>

    </LinearLayout>
</RelativeLayout>

工具类-正则匹配手机号

public class Textutil {
    /**
     * 正则匹配手机号码
     * @param tel
     * @return
     */
    public static boolean checkTel(String tel){
        Pattern p = Pattern.compile("^[1][3,4,5,7,8][0-9]{9}$");
        Matcher matcher = p.matcher(tel);
        return matcher.matches();
    }

}

要注意哦,未成为开发者之前每台手机每天只能接受10封短信。主要是在AfterEvent方法的重写去实现验证码验证后回调的逻辑。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容

  • afinalAfinal是一个android的ioc,orm框架 https://github.com/yangf...
    passiontim阅读 15,366评论 2 44
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,331评论 0 17
  • 详情页面 packagecom.example.shoppingcar; importandroid.conten...
    ForCrazyLove阅读 553评论 0 2
  • 在我身外那么广阔 你没有理由眷落在我的身边 上天赋予你们一叶羽翅 你就必须要作一次飞翔 你应该选择一个阳光明媚的日...
    乡村诗人阅读 360评论 0 7
  • 推荐阅读PHP 百万级数据导出方案(多 CSV 文件压缩)php有自带函数fputcsv,fgetcsv是可以行操...
    云龙789阅读 1,177评论 0 0