Android Studio中build.gradle文件详解

 更新时间:2016年9月20日 19:54  点击:2145
build.gradle模板文件的配置我相信做安卓开发的朋友肯定都知道它的使用了,我们通常要使用它来做项目打包等等操作了,下面一起来看build.gradle文件详解

首先,在Android studio的一个工程里,在Gradle Scripts目录下,有一个文件build.gradle(Module:app),打开。这里我随便选了一个工程的build.gradle文件做例子:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 21
    buildToolsVersion "21.1.2"

    defaultConfig {
        minSdkVersion 14
        targetSdkVersion 21
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile project(':xDroid')
    compile project(':UniversalImageLoader')
    compile project(':xdroidrequest')
    compile project ( ':library')
}

compileSdkVersion 21,说明要运行该源码,你必选已经安装了android API 21。
buildToolsVersion 21.1.2 说明要运行该源码,你必须已经安装了 android sdk build-tools 21.1.2。
minSdkVerison 表示向下低至android API 14,即androd 4.0和4.0以上的版本都可以运行该工程。
targetSdkVerision 表示采用的目标android API是 API 21即 android 5.0。
下面的dependencies里指明的就是一些需要用到的第三方库。

ndbase是为Android开发者量身打造的一款开源类库产品,本文我们介绍使用AndBase框架实现无参Http Get请求,有参Http Post请求,有参Http Get请求。

AndBase框架为我们提供了一些相关的方法提供给我们使用,用来完成Http网络请求...总体就是对Http请求的一个封装,不过个人认为,网络请求这一模块更加推荐使用Volley框架..楼主对比了两个框架中的源码...Volley更多的地方是使用抽象方法封装在接口内,然后对外暴露接口,其他类在实现接口的同时需要实现内部的抽象方法...而AndBase则是使用继承的方式..继承父类..实现类通过重写的方式对封装的方法重写从而进行下一步的操作...

相比二者网络请求的源码,Volley源码的书写还是更胜一筹...Volley是Google推出的,针对的也仅仅是网络请求这一模块...同样AndBase也是非常优秀的,是国内牛人写的一款重量级框架,涉及的模块非常的广泛,还是非常好用的...

1.使用AndBase框架实现无参Http Get请求...

一般普通的网络请求如果不涉及到数据信息的变化,也就是不涉及一些安全性问题,都可以使用Get方式来完成网络请求...Get请求也是分为有参和无参的,给我的感觉有参一般可以用于向服务器上传资源数据...先介绍一下无参的Get请求...还是先从源码的地方来看看...

源码的调用方式是先使用AbHttpUtils.get()函数调用...不过这无关紧要...通过这个方法走向AbHttpClient类内部...执行下面这段源码...无论是有参还是无参..都会调用这个方法..无参的时候第二个参数传递null就行了...


public void get(final String url,final AbRequestParams params,final AbHttpResponseListener responseListener) {
        
        responseListener.setHandler(new ResponderHandler(responseListener));
        executorService.submit(new Runnable() {
            public void run() {
                try {
                    doGet(url,params,responseListener);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }                 
        });                
    }



我们可以看到,这段函数首先通过Handler发送Message...同时开启一个线程池,来提交当前的请求...最后将执行doGet()方法...同时Handler一直对responseListener的消息进行处理..doGet()方法的源码过程如下...


private void doGet(String url,AbRequestParams params,AbHttpResponseListener responseListener){
          try {
              
              responseListener.sendStartMessage();
              
              if(!debug && !AbAppUtil.isNetworkAvailable(mContext)){
                    responseListener.sendFailureMessage(AbConstant.CONNECT_FAILURE_CODE,AbConstant.CONNECTEXCEPTION, new AbAppException(AbConstant.CONNECTEXCEPTION));
                    return;
              }
              
              //HttpGet连接对象  
              if(params!=null){
                  url += params.getParamString(); //如果有参,那么获取相关参数...
              }
              HttpGet httpRequest = new HttpGet(url); //定义连接对象..
              
              BasicHttpParams httpParams = new BasicHttpParams();
              
              // 从连接池中取连接的超时时间,设置为1秒
              ConnManagerParams.setTimeout(httpParams, DEFAULT_SOCKET_TIMEOUT);
              ConnManagerParams.setMaxConnectionsPerRoute(httpParams, new ConnPerRouteBean(DEFAULT_MAX_CONNECTIONS));
              ConnManagerParams.setMaxTotalConnections(httpParams, DEFAULT_MAX_CONNECTIONS);
              // 读响应数据的超时时间
              HttpConnectionParams.setSoTimeout(httpParams, DEFAULT_SOCKET_TIMEOUT);
              HttpConnectionParams.setConnectionTimeout(httpParams, DEFAULT_SOCKET_TIMEOUT);
              HttpConnectionParams.setTcpNoDelay(httpParams, true);
              HttpConnectionParams.setSocketBufferSize(httpParams, DEFAULT_SOCKET_BUFFER_SIZE);
              //设置协议版本...
              HttpProtocolParams.setVersion(httpParams, HttpVersion.HTTP_1_1);
              HttpProtocolParams.setUserAgent(httpParams, String.format("andbase-http/%s (http://www.418log.org/)", 1.0));
              // 设置请求参数
              httpRequest.setParams(httpParams);
              
              //取得HttpClient对象  
              HttpClient httpClient = new DefaultHttpClient();  
              //请求HttpClient,取得HttpResponse  
              HttpResponse httpResponse = httpClient.execute(httpRequest);  
              //请求成功  
              int statusCode = httpResponse.getStatusLine().getStatusCode();
              
              //取得返回的字符串  
              HttpEntity  mHttpEntity = httpResponse.getEntity();
              if (statusCode == HttpStatus.SC_OK){  
                  if(responseListener instanceof AbStringHttpResponseListener){
                      String content = EntityUtils.toString(mHttpEntity);
                      ((AbStringHttpResponseListener)responseListener).sendSuccessMessage(statusCode, content);
                  }else if(responseListener instanceof AbBinaryHttpResponseListener){
                      readResponseData(mHttpEntity,((AbBinaryHttpResponseListener)responseListener));
                  }else if(responseListener instanceof AbFileHttpResponseListener){
                      //获取文件名
                      String fileName = AbFileUtil.getFileNameFromUrl(url, httpResponse);
                      writeResponseData(mHttpEntity,fileName,((AbFileHttpResponseListener)responseListener));
                  }
              }else{
                  String content = EntityUtils.toString(mHttpEntity);
                  responseListener.sendFailureMessage(statusCode, content, new AbAppException(AbConstant.UNKNOWNHOSTEXCEPTION));
              }
        } catch (Exception e) {
            e.printStackTrace();
            //发送失败消息
            responseListener.sendFailureMessage(AbConstant.UNTREATED_CODE,e.getMessage(),new AbAppException(e));
        }finally{
            responseListener.sendFinishMessage();
        }
    }
    



有了上面的源码调用过程其实就非常的清晰了..

无论是doGet()方法还是doPost()方法模式基本是相同的,都是需要先建立一个连接对象,HttpGet或HttpPost..不同之处在于有参的Get请求直接将params加入到url后面即可,而Post请求需要获取实体数据..在实体数据中加入我们传递的params..设置连接过程和读取数据过程中的相关参数,比如说超时的时间,使用的Http版本,设置UserAgent等等...设置完之后执行请求获取响应了...

中间涉及到了一个判断的过程..判断返回的响应数据到底属于什么类型的数据,是基本的String类型,还是与图片或者视频相关的Byte类型,还是与文件相关的File类型...通过对相关类型的判断,执行不同的方法,虽然方法不相同,但是最后的目的是一样的,都是把实体数据进行封装...封装完毕后调用sendSuccessMessage然后Handler自动回去处理Message...最后调用OnSuccess方法..将数据返回给客户端..

还是看一下实际的调用过程...

无参的Get请求调度...这里需要设置相应监听...

public void FileClick(View v){
    url="http://192.168.199.172:8080/JSP/imageview.jpg";
    getView();
    httpUtil.get(url, new FileResponseListener(this, this, v,max_tv,num_tv,progressBar));
}

GetResponseListener.java

对响应的监听的一个重写过程...通过为请求设置上url+相关监听就能够完成网络请求,并对请求数据进行相关处理了...这里完成了一个图片数据的下载,然后通过对数据进行封装,就成了一个Bitmap..这样就能够在控件上进行显示了..


package com.example.andbasehttp;

import java.io.File;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.graphics.Bitmap;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.ab.activity.AbActivity;
import com.ab.http.AbFileHttpResponseListener;
import com.ab.util.AbFileUtil;
import com.ab.view.progress.AbHorizontalProgressBar;

public class FileResponseListener extends AbFileHttpResponseListener{

    
    private int max=100;
    private int progress=0;
    private AbActivity activity;
    private Context context;
    private AlertDialog dialog;
    private View view;
    private TextView max_tv,num_tv;
    private AbHorizontalProgressBar progressBar;
    
    public FileResponseListener(AbActivity activity,Context context,View v,TextView v1,TextView v2, AbHorizontalProgressBar progressBar ){
        this.activity=activity;
        this.context=context;
        this.view=v;
        this.max_tv=v1;
        this.num_tv=v2;
        this.progressBar=progressBar;
    }
    
    @Override
    public void onSuccess(int statusCode, File file){
        Bitmap bitmap=AbFileUtil.getBitmapFromSD(file);
        ImageView view=new ImageView(context);
        view.setImageBitmap(bitmap);
        activity.showDialog("返回结果", view, new OnClickListener() {
            
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // TODO Auto-generated method stub
                
            }
        });
    }
    
    @Override
    public void onFailure(int statusCode, String content,Throwable error){
        activity.showToast(error.toString());
    }
    
    @Override
    public void onStart(){
        max_tv.setText(progress+"/"+String.valueOf(max));
        progressBar.setMax(max);
        progressBar.setProgress(progress);
        dialog=activity.showDialog("正在下载", view);
    }
    
    @Override
    public void onProgress(int bytesWritten, int totalSize){
        max_tv.setText(bytesWritten/(totalSize/max)+"/"+max);
        progressBar.setProgress(bytesWritten/(totalSize/max));
    }
    
    @Override
    public void onFinish(){
        dialog.cancel();
        dialog=null;
    }
}



2.使用AndBase框架实现有参Http Post请求...

其实调用的方式都是相同的,,只不过Post请求需要传递相关的参数...使用有参的Post请求...这里是向一个JSP传递相关参数来完成数据信息的验证...


public void PostClick(View v){
    url="http://192.168.199.172:8080/JSP/post.jsp";
    params=new AbRequestParams();
    params.put("name", "darker");
    params.put("password", "49681888");
    httpUtil.post(url, params, new PostResponseListener(this));
}
    



这里我就不粘贴PostResponseListener的代码了...贴一下JSP页面的代码..相关的JSP代码如下...这里的JSP代码非常的简单..并且前面在使用Volley的时候也使用过..JSP页面我们完全可以自己书写的更加复杂一些,那么就能够实现更多的功能...


<%
  String name=request.getParameter("name");
  String password=request.getParameter("password");
  if("darker".equals(name)&& "49681888".equals(password)){
      out.println("Receive name is:"+name);
    out.println("Receive password is:"+password);%>
    Your Message are right!
  <%}else{
      out.println("Receive name is:"+name);
    out.println("Receive password is:"+password);%>
    Your Message are wrong!
  <%}%>  



3.使用AndBase框架实现有参Http Get请求...

有参的Get请求一般用于文件,数据资源的上传...将上传的资源以及名称作为参数传递给服务器..这里不涉及安全上的问题..因此可以使用带有参数的Get请求...这里向服务器上传文件..需要添加相关参数...

public void FileLoadClick(View v){
        url="http://192.168.199.172:8080";
        AbRequestParams params = new AbRequestParams();
        File pathRoot = Environment.getExternalStorageDirectory();
        String path = pathRoot.getAbsolutePath();
        File file1 = new File(path+"/download/cache_files/aa.txt");
        params.put(file1.getName(),file1);
        
        getView();
        httpUtil.get(url, params, new FileSendResponseListener(this, this, v, max_tv, num_tv, progressBar));
    }


这里的监听事件简单的粘贴一下...监听事件之所以传递控件..是为了更好的向用户进行展示...这里设置了一个进度条的方式,来贯穿整个请求——响应的过程...如果下载或者是上传的文件和资源过多...我们是必须通知用户相关进度的..总不能一直卡死在界面上..这样用户也无法知道到底是否完成了数据的上传或者是下载...


package com.example.andbasehttp;



import android.app.AlertDialog;
import android.content.Context;
import android.view.View;
import android.widget.TextView;
import com.ab.activity.AbActivity;
import com.ab.http.AbStringHttpResponseListener;
import com.ab.view.progress.AbHorizontalProgressBar;

public class FileSendResponseListener extends AbStringHttpResponseListener{

    private int max=100;
    private int progress=0;
    private AbActivity activity;
    private Context context;
    private AlertDialog dialog;
    private View view;
    private TextView max_tv,num_tv;
    private AbHorizontalProgressBar progressBar;
    
    public FileSendResponseListener(AbActivity activity,Context context,View v,TextView v1,TextView v2, AbHorizontalProgressBar progressBar ){
        this.activity=activity;
        this.context=context;
        this.view=v;
        this.max_tv=v1;
        this.num_tv=v2;
        this.progressBar=progressBar;
    }
    
    @Override
    public void onSuccess(int statusCode, String content){
        activity.showToast("OnSuccess");
        System.out.println(content);
    }
    
    @Override
    public void onFailure(int statusCode, String content,Throwable error){
        activity.showToast(error.toString());
    }
    
    @Override
    public void onStart(){
        max_tv.setText(progress+"/"+String.valueOf(max));
        progressBar.setMax(max);
        progressBar.setProgress(progress);
        activity.showToast("正在下载");
        dialog=activity.showDialog("正在下载", view);
    }
    
    @Override
    public void onProgress(int bytesWritten, int totalSize){
        max_tv.setText(bytesWritten/(totalSize/max)+"/"+max);
        progressBar.setProgress(bytesWritten/(totalSize/max));
    }
    
    @Override
    public void onFinish(){
        dialog.cancel();
        dialog=null;
    }
}



涉及到的类为com.ab.http保内的所有类...

1.AbStringHttpResponseListener.java
2.AbBinaryHttpResponseListener.java
3.AbFileHttpResponseListener.java

这三个类是对AbHttpResponseListener.java的一个继承...继承了其内部的一些相关方法..包括请求开始,结束,失败等等函数...

AbHttpClient.java就是用来完成请求——连接过程的实现...其中还包含数据的封装..

AbHttpUtils.java则是对post,get等方法调用的一个中间层...

AbRequestParams.java 则是对请求参数处理的一个类...不仅包含对请求参数的处理,还包含对实体的创建..为实体添加相关参数等方法的实现过程...


Android Http Get和Post请求

通过HttpGet和HttpPost向服务器提交请求,并从服务器返回结果信息。通过如下3步访问Http资源。

(1)创建HttpGet或者HttpPost对象,将要请求的URL通过构造方法传入HttpGet或HttpPost对象。
(2)使用DefaultHttpClient.execute方法发送Http Get或Http Post请求,并返回HttpResponse对象。
(3)通过HttpResponse.getEntity方法返回响应信息,并进行相应的处理。

如果使用HttpPost方法提交Http Post请求,还需要使用HttpPost.setEntity方法设置请求参数。


实例代码如下:

package mobile.android.ch13.httpgetpost;

import java.util.ArrayList;
import java.util.List;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

public class Main extends Activity implements OnClickListener
{

    @Override
    public void onClick(View view)
    {
        String url = "http://172.22.20.194:8080/querybooks/QueryServlet";
        TextView tvQueryResult = (TextView) findViewById(R.id.tvQueryResult);
        EditText etBookName = (EditText) findViewById(R.id.etBookName);
        HttpResponse httpResponse = null;
        try
        {
            
            switch (view.getId())
            {
                case R.id.btnGetQuery:
                    url += "?bookname=" + etBookName.getText().toString();
                    HttpGet httpGet = new HttpGet(url);
                    httpResponse = new DefaultHttpClient().execute(httpGet);
                    if (httpResponse.getStatusLine().getStatusCode() == 200)
                    {
                        
                        String result = EntityUtils.toString(httpResponse
                                .getEntity());
                        tvQueryResult.setText(result.replaceAll("\r", ""));
                    }
                    break;

                case R.id.btnPostQuery:
                    HttpPost httpPost = new HttpPost(url);
                    List<NameValuePair> params = new ArrayList<NameValuePair>();
                    params.add(new BasicNameValuePair("bookname", etBookName
                            .getText().toString()));
                    httpPost.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));
                    
                    httpResponse = new DefaultHttpClient().execute(httpPost);
                    if (httpResponse.getStatusLine().getStatusCode() == 200)
                    {
                        String result = EntityUtils.toString(httpResponse
                                .getEntity());
                        tvQueryResult.setText(result.replaceAll("\r", ""));
                    }
                    break;
            }
        }
        catch (Exception e)
        {
            tvQueryResult.setText(e.getMessage());
        }

    }

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Button btnGetQuery = (Button) findViewById(R.id.btnGetQuery);
        Button btnPostQuery = (Button) findViewById(R.id.btnPostQuery);
        btnGetQuery.setOnClickListener(this);
        btnPostQuery.setOnClickListener(this);
        
    }
}

RecyclerView 是 android-support-v7-21 版本中新增的一个 Widgets, 是 ListView 的升级版本,更加先进和灵活。

Android RecyclerView 是Android5.0推出来的,导入support-v7包即可使用。

个人体验来说,RecyclerView绝对是一款功能强大的控件。

首先总结下RecyclerView的特点:

1.支持不同方向,不同排版模式,实现多种展现数据的形式,涵盖了ListView,GridView,瀑布流等数据表现的形式

2.内部实现了回收机制,无需我们考虑View的复用情况

3.取消了onItemClick等点击事件,需要自己手动去写

------------------------------------------------------------------------------------

那么让我们通过一些Demo来了解RecyclerView的基本使用

 

首先,要导入support-v7 包

import android.support.v7.widget.RecyclerView;

RecyclerView和ListView的使用一样,都需要有对应的Adapter,列表项布局,数据源

1.先写主Activity布局

可以看到RecyclerView的标签

<android.support.v7.widget.RecyclerView>

 
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical"
              tools:context="com.xqx.superapp.app.Android5Activity">

    <Button
            android:text="添加一个数据"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="btnAddItem"
            />
    <Button
            android:text="删除第一个"
            android:onClick="btnRemoveItem"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycle_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
    </android.support.v7.widget.RecyclerView>

</LinearLayout> 

菜单项布局,标准的上面图片,下面文字

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:gravity="center"
              android:layout_width="match_parent"
              android:layout_height="match_parent">
        <ImageView
                android:id="@+id/item_icon"
                android:src="@mipmap/machao_moqi"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>
        <TextView
                android:id="@+id/item_title"
                android:text="名称"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                />
</LinearLayout> 

2.接下来就看Activity代码了

首先看成员变量,与ListView,GridView一样 标准三样, 控件,数据源,适配器

private List<String> data;          
private RecyclerView recyclerView;
private MyRecycleAdapter adapter;   //自定义适配器,继承RecyclerView.Adapter


接着我们必须要自定义一个ViewHolder,这个ViewHolder 必须要继承 RecyclerView.ViewHolder

注意RecyclerView不再提供onItemClick事件监听,所以需要我们自己手工写监听事件的方法

private static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        public ImageView imageView;
        public TextView textView;

        public ViewHolder(View itemView) {
            super(itemView);
            // 通常ViewHolder的构造,就是用于获取控件视图的
            imageView = (ImageView) itemView.findViewById(R.id.item_icon);
            textView = (TextView) itemView.findViewById(R.id.item_title);
            // TODO 后续处理点击事件的操作
            itemView.setOnClickListener(this);

        }
        @Override
        public void onClick(View v) {
            int position = getAdapterPosition();
            Context context = imageView.getContext();
            Toast.makeText(context,"显示第"+position+"个项",Toast.LENGTH_SHORT).show();
        }
    }
 

再让我们看自定义适配器,注意这里的参数是ViewHolder,这个ViewHodler是我们自己的,不要导入v7包下的ViewHolder,

之后要重写三个方法

private class MyRecycleAdapter extends RecyclerView.Adapter<ViewHolder>{
  
@Override
public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
    return null;
}

@Override
public void onBindViewHolder(ViewHolder viewHolder, int i) {

}

@Override
public int getItemCount() {
    return 0;
}

}


在自定义适配器MyRecycleAdapter中,首先要写一个构造方法,因为有数据源,所有构造方法里必然有List

private List<String> strings;
public MyRecycleAdapter(List<String> strings) {
     this.strings = strings;
}

然后就要重写三个方法了,

@Override
public int getItemCount() {
    int ret = 0;
    if (strings != null) {
        ret = strings.size();
     }
        return ret;
}

 @Override
        public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
            ViewHolder ret = null;
            // 不需要检查是否复用,因为只要进入此方法,必然没有复用
            // 因为RecyclerView 通过Holder检查复用
            View v = LayoutInflater.from(Android5Activity.this).inflate(R.layout.item_recycler, viewGroup, false);
            ret = new ViewHolder(v);
            return ret;
        }

@Override
        public void onBindViewHolder(ViewHolder viewHolder, int i) {
            // 1.这里进行图片的加载
            viewHolder.textView.setText(strings.get(i));
            int resId = R.mipmap.ic_launcher;
            int index = i%5;
            switch (index){
                case 0:
                    resId = R.mipmap.a11;
                    break;
                case 1:
                    resId = R.mipmap.a33;
                    break;
                case 2:
                    resId = R.mipmap.a22;
                    break;
            }
            viewHolder.imageView.setImageResource(resId);
        }


---------------------------------------------------------------------------------------------------------------

完成自定义适配器和自定义ViewHolder的代码 就要进行RecyclerView的使用了

首先 要了解  RecyclerView.LayoutManager 这个属性

用于进行一个布局的设置,可以设置显示模式,ListView或者GridView或者瀑布流

1.ListView显示模式

// 1.线性布局
        LinearLayoutManager layoutManager =
                new LinearLayoutManager(this,   // 上下文
                                        LinearLayout.VERTICAL,  //垂直布局,
                                        false);

                                    


2.GridView显示模式

// 2.Grid布局
        RecyclerView.LayoutManager layoutManager =
                new GridLayoutManager(this,
                                      2,  // 每行显示item项数目
                                      GridLayoutManager.HORIZONTAL, //水平排列
                                      false
                                      );


     

3.瀑布流显示模式

// 3.瀑布流
        RecyclerView.LayoutManager layoutManager =
                new StaggeredGridLayoutManager(3,  // 每行显示的item项数目
                        StaggeredGridLayoutManager.VERTICAL);  // 垂直排列

 

以上三种显示模式任意设置一种 就可以继续下面的代码

recyclerView.setLayoutManager(layoutManager);
        // 设置 RecyclerView的Adapter
        // 注意一定在设置了布局管理器之后调用
        adapter = new MyRecycleAdapter(data);
        recyclerView.setAdapter(adapter);

 

最后记得加上“添加一个数据”,“删除第一个数据”的按钮响应事件

public void btnAddItem(View view) {
        data.add(0,"Time:"+System.currentTimeMillis());
        adapter.notifyDataSetChanged();
    }



    public void btnRemoveItem(View view) {
        if (!data.isEmpty()) {
            data.remove(0);
        }
        adapter.notifyItemRemoved(0);
    }

 

完整代码:

package com.xqx.superapp.app;

import android.app.Activity;
import android.content.Context;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.util.Log;
import android.view.*;
import android.widget.*;

import java.util.LinkedList;
import java.util.List;


public class Android5Activity extends Activity {

    private List<String> data;
    private RecyclerView recyclerView;
    private MyRecycleAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_android5);
        data = new LinkedList<String>();
        recyclerView = (RecyclerView) findViewById(R.id.recycle_view);
        // 设置布局管理器
        // 支持 单列线性排列,支持GridView模式,瀑布流模式
        // 1.线性布局
        LinearLayoutManager layoutManager =
                new LinearLayoutManager(this,   // 上下文
                                        LinearLayout.VERTICAL,  //垂直布局,
                                        false);

//        // 2.Grid布局
//        RecyclerView.LayoutManager layoutManager =
//                new GridLayoutManager(this,
//                                      2,
//                                      GridLayoutManager.HORIZONTAL,
//                                      false
//                                      );
//
//         // 3.瀑布流
//        RecyclerView.LayoutManager layoutManager =
//                new StaggeredGridLayoutManager(3,
//                        StaggeredGridLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(layoutManager);
        // 设置 RecyclerView的Adapter
        // 注意一定在设置了布局管理器之后调用
        adapter = new MyRecycleAdapter(data);
        recyclerView.setAdapter(adapter);
    }

    public void btnAddItem(View view) {
        data.add(0,"Time:"+System.currentTimeMillis());
        adapter.notifyDataSetChanged();
    }

    public void btnRemoveItem(View view) {
        if (!data.isEmpty()) {
            data.remove(0);
        }
        adapter.notifyItemRemoved(0);
    }

    /**
     * 继承RecyclerView.Adapter,用于显示数据
     * 需要定义并且使用 ViewHolder ,必须要使用
     */
    private class MyRecycleAdapter extends RecyclerView.Adapter<ViewHolder>{
        private List<String> strings;
        public MyRecycleAdapter(List<String> strings) {
            this.strings = strings;
        }

        @Override
        public int getItemCount() {
            int ret = 0;
            if (strings != null) {
                ret = strings.size();
            }
            return ret;
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
            ViewHolder ret = null;
            // 不需要检查是否复用,因为只要进入此方法,必然没有复用
            // 因为RecyclerView 通过Holder检查复用
            View v = LayoutInflater.from(Android5Activity.this).inflate(R.layout.item_recycler, viewGroup, false);
            ret = new ViewHolder(v);
            return ret;
        }

        @Override
        public void onBindViewHolder(ViewHolder viewHolder, int i) {
            viewHolder.textView.setText(strings.get(i));
            int resId = R.mipmap.ic_launcher;
            int index = i%5;
            switch (index){
                case 0:
                    resId = R.mipmap.a11;
                    break;
                case 1:
                    resId = R.mipmap.a33;
                    break;
                case 2:
                    resId = R.mipmap.a22;
                    break;
            }
            viewHolder.imageView.setImageResource(resId);
        }
    }

    /**
     * 创建自己的ViewHolder ,必须要继承RecyclerView.ViewHolder
     */
    private static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        public ImageView imageView;
        public TextView textView;

        public ViewHolder(View itemView) {
            super(itemView);
            // 通常ViewHolder的构造,就是用于获取控件视图的
            imageView = (ImageView) itemView.findViewById(R.id.item_icon);
            textView = (TextView) itemView.findViewById(R.id.item_title);
            // TODO 后续处理点击事件的操作
            itemView.setOnClickListener(this);

        }
        @Override
        public void onClick(View v) {
            int position = getAdapterPosition();
            Context context = imageView.getContext();
            Toast.makeText(context,"显示第"+position+"个项",Toast.LENGTH_SHORT).show();
        }
    }
}

andbase框架是为Android开发者量身打造的一款开源类库产品,本文我们学习andbase框架现多功能标题栏的实例代码。

本教程的学习内容:

1.使用AndBase实现多功能标题栏...

 AndBase框架内部提供了许多的方式能够使我们去设置一个更好的标题栏,进行动态的改变,而并非静态的将标题栏界面写死...能够使得标题栏更加的美观...总体就是动态的获取布局然后通过对布局的操作来自定义一个良好的标题栏...

使用AndBase框架的时候我们的主函数就不是继承于Acticity了,而是继承于AbActivity,万变不离其宗还是Activity...

public class MainActivity extends AbActivity

继承了AbActivity这样就使得自己的Activity拥有一个框架...我们就可以使用内部包含的一些变量...就拿我们的标题栏变量来说吧...mAbTitleBar就是内在的一个标题栏变量...凡是继承了AbActivity的Activity就可以使用这个变量,这个变量会作为当前Activity中的标题栏变量..我们就直接可以对其进行一些相关属性的设定..从而书写我们自己的标题栏...

1.1 设置标题栏的有无...

标题栏的有无设置其实就是设置当前的mAbTitleBar是否进行显示...通过调用SetVisiable函数来进行相关的设置...
 
public void setVisiable(boolean b){
        if(b){
            titleBar.setVisibility(View.VISIBLE); //设置标题栏显示在视图上..
        }else{
            titleBar.setVisibility(View.GONE);//设置标题栏不显示在视图上..
        }
}

1.2 为标题栏添加新的控件...

无论是在标题栏还是Activity中如果想要动态的添加控件,那么必然是添加View的过程...添加View之前,我们需要把控件添加到View当中,然后再把View添加在Activity上,这样就完成了动态添加视图的效果...View其实只是一个空的架子,更形象的来说是一个没有打起的气球一样..光有外壳,没有东西,那么如果想要有东西,那么我们需要为其进行充气,只有充了气之后的View才是我们想要去进行加载的...否则是没什么用的..

充气的过程就是使用LayoutInflater.inflater(R.layout.XXX,root);函数来完成的...对ListView使用熟悉的那么想必也就非常清楚这个过程了...使用ListView时,我们只是对ListView中的Item进行布局的书写,但是绝对不会在布局中写死Item布局中的东西..因为Item中的东西一般都是进行动态添加的...所以我们一般是获取ListView的ID,然后对每一个Item进行动态的加载...这一般是对ListView进行操作...其实都一样...添加视图都是进行动态添加...添加了View之后,我们仍然可以通过获取控件然后绑定监听,从而做更多的操作...

这个函数是我自定义的一个函数...arg0和arg1是通过Inflater.inflate获取到的视图...

public void setRightview(int resid0,int resid1){
        ClearRightView();  
        titleBar.addRightView(arg0);  //为标题栏添加视图...
        titleBar.addRightView(arg1);  
        moreview=(Button) arg0.findViewById(resid0);//通过当前视图获取控件...
        moreapp=(Button) arg1.findViewById(resid1);
        
        //通过获取控件之后,设置相关监听,从而完成更多的操作...
        moreview.setOnClickListener(new View.OnClickListener() {
            
            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                Toast.makeText(context, "点击", Toast.LENGTH_LONG).show();
            }
        });
        
        moreapp.setOnClickListener(new View.OnClickListener() {
            
            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub
                Toast.makeText(context, "继续点击", Toast.LENGTH_LONG).show();
            }
        });

    }

这样就完成了视图的添加...其实标题栏的操作基本都是这个过程...最重要的还是需要清楚其中的函数到底怎么用,如何为标题栏设置相关的属性,比如说背景颜色,标题栏的文本,标题栏的Logo以及一些相关属性的设置,其实说白了就是把xml那种布局方式通过Java代码来更加灵活的进行运用...可以使得布局上的控件更加的灵活...

1.3 操作栏变换...

我们通过一个函数来看一看mAbTitleBar到底有哪些函数可以动态设置View视图...这里是为了介绍一些相关的函数的应用...并没有完整的代码过程...源代码过程我会最后进行给出..

btn5.setOnClickListener(new View.OnClickListener() {
                        
            @Override
            public void onClick(View v) {//以下函数的调用时不存在函数顺序的问题的...不同的调用顺序显示的结果是一样的,,,
            mAbTitleBar.setLogo(R.drawable.button_selector_delete); //为标题栏设置logo..
                mAbTitleBar.setLogo2(R.drawable.button_selector_app); //为标题栏设置第二个Logo..这个setLogo只有这两个函数..没有Logo3...如果想设置更多的Logo..只能去写xml布局...
                mAbTitleBar.clearRightView(); //清除标题栏右边的视图..
                View rightViewOk = mInflater.inflate(R.layout.ok_btn, null); //为一个View视图充气...
                mAbTitleBar.addRightView(rightViewOk); //在标题栏右边添加当前充气后的视图..
                mAbTitleBar.setTitleText("正在修改"); //设置标题栏的文字属性..
                mAbTitleBar.setTitleBarBackground(R.drawable.top_bg2);//设置文字的背景..
                mAbTitleBar.setTitleBarGravity(Gravity.LEFT,Gravity.CENTER);//设置标题栏的对其方式...
                mAbTitleBar.setLogoOnClickListener(new OnClickListener() {
                    //为Logo图片设置监听...
                    @Override
                    public void onClick(View v) {
                    //改变相关的属性...    mAbTitleBar.setTitleBarBackground(R.drawable.top_bg);
                        mAbTitleBar.setTitleText("多功能标题栏");
                        mAbTitleBar.clearRightView();
                        mAbTitleBar.setLogo(R.drawable.button_selector_back);
                        mAbTitleBar.setTitleBarGravity(Gravity.LEFT,Gravity.RIGHT);
                        mAbTitleBar.setTitleTextMargin(20, 0, 0, 0);
                        mAbTitleBar.getLogoView2().setVisibility(View.GONE); //设置可见性...
                        mAbTitleBar.setLogoOnClickListener(new OnClickListener() {
                            
                            @Override
                            public void onClick(View v) {
                                finish();
                            }
                        });
                    }
                });
            }
        });    



1.4 标题栏的下拉效果...


http://images2015.cnblogs.com/blog/734980/201510/734980-20151030192744638-1617911112.png


标题栏的下拉其实就是点击标题形成下拉菜单..每个下拉菜单都是一个子选项...通过这些子选项我们可以做更多的事情...效果如上...其实每一个这样类似的菜单都是一个ListView..通过对ListView的每一个Item进行初始化,那么也就自然而然的形成这样的效果了...其实总体还是进行动态布局...

btn7.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                 mAbTitleBar.setTitleTextBackgroundResource(R.drawable.drop_down_title_btn);
                 View popView = mInflater.inflate(R.layout.list_pop, null); //先获取一个视图..
                 ListView popListView = (ListView) popView.findViewById(R.id.pop_list);  //获取ListView的ID..
                 List<AbMenuItem> list = new ArrayList<AbMenuItem>();  //定义一个列表项...这个类只保存ID和Text属性..
                 list.add(new AbMenuItem("蔡文姬"));   
                 list.add(new AbMenuItem("貂蝉"));
                 list.add(new AbMenuItem("紫罂粟"));
                 list.add(new AbMenuItem("孙尚香"));
                 ListPopAdapter mListPopAdapter = new ListPopAdapter(TitleBarActivity.this, list,R.layout.list_pop_item); //定义一个适配器...
                 popListView.setAdapter(mListPopAdapter); //为ListView设置一个适配器...
                 
                 mAbTitleBar.setTitleTextDropDown(popView); //设置TitleBar上的TextView被点击的监听..
            }
        });



通过这样的设置,很容易形成这样的效果..这里使用了AbMenuItem类...通过源码我们可以知道这个类有两个构造函数,一个是对int ID 和String Text两个属性进行保存,一个则是仅仅保存一个String Text属性...这样也就使得ListView显示的东西比较的简单...并且一般这种小型的ListView一般也就一个Text值..因此使用AbMenuItem还是比较方便的...这样也就完成了一个下拉菜单...

1.5 适配器...

再简单的说一下适配器这个东西吧...适配器主要还是用于为ListView中的每一个Item进行动态的设置,使得ListView的布局能够更加的灵活...说白了就是定义了一种样式,ListView中的每一个Item都需要按照这个样式来进行显示...非常的简单...


/*
 *
 *  CopyRight@ By Darker 2015-10-30
 *  多功能菜单...
 *
 * */

package com.example.andbasetitlebar;


import java.util.List;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

import com.ab.global.AbMenuItem;


public class Adapter extends BaseAdapter {  //继承BaseAdapter

    private Context context;

    private List<AbMenuItem> list;

    private int itemResource;
    
    public Adapter(Context context, List<AbMenuItem> list,int itemResource) {
        this.context = context;
        this.list = list;
        this.itemResource = itemResource;
    }

    @Override
    public int getCount() {
        return list.size();
    }

    @Override
    public Object getItem(int position) {

        return list.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }
        //由于我们的ListView中的Item显示的方式都非常的简单,因此ViewHolder类中也就仅仅定义了一个TextView属性...
    @Override
    public View getView(int position, View convertView, ViewGroup viewGroup) {

        ViewHolder holder;
                /*这个判断其实是为了复用而定义的...因为ListView都是动态进行加载的..             *如果ListView中的Item超过了屏幕的大小,那么势必就需要进行下拉...            *下拉就代表原来的那些ListView就需要被销毁...
                  * 那么总不能每一次滑动我们都新建立一个ViewHolder对象..我们可以把这些被销毁的ViewHolder进行复用...
                  * 有了复用之后,再次滑动的时候ViewHolder会自动调用那些被销毁掉的进行复用...从而使得资源得到重新加载...
                  * 说白了就是提高了复用性...
                  */
        if (convertView == null) {
            convertView = LayoutInflater.from(context).inflate(itemResource, null); //定义一个视图对象View...
            holder = new ViewHolder();
            holder.itemText = (TextView) convertView.findViewById(R.id.pop_item); //在当前的View中获取ListView的ID..
            convertView.setTag(holder);//设置标志..
        } else {
            holder = (ViewHolder) convertView.getTag();//这里可以直接获取标志,对holder进行复用,减少了内存的分配和开销...
        }
        AbMenuItem item = list.get(position);
        holder.itemText.setText(item.getText()); //为每一个Item中的TextView设置值...
        
        return convertView;
    }

    static class ViewHolder {
        TextView itemText;
    }

}
    

实现多功能菜单其实非常的简单,大部分的函数都是写好的函数,我们只需要调用就可以设置标题栏的相关样式...实现多功能的菜单...最后附加上我自己书写的代码..还是完整的代码能够使我们进步的更加迅速...从而可以理解其中的调用过程和原理...




Android快速开发框架andbase

andbase是为Android开发者量身打造的一款开源类库产品,您可以在本站中获取到最新的代码,示例以及开发文档。

andbase能做什么?

1.andbase中包含了大量的开发常用手段。

如网络下载数据的方法,多线程,线程池的管理,图片缓存管理,图片文件下载上传,对常用工具类入字符串,日期,文件处理,图片处理等进行了进一步的封装,能够使您的应用在团队开发中减少冗余代码,很大的提高了代码的维护性与开发高效行,能很好的规避由于开发疏忽而导致常犯的错误。

2.andbase封装了大量的常用且系统本身没有的控件。

如list分页,下拉刷新,图片轮播,多线程支持断点续传的下载器,左右侧边栏,图片多张上传,iphone中比较流行的轮子选择,在achart基础上进行改进与精简的图表,日历选择器等。

2.强大的AbActivity,您没有理由不继承它。

继承它你能够获得一个简单强大可设置的标题栏,以及一系列的简单调用,如弹出框,提示框,进度框等。

3.提供效率较高图片缓存管理策略,使内存大幅度节省,利用率提高,效率提高。

程序中要管理大量的图片资源,还要从网络下载,andbase提供简单的方法,几步完成下载与显示,并支持缩放,裁剪,缓存功能。

4.日期处理代码,项目中的格式很多,一个一个从网上找吗,多麻烦,andbase都能解决。

日期处理各种函数,你懂的。

5.用andbase再也不需要handler了,线程里直接toast,都没问题。

handler会产生大量代码,并且不好维护,andbase对handler进行了封装,忘了它吧。

6.简单轻量支持注解自动建表的ORM框架(1.5.2新增,多谢小胖提供源码)。

是否感觉写sql写字段,建表很麻烦呢,用注解吧,一句话完成增删改查。

Android中双进程Service可以让2个进程互相保护,其中一个Service被清理后,另外没被清理的进程可以立即重启进程。本文我们对Android开发中双进程守护的尝试。

最近在做一个Android的项目,其包含一个消息推送的后台服务。由于该服务可能会有重要的信息推送,因此并不希望当APP程序退出、APP程序被一键清理、APP被强制停止等用户操作发生时,这个后台服务也随之被杀死。这个问题也就是所谓的“内存永驻”。关于这个问题,网上有很多说法,如调用startforehand函数以提高service的优先级、在service中创建一个不能被删掉的notification(或者产生一个其他的与用户界面交互的UI控件)、在service的onDestroy函数中重启这个服务、修改onstartcommand函数的返回值等等。这些方法,笔者都一一试过,但都没有效果。但是,我们可以看到市面上也确实存在一些App在一定的时间后可以自动重启,说明仍然是存在方法可以完成这项任务的。

文章中介绍的方法涉及到Android的JNI编程,主要思想就是通过调用native函数创建一个子进程。父子进程相互监听,若子进程死去,父进程妥善处理后重新创建新的子进程;若父进程死去,子进程使用AM命令重启父进程。这种思想唯一的缺陷就是如何保证父子进程不被同时杀死的情况。子进程能不能被杀死,只能用实验来证明。

首先笔者按照文章介绍的,整理了代码,并将相关代码植入到自己的项目中。

步骤1)编写Watcher类。它为上面的Java程序调用提供必要的接口,声明需要native语言实现的的具体函数。native语言主要是指C/C++语言。上层的Java程序只需要创建一个Watcher类并调用它的createAppMonitor(String userId)函数即可。

public class Watcher {
    private static final String PACKET = 
"com.example.dameonservice";
    private String mMonitoredService = 
"com.example.mqtt.MQTTSubscribeService";
    private volatile boolean 
bHeartBreak = false;
    private Context mContext;
    private boolean 
mRunning = true;
    
    public void createAppMonitor(String 
userId)
    {
        if(!createWatcher(userId))
        
{
            Log.e("Watcher", "<<Monitor created 
failed>>");
        }
    }
    
    public Watcher(Context 
context)
    {
        mContext = context;
    }
   
    
/*创建一个监视子进程
     *userId 当前进程的用户ID,子进程重启当前进程时需要用到当前进程的用户ID
     *return  
若子进程创建成功返回TRUE,否则返回FALSE
     */
    private native boolean 
createWatcher(String userId);
    
    /* 让当前进程连接到监视进程
     * return 
连接成功返回TRUE,否则返回FALSE
     */
    private native boolean 
connectToMonitor();
    
    /*向监视进程发送任意信息
     * msg 
发给monitor的信息
     * return 实际发送的字节数
     */
    private native int 
sendMsgToMonitor(String msg);
    
    static
    {
        
System.loadLibrary("monitor");   //这里要和后面的Android.mk中模块名对应
    }
}

2)编译上面的文件会在bin/classes 目录下生成相对应的Watcher.class文件,通过DOs界面进入该bin/classes 目录下,通过javah命令生成C/C++对应的头文件。

“javah 包名+类名” 得到以下头文件:

#include <jni.h>
/* Header for class com_example_dameonservice_Watcher */

#ifndef _Included_com_example_dameonservice_Watcher
#define _Included_com_example_dameonservice_Watcher
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_dameonservice_Watcher
 * Method:    createWatcher
 * Signature: (Ljava/lang/String;)Z
 */
JNIEXPORT jboolean JNICALL Java_com_example_dameonservice_Watcher_createWatcher
  (JNIEnv *, jobject, jstring);

/*
 * Class:     com_example_dameonservice_Watcher
 * Method:    connectToMonitor
 * Signature: ()Z
 */
JNIEXPORT jboolean JNICALL Java_com_example_dameonservice_Watcher_connectToMonitor
  (JNIEnv *, jobject);

/*
 * Class:     com_example_dameonservice_Watcher
 * Method:    sendMsgToMonitor
 * Signature: (Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_example_dameonservice_Watcher_sendMsgToMonitor
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

3)创建JNI文件夹,将得到的头文件移到该文件夹下,继续在该文件夹下创建与上面得到的头文件同名的C/C++文件,然后实现头文件中提到的方法。(具体实现太多,这里就不再贴出来了)

4)添加Android.mk文件。这个文件的格式基本是统一的。只需要修改LOCAL_MODULE和LOCAL_SRC_FILES两处即可。如果你还有添加Log打印函数,还要在这里添加 “LOCAL_LDLIBS := -lm -llog”。

下面一张图来说明整体的文件结构分布:


其中com_example_dameonservice_Watcher.c和com_example_dameonservice_Watcher.cpp内容相同。process.cpp定义一些辅助类。

实验结果:

这当然是大家最关心的。测试的手机选用的小米,感觉 小米在这一块的优化还是很不错的,所以用它来试试。最终的测试结果是:被杀死的服务概率性地可以重启成功,且失败的概率更大。通过Log分析,不能重启的时候是因为子进程也死掉了。截止到笔者写下这篇文章,还没有抓住其中的规律。一键清理和子进程的被杀死没有绝对的对应关系。而且即使是在App运行的时候,也会发现子进程会被杀死,然后又被父进程重启。子进程被杀死是重启失败的主要原因。但现在的现象无法确定子进程被杀死的确切原因,有一种可能是被系统杀死了,但这样的不确定性太大,对效果也不能有很好的保证。

虽然没有完美解决问题,但至少比前面的办法强很多,至少它也重启成功过。这个方法感觉继续优化一下还是可以做好的。



Android实现双进程守护


做过android开发的人应该都知道应用会在系统资源匮乏的情况下被系统杀死!当后台的应用被系统回收之后,如何重新恢复它呢?网上对此问题有很多的讨论。这里先总结一下网上流传的各种解决方案,看看这些办法是不是真的可行。

1.提高优先级
这个办法对普通应用而言,应该只是降低了应用被杀死的概率,但是如果真的被系统回收了,还是无法让应用自动重新启动!
    
2.让service.onStartCommand返回START_STICKY
通过实验发现,如果在adb shell当中kill掉进程模拟应用被意外杀死的情况(或者用360手机卫士进行清理操作),如果服务的onStartCommand返回START_STICKY,在eclipse的进程管理器中会发现过一小会后被杀死的进程的确又会出现在任务管理器中,貌似这是一个可行的办法。但是如果在系统设置的App管理中选择强行关闭应用,这时候会发现即使onStartCommand返回了START_STICKY,应用还是没能重新启动起来!

3.android:persistent="true"
网上还提出了设置这个属性的办法,通过实验发现即使设置了这个属性,应用程序被kill之后还是不能重新启动起来的!

4.让应用成为系统应用
实验发现即使成为系统应用,被杀死之后也不能自动重新启动。但是如果对一个系统应用设置了persistent="true",情况就不一样了。实验表明对一个设置了persistent属性的系统应用,即使kill掉会立刻重启。一个设置了persistent="true"的系统应用,在android中具有core service优先级,这种优先级的应用对系统的low memory killer是免疫的!

OK,说了半天,只有core service优先级的应用才能保证在被意外杀死之后做到立刻满血复活。而普通应用要想成为系统应用就必须要用目标机器的签名文件进行签名,但这样又造成了应用无法保证兼容所有不同厂商的产品。那么该怎么办呢?这里就来说一说双进程守护。网上也有人提到过双进程守护的办法,但是很少能搜索到类似的源码!如果从进程管理器重观察会发现新浪微博或者360卫视都有两个相关的进程,其中一个就是守护进程,由此可以猜到这些商业级的软件也采用了双进程守护的办法。

什么是双进程守护呢?顾名思义就是两个进程互相监视对方,发现对方挂掉就立刻重启!不知道应该把这样的一对进程是叫做相依为命呢还是难兄难弟好呢,但总之,双进程守护的确是一个解决问题的办法!相信说到这里,很多人已经迫切的想知道如何实现双进程守护了。这篇文章就介绍一个用NDK来实现双进程保护的办法,不过首先说明一点,下面要介绍的方法中,会损失不少的效率,反应到现实中就是会使手机的耗电量变大!但是这篇文章仅仅是抛砖引玉,相信看完之后会有更多高人指点出更妙的实现办法。

需要了解些什么?
这篇文章中实现双进程保护的方法基本上是纯的NDK开发,或者说全部是用C++来实现的,需要双进程保护的程序,只需要在程序的任何地方调用一下JAVA接口即可。下面几个知识点是需要了解的:
1.linux中多进程;
2.unix domain套接字实现跨进程通信;
3.linux的信号处理;
4.exec函数族的用法;

其实这些东西本身并不是多复杂的技术,只是我们把他们组合起来实现了一个双进程守护而已,没有想象中那么神秘!在正式贴出代码之前,先来说说几个实现双进程守护时的关键点:
1.父进程如何监视到子进程(监视进程)的死亡?
很简单,在linux中,子进程被终止时,会向父进程发送SIG_CHLD信号,于是我们可以安装信号处理函数,并在此信号处理函数中重新启动创建监视进程;
2.子进程(监视进程)如何监视到父进程死亡?
当父进程死亡以后,子进程就成为了孤儿进程由Init进程领养,于是我们可以在一个循环中读取子进程的父进程PID,当变为1就说明其父进程已经死亡,于是可以重启父进程。这里因为采用了循环,所以就引出了之前提到的耗电量的问题。
3.父子进程间的通信
有一种办法是父子进程间建立通信通道,然后通过监视此通道来感知对方的存在,这样不会存在之前提到的耗电量的问题,在本文的实现中,为了简单,还是采用了轮询父进程PID的办法,但是还是留出了父子进程的通信通道,虽然暂时没有用到,但可备不时之需!

OK, 下面就贴上代码!首先是Java部分,这一部分太过简单,只是一个类,提供了给外部调用的API接口用于创建守护进程,所有的实现都通过native方法在C++中完成!

package com.example.dameonservice;
import java.util.ArrayList;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.util.Log;

/**
* 监视器类,构造时将会在Native创建子进程来监视当前进程,
* @author wangqiang
* @date 2014-04-24
*/
public class Watcher
{
//TODO Fix this according to your service
private static final String PACKAGE = "com.example.dameonservice/";
private String mMonitoredService = "";
private volatile boolean bHeartBreak = false;
private Context mContext;
private boolean mRunning = true;

public void createAppMonitor(String userId)
{
if( !createWatcher(userId) )
{
Log.e("Watcher", "<<Monitor created failed>>");
}
}

public Watcher( Context context)
{
mContext = context;
}

private int isServiceRunning()
{
ActivityManager am=(ActivityManager)mContext.getSystemService(Context.ACTIVITY_SERVICE);
ArrayList<RunningServiceInfo> runningService = (ArrayList<RunningServiceInfo>)am.getRunningServices(1024);
for( int i = 0; i < runningService.size(); ++i )
{
if( mMonitoredService.equals(runningService.get(i).service.getClassName().toString() ))
{
return 1;
}
}
return 0;
}

/**
* Native方法,创建一个监视子进程.
* @param userId 当前进程的用户ID,子进程重启当前进程时需要用到当前进程的用户ID.
* @return 如果子进程创建成功返回true,否则返回false
*/
private native boolean createWatcher(String userId);

/**
* Native方法,让当前进程连接到监视进程.
* @return 连接成功返回true,否则返回false
*/
private native boolean connectToMonitor();

/**
* Native方法,向监视进程发送任意信息
* @param 发给monitor的信息
* @return 实际发送的字节
*/
private native int sendMsgToMonitor(String msg);

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


代码中很多属性都是测试时用的,懒得去掉,其实有些都没用到。只需要关心createAppMonitor这个对外接口就可以了,它要求传入一个当前进程的用户ID,然后会调用createWatcher本地方法来创建守护进程。还有两个方法connectToMonitor用于创建和监视进程的socket通道,sendMsgToMonitor用于通过socket向子进程发送数据。由于暂时不需要和子进程进行数据交互,所以这两个方法就没有添加对外的JAVA接口,但是要添加简直是轻而易举的事!


Ok,JAVA只是个壳,内部的实现还得是C++,为了让程序更加的面向对象,在实现native时,我们用一个ProcessBase基类来对父子进程进行一个抽象,把父子进程都会有的行为抽象出来,而父子进程可以根据需要用自己的方式去实现其中的接口,先来看看这个抽象了父子进程共同行为的ProcessBase基类:
#ifndef _PROCESS_H
#define _PROCESS_H


#include <jni.h>
#include <sys/select.h>
#include <unistd.h>
#include <sys/socket.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#include <android/log.h>
#include <sys/types.h>
#include <sys/un.h>
#include <errno.h>
#include <stdlib.h>
#include "constants.h"


#define LOG_TAG "Native"


#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)


/**
* 功能:对父子进程的一个抽象
* @author wangqiang
* @date 2014-03-14
*/
class ProcessBase
{
public:

ProcessBase( );

/**
* 父子进程要做的工作不相同,留出一个抽象接口由父子进程
* 自己去实现.
*/
virtual void do_work() = 0;

/**
* 进程可以根据需要创建子进程,如果不需要创建子进程,可以给
* 此接口一个空实现即可.
*/
virtual bool create_child() = 0;

/**
* 捕捉子进程死亡的信号,如果没有子进程此方法可以给一个空实现.
*/
virtual void catch_child_dead_signal() = 0;

/**
* 在子进程死亡之后做任意事情.
*/
virtual void on_child_end() = 0;

/**
* 创建父子进程通信通道.
*/
bool create_channel();

/**
* 给进程设置通信通道.
* @param channel_fd 通道的文件描述
*/
void set_channel(int channel_fd);

/**
* 向通道中写入数据.
* @param data 写入通道的数据
* @param len  写入的字节数
* @return 实际写入通道的字节数
*/
int write_to_channel( void* data, int len );

/**
* 从通道中读数据.
* @param data 保存从通道中读入的数据
* @param len  从通道中读入的字节数
* @return 实际读到的字节数
*/
int read_from_channel( void* data, int len );

/**
* 获取通道对应的文件描述符
*/
int get_channel() const;

virtual ~ProcessBase();

   protected:

int m_channel;
};


只是很简单的一个类,相信看看注释就知道是什么意思了,比如父子进程可能都需要捕获他的子孙死亡的信号,于是给一个catch_child_dead_signal函数,如果对子进程的死活不感兴趣,可以给个空实现,忽略掉就可以了,谁叫他大逆不道呢?由于用了纯虚函数,所以ProcessBase是一个抽象类,也就是说它不能有自己的实例,只是用来继承的,它的子孙后代可以用不同的方式实现它里面的接口从而表现出不一样的行为,这里父进程和子进程的行为就是有区别的,下面就先为诸君奉上父进程的实现:
/**
* 功能:父进程的实现
* @author wangqiang
* @date 2014-03-14
*/
class Parent : public ProcessBase
{
public:

Parent( JNIEnv* env, jobject jobj );

virtual bool create_child( );

virtual void do_work();

virtual void catch_child_dead_signal();

virtual void on_child_end();

virtual ~Parent();

bool create_channel();

/**
* 获取父进程的JNIEnv
*/
JNIEnv *get_jni_env() const;

/**
* 获取Java层的对象
*/
jobject get_jobj() const;

private:

JNIEnv *m_env;

jobject m_jobj;

};

以上是定义部分,其实JNIEnv和jobject基本上没用到,完全可以给剃掉的,大家就当这两个属性不存在就是了!实现部分如下:
#include "process.h"
#include "Utils.h"

extern ProcessBase *g_process;

extern const char* g_userId;

extern JNIEnv* g_env;

//子进程有权限访问父进程的私有目录,在此建立跨进程通信的套接字文件
static const char* PATH = "/data/data/com.example.dameonservice/my.sock";

//服务名称
static const char* SERVICE_NAME = "com.example.dameonservice/com.example.dameonservice.MyService";

bool ProcessBase::create_channel( )
{
}

int ProcessBase::write_to_channel( void* data, int len )
{
return write( m_channel, data, len );
}

int ProcessBase::read_from_channel( void* data, int len )
{
return read( m_channel, data, len );
}

int ProcessBase::get_channel() const
{
return m_channel;
}

void ProcessBase::set_channel( int channel_fd )
{
m_channel = channel_fd;
}

ProcessBase::ProcessBase()
{

}

ProcessBase::~ProcessBase()
{
close(m_channel);
}

Parent::Parent(JNIEnv *env, jobject jobj) : m_env(env)
{
LOGE("<<new parent instance>>");

m_jobj = env->NewGlobalRef(jobj);
}

Parent::~Parent()
{
LOGE( "<<Parent::~Parent()>>" );

g_process = NULL;
}

void Parent::do_work()
{
}

JNIEnv* Parent::get_jni_env() const
{
return m_env;
}

jobject Parent::get_jobj() const
{
return m_jobj;
}

/**
* 父进程创建通道,这里其实是创建一个客户端并尝试
* 连接服务器(子进程)
*/
bool Parent::create_channel()
{
int sockfd;

sockaddr_un addr;

while( 1 )
{
sockfd = socket( AF_LOCAL, SOCK_STREAM, 0 );

if( sockfd < 0 )
{
LOGE("<<Parent create channel failed>>");

return false;
}

memset(&addr, 0, sizeof(addr));

addr.sun_family = AF_LOCAL;

strcpy( addr.sun_path, PATH );

if( connect( sockfd, (sockaddr*)&addr, sizeof(addr)) < 0 )
{
close(sockfd);

sleep(1);

continue;
}

set_channel(sockfd);

LOGE("<<parent channel fd %d>>", m_channel );

break;
}

return true;
}

/**
* 子进程死亡会发出SIGCHLD信号,通过捕捉此信号父进程可以
* 知道子进程已经死亡,此函数即为SIGCHLD信号的处理函数.
*/
static void sig_handler( int signo )
{
pid_t pid;

int status;

//调用wait等待子进程死亡时发出的SIGCHLD
//信号以给子进程收尸,防止它变成僵尸进程
pid = wait(&status);

if( g_process != NULL )
{
g_process->on_child_end();
}
}

void Parent::catch_child_dead_signal()
{
LOGE("<<process %d install child dead signal detector!>>", getpid());

struct sigaction sa;

sigemptyset(&sa.sa_mask);

sa.sa_flags = 0;

sa.sa_handler = sig_handler;

sigaction( SIGCHLD, &sa, NULL );
}

void Parent::on_child_end()
{
LOGE("<<on_child_end:create a new child process>>");

create_child();
}

bool Parent::create_child( )
{
pid_t pid;

if( (pid = fork()) < 0 )
{
return false;
}
else if( pid == 0 ) //子进程
{
LOGE("<<In child process,pid=%d>>", getpid() );

Child child;

ProcessBase& ref_child = child;

ref_child.do_work();
}
else if( pid > 0 )  //父进程
{
LOGE("<<In parent process,pid=%d>>", getpid() );
}

return true;
}
这里先要说明一下三个全局变量:
g_process是父进程的指针;
g_userId是父进程用户ID,由Java侧传递过来,我们需要把它用全局变量保存起来,因为子进程在重启父进程的时候需要用到用户ID,否则会有问题,当然这里也得益于子进程能够继承父进程的全局变量这个事实!
g_env是JNIEnv的指针,把这个变量也作为一个全局变量,是保留给子进程用的;


父进程在create_child中用fork创建了子进程,其实就是一个fork调用,然后父进程什么都不做,子进程创建一个Child对象并调用其do_work开始做自己该做的事!


父进程实现了catch_child_dead_signal,在其中安装了SIG_CHLD信号处理函数,因为他很爱他的儿子,时刻关心着他。而在信号处理函数sig_handler中,我们留意到了wait调用,这是为了防止子进程死了以后变成僵尸进程,由于我们已经知道父进程最多只会创建一个子监视进程,所以wait就足够了,不需要waitpid函数亲自出马!而信号处理函数很简单,重新调用一下on_child_end,在其中再次create_child和他亲爱的夫人在make一个小baby就可以了!


最后要说说create_channel这个函数,他用来创建和子进程的socket通道,这个编程模型对于有网络编程经验的人来说显得非常亲切和熟悉,他遵循标准的网络编程客户端步骤:创建socket,connect,之后收发数据就OK了,只是这里的协议用的是AF_LOCAL,表明我们是要进行跨进程通信。由于域套接字用的不是IP地址,而是通过指定的一个文件来和目标进程通信,父子进程都需要这个文件,所以这个文件的位置指定在哪里也需要注意一下:在一个没有root过的手机上,几乎所有的文件都是没有写入权限的,但是很幸运的是linux的子进程共享父进程的目录,所以把这个位置指定到/data/data/下应用的私有目录就可以做到让父子进程都能访问这个文件了!

接下来是子进程的实现了,它的定义如下:
/**
* 子进程的实现
* @author wangqiang
* @date 2014-03-14
*/
class Child : public ProcessBase
{
public:

Child( );

virtual ~Child();

virtual void do_work();

virtual bool create_child();

virtual void catch_child_dead_signal();

virtual void on_child_end();

bool create_channel();

private:

/**
* 处理父进程死亡事件
*/
void handle_parent_die();

/**
* 侦听父进程发送的消息
*/
void listen_msg();

/**
* 重新启动父进程.
*/
void restart_parent();

/**
* 处理来自父进程的消息
*/
void handle_msg( const char* msg );

/**
* 线程函数,用来检测父进程是否挂掉
*/
void* parent_monitor();

void start_parent_monitor();

/**
* 这个联合体的作用是帮助将类的成员函数做为线程函数使用
*/
union
{
void* (*thread_rtn)(void*);

void* (Child::*member_rtn)();
}RTN_MAP;
};
#endif
注意到里面有个union,这个联合体的作用是为了辅助把一个类的成员函数作为线程函数来传递给pthread_create,很多时候我们都希望线程能够像自己人一样访问类的私有成员,就像一个成员函数那样,用friend虽然可以做到这一点,但总感觉不够优美,由于成员函数隐含的this指针,使我们完全可以将一个成员函数作为线程函数来用。只是由于编译器堵死了函数指针的类型转换,所以这里就只好用一个结构体。

废话不多说,看看子进程的实现:
bool Child::create_child( )
{
//子进程不需要再去创建子进程,此函数留空
return false;
}

Child::Child()
{
RTN_MAP.member_rtn = &Child::parent_monitor;
}

Child::~Child()
{
LOGE("<<~Child(), unlink %s>>", PATH);

unlink(PATH);
}

void Child::catch_child_dead_signal()
{
//子进程不需要捕捉SIGCHLD信号
return;
}

void Child::on_child_end()
{
//子进程不需要处理
return;
}

void Child::handle_parent_die( )
{
//子进程成为了孤儿进程,等待被Init进程收养后在进行后续处理
while( getppid() != 1 )
{
usleep(500); //休眠0.5ms
}

close( m_channel );

//重启父进程服务
LOGE( "<<parent died,restart now>>" );

restart_parent();
}

void Child::restart_parent()
{
LOGE("<<restart_parent enter>>");

/**
* TODO 重启父进程,通过am启动Java空间的任一组件(service或者activity等)即可让应用重新启动
*/
execlp( "am",
"am",
"startservice",
"--user",
g_userId,
"-n",
SERVICE_NAME, //注意此处的名称
(char *)NULL);
}

void* Child::parent_monitor()
{
handle_parent_die();
}

void Child::start_parent_monitor()
{
pthread_t tid;

pthread_create( &tid, NULL, RTN_MAP.thread_rtn, this );
}

bool Child::create_channel()
{
int listenfd, connfd;

struct sockaddr_un addr;

listenfd = socket( AF_LOCAL, SOCK_STREAM, 0 );

unlink(PATH);

memset( &addr, 0, sizeof(addr) );

addr.sun_family = AF_LOCAL;

strcpy( addr.sun_path, PATH );

if( bind( listenfd, (sockaddr*)&addr, sizeof(addr) ) < 0 )
{
LOGE("<<bind error,errno(%d)>>", errno);

return false;
}

listen( listenfd, 5 );

while( true )
{
if( (connfd = accept(listenfd, NULL, NULL)) < 0 )
{
if( errno == EINTR)
continue;
else
{
LOGE("<<accept error>>");

return false;
}
}

set_channel(connfd);

break;
}

LOGE("<<child channel fd %d>>", m_channel );

return true;
}

void Child::handle_msg( const char* msg )
{
//TODO How to handle message is decided by you.
}

void Child::listen_msg( )
{
fd_set rfds;

int retry = 0;

while( 1 )
{
FD_ZERO(&rfds);

FD_SET( m_channel, &rfds );

timeval timeout = {3, 0};

int r = select( m_channel + 1, &rfds, NULL, NULL, &timeout );

if( r > 0 )
{
char pkg[256] = {0};

if( FD_ISSET( m_channel, &rfds) )
{
read_from_channel( pkg, sizeof(pkg) );

LOGE("<<A message comes:%s>>", pkg );

handle_msg( (const char*)pkg );
}
}
}
}

void Child::do_work()
{
start_parent_monitor(); //启动监视线程

if( create_channel() )  //等待并且处理来自父进程发送的消息
{
listen_msg();
}
}


子进程在他的do_work中先创建了一个线程轮询其父进程的PID,如果发现变成了1,就会调用restart_parent,在其中调用execlp,执行一下am指令启动JAVA侧的组件,从而实现父进程的重启!这里请留意一下execlp中给am传入的参数,带了--user并加上了之前我们在全局变量中保存的user id,如果不加这个选项,就无法重启父进程,我在这花费了好长时间哦!


子进程剩余的工作很简单,创建通道,监听来自父进程的消息,这里我们用select来监听,由于实际上只有一个客户端(父进程),所以用select有点脱裤子放屁,把简单问题复杂化的嫌疑,但是实际上也没啥太大影响!


有了以上的实现,JNI的实现就相当的简单了:
#include "process.h"
#include "Utils.h"


/**
* 全局变量,代表应用程序进程.
*/
ProcessBase *g_process = NULL;

/**
* 应用进程的UID.
*/
const char* g_userId = NULL;

/**
* 全局的JNIEnv,子进程有时会用到它.
*/
JNIEnv* g_env = NULL;

extern "C"
{
JNIEXPORT jboolean JNICALL Java_com_example_dameonservice_Watcher_createWatcher( JNIEnv*, jobject, jstring);

JNIEXPORT jboolean JNICALL Java_com_example_dameonservice_Watcher_connectToMonitor( JNIEnv*, jobject );

JNIEXPORT jint JNICALL Java_com_example_dameonservice_Watcher_sendMsgToMonitor( JNIEnv*, jobject, jstring );

JNIEXPORT jint JNICALL JNI_OnLoad( JavaVM* , void* );
};

JNIEXPORT jboolean JNICALL Java_com_example_dameonservice_Watcher_createWatcher( JNIEnv* env, jobject thiz, jstring user )
{
g_process = new Parent( env, thiz );

g_userId  = (const char*)jstringTostr(env, user);

g_process->catch_child_dead_signal();

if( !g_process->create_child() )
{
LOGE("<<create child error!>>");

return JNI_FALSE;
}

return JNI_TRUE;
}


JNIEXPORT jboolean JNICALL Java_com_example_dameonservice_Watcher_connectToMonitor( JNIEnv* env, jobject thiz )
{
if( g_process != NULL )
{
if( g_process->create_channel() )
{
return JNI_TRUE;
}

return JNI_FALSE;
}
}

把上面这些代码整合起来,一个双进程守护的实现就完成了,只需要调用一下Watcher.java的createAppMonitor,你的应用就会有一个守护进程来监视,被杀死后也会立刻重新启动起来!是不是很有意思呢?


[!--infotagslink--]

相关文章

  • php读取zip文件(删除文件,提取文件,增加文件)实例

    下面小编来给大家演示几个php操作zip文件的实例,我们可以读取zip包中指定文件与删除zip包中指定文件,下面来给大这介绍一下。 从zip压缩文件中提取文件 代...2016-11-25
  • Jupyter Notebook读取csv文件出现的问题及解决

    这篇文章主要介绍了JupyterNotebook读取csv文件出现的问题及解决,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2023-01-06
  • Android子控件超出父控件的范围显示出来方法

    下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
  • Rstudio中安装package出现的问题及解决

    这篇文章主要介绍了Rstudio中安装package出现的问题及解决方案,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-05-06
  • Photoshop打开PSD文件空白怎么解决

    有时我们接受或下载到的PSD文件打开是空白的,那么我们要如何来解决这个 问题了,下面一聚教程小伙伴就为各位介绍Photoshop打开PSD文件空白解决办法。 1、如我们打开...2016-09-14
  • 解决python 使用openpyxl读写大文件的坑

    这篇文章主要介绍了解决python 使用openpyxl读写大文件的坑,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-03-13
  • C#实现HTTP下载文件的方法

    这篇文章主要介绍了C#实现HTTP下载文件的方法,包括了HTTP通信的创建、本地文件的写入等,非常具有实用价值,需要的朋友可以参考下...2020-06-25
  • SpringBoot实现excel文件生成和下载

    这篇文章主要为大家详细介绍了SpringBoot实现excel文件生成和下载,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-02-09
  • C#操作本地文件及保存文件到数据库的基本方法总结

    C#使用System.IO中的文件操作方法在Windows系统中处理本地文件相当顺手,这里我们还总结了在Oracle中保存文件的方法,嗯,接下来就来看看整理的C#操作本地文件及保存文件到数据库的基本方法总结...2020-06-25
  • php无刷新利用iframe实现页面无刷新上传文件(1/2)

    利用form表单的target属性和iframe 一、上传文件的一个php教程方法。 该方法接受一个$file参数,该参数为从客户端获取的$_files变量,返回重新命名后的文件名,如果上传失...2016-11-25
  • Android开发中findViewById()函数用法与简化

    findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20
  • php批量替换内容或指定目录下所有文件内容

    要替换字符串中的内容我们只要利用php相关函数,如strstr,str_replace,正则表达式了,那么我们要替换目录所有文件的内容就需要先遍历目录再打开文件再利用上面讲的函数替...2016-11-25
  • Android模拟器上模拟来电和短信配置

    如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
  • PHP文件上传一些小收获

    又码了一个周末的代码,这次在做一些关于文件上传的东西。(PHP UPLOAD)小有收获项目是一个BT种子列表,用户有权限上传自己的种子,然后配合BT TRACK服务器把种子的信息写出来...2016-11-25
  • 夜神android模拟器设置代理的方法

    夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
  • AI源文件转photoshop图像变模糊问题解决教程

    今天小编在这里就来给photoshop的这一款软件的使用者们来说下AI源文件转photoshop图像变模糊问题的解决教程,各位想知道具体解决方法的使用者们,那么下面就快来跟着小编...2016-09-14
  • android自定义动态设置Button样式【很常用】

    为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
  • C++万能库头文件在vs中的安装步骤(图文)

    这篇文章主要介绍了C++万能库头文件在vs中的安装步骤(图文),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-23
  • Zend studio文件注释模板设置方法

    步骤:Window -> PHP -> Editor -> Templates,这里可以设置(增、删、改、导入等)管理你的模板。新建文件注释、函数注释、代码块等模板的实例新建模板,分别输入Name、Description、Patterna)文件注释Name: 3cfileDescriptio...2013-10-04
  • php文件上传你必须知道的几点

    本篇文章主要说明的是与php文件上传的相关配置的知识点。PHP文件上传功能配置主要涉及php.ini配置文件中的upload_tmp_dir、upload_max_filesize、post_max_size等选项,下面一一说明。打开php.ini配置文件找到File Upl...2015-10-21