[读书笔记]《APP研发录》第二章

《APP研发录》源码(自己写) 《APP研发录》这本书的源码。然后本人对着书写的。 立即下载

APP研发录第二章笔记


  • 抛弃AsyncTask,自定义一套网络底层的封装框架。
  • 设计一套App缓存策略。
  • 设计一套MockService的机制,在没有MobileAPI的时候,也能假装获取到了网络返回的数据。
  • 封装了用户Cookie的逻辑。

2.1 网络底层封装

很多公司和团队都是用AsyncTask来封装网络底层,因为这个类非常好用,内部封装了很多好用的方法,但缺点是可扩展性不高。

对于网络请求,我们一般定义为GET和POST即可,GET为请求数据,POST为修改数据(增删改)。

1.Request格式

所有的MobileAPI都可以写作http://www.xxx.com/aaaa.api的形式。

  • 对于GET,我们可以写作:http://www.xxx.com/aaaa.api?k1=va&k2=v2的形式,也就是说,把key-value这样的键值对存放在URL上。之所以这样设计,是为了更方便地定义数据缓存。我们尽量使GET的参数都是string、int这样的简单类型。
  • 对于POST,我们将key-value这样的键值对存放在Form表单中,进行提交。POST经常会提交大量数据,所以有些键值对要定义成集合或者复杂的自定义实体,这时我们就需要将这样的值转换为JSON字符串进行提交,由App传递到MobileAPI后,再将JSON字符串转换为对应的实体。

2.Response格式

我们一般使用JSON作为MobileAPI返回的结果。最规范的JSON数据返回格式如下。
JSON数据格式1:

{ 
  "isError" : true,
    "errorType" : 1,
      "errorMessage" : "网络异常",
        "result" : ""
}

JSON数据格式2:

{ 
  "isError" : false,
    "errorType" : 0,
      "errorMessage" : "",
        "result" : {
          "cinemaId" : 1,
            "cinemaName" : "星美"
        }
} 

这里,isError是调用MobileAPI成功与否,errorType是错误类型(如果成功则为0),errorMessage是错误消息(如果成功则为空),result是成功请求返回的数据结果(如果失败则返回空)。

既然所有的JSON都返回isError、errorType、errorMessage、result这4个字段,我们不妨定义一个Response实体类,作为所有JSON实体的最外层,代码如下所示:

public class Response
{
  private boolean error;
  private int errorType;       // 1为Cookie失效
  private String errorMessage;
  private String result;
  public boolean hasError() {
    return error;
  }
  public void setError(boolean hasError) {
    this.error = hasError;
  }
  public String getErrorMessage() {
    return errorMessage;
  }
  public void setErrorMessage(String errorMessage) {
    this.errorMessage = errorMessage;
  }
  public String getResult() {
    return result;
  }
  public void setResult(String result) {
    this.result = result;
  }
  public int getErrorType() {
      return errorType;
  }
  public void setErrorType(int errorType) {
    this.errorType = errorType;
  }
} 

如果成功返回了数据,数据会存放在result字段中,映射为Response实体的result属性。

上面的JSON数据返回的是一笔影院数据,如果返回的result是很多影院的数据集合,那么就要把result解析为相应的实体集合,如下所示:

{ 
  "isError" : false,
    "errorType" : 0,
      "errorMessage" : "",
        "result" : [
          {"cinemaId" : 1, "cinemaName" : "星美"},
          {"cinemaId" : 2, "cinemaName" : "万达"}
        ]
} 
2.1.2 AsyncTask的使用和缺点

对AsyncTask的封装属于网络底层的技术,所以AsyncTask应该封装在AndroidLib类库中,而不是具体的项目里。
对网络异常的分类,也就是Response类中的errorType字段,分析如下:

  • 一种是请求发送到MobileAPI,MobileAPI执行过程中发现的异常,这时候要自定义错误类型,也就是errorType,比如说1是Cookie过期,2是第三方支付平台不能连接,等等,这些已知的错误都是大于0的整数,因接口不同而各自定义不同。
  • 另一种是在App访问MobileAPI接口时发生的异常,有可能App自身网络不稳定,有可能因为网络传输不好导致返回了空值,这些异常情况我们都标记为负数。

基于上述分析,AsyncTask的doInBackground方法复写为:

@Override
protected Response doInBackground(String… url) {
  return getResponseFromURL(url[0]);
}
private Response getResponseFromURL(String url) {
  Response response = new Response();
  HttpGet get = new HttpGet(url);
  String strResponse = null;
  try {
    HttpParams httpParameters = new BasicHttpParams();
    HttpConnectionParams.setConnectionTimeout(httpParameters, 8000);
    HttpClient httpClient = new DefaultHttpClient(httpParameters);
    HttpResponse httpResponse = httpClient.execute(get);
    if (httpResponse.getStatusLine().getStatusCode() 
        == HttpStatus.SC_OK) {
      strResponse = EntityUtils.toString(httpResponse.getEntity());
    }
  } catch (Exception e) {
    response.setErrorType(-1);
    response.setError(true);
    response.setErrorMessage(e.getMessage());
  }
  if (strResponse == null) {
      response.setErrorType(-1);
    response.setError(true);
    response.setErrorMessage("网络异常, 返回空值");
  } else {
    strResponse = "{'isError':false,'errorType':0,'errorMessage':'',
    'result':{'city':'北京','cityid':'101010100','temp':'17',
      'WD':'西南风','WS':'2级','SD':'54%','WSE':'2','time':'23:15',
        'isRadar':'1','Radar':'JC_RADAR_AZ9010_JB',
          'njd':'暂无实况','qy':'1016'}}";
          response = JSON.parseObject(strResponse, Response.class);
}
return response;
} 

相应的,在AsyncTask的onPostExecute方法中,我们要对错误类型进行分类,从而进一步回调:

public abstract class RequestAsyncTask 
  extends AsyncTask<String, Void, Response> {
    public abstract void onSuccess(String content);
    public abstract void onFail(String errorMessage);
    @Override
      protected void onPreExecute() {
    }
    @Override
      protected void onPostExecute(Response response) {
      if(response.hasError()) {
        onFail(response.getErrorMessage());
      } else {
        onSuccess(response.getResult());
      }
    } 

目前我们只定义了onSuccess和onFail两个回调函数,将网络返回值简单地分为成功与失败两种情况。

在相应的Activity页面,调用AysncTask如下所示:

protected void loadData() {
  String url = "http://www.weather.com.cn/data/sk/101010100.html";
  RequestAsyncTask task = new RequestAsyncTask() {
    @Override
    public void onSuccess(String content) {
      // 第2种写法, 基于fastJSON
      WeatherEntity weatherEntity = JSON.parseObject(content,
                                                     WeatherEntity.class);
      WeatherInfo weatherInfo = weatherEntity.getWeatherInfo();
      if (weatherInfo != null) {
        tvCity.setText(weatherInfo.getCity());
        tvCityId.setText(weatherInfo.getCityid());
      }
    }
     @Override
     public void onFail(String errorMessage) {
       new AlertDialog.Builder(WeatherByFastJsonActivity.this)
         .setTitle("出错啦").setMessage(errorMessage)
         .setPositiveButton("确定", null).show();
     }
  };
  task.execute(url);
} 

网上关于如何使用AsyncTask的文章不胜枚举,大家都在欣赏它的优点,却忽略了它的致命缺点,那就是不能灵活控制其内部的线程池。

线程池里面的每个线程存放的都是MobileAPI的调用请求,而AsyncTask中又没有暴露出取消这些请求的方法,也就是我们熟知的CancelRequest方法,所以,一旦从A页面跳转到B页面,那么在A页面发起的MobileAPI请求,如果还没有返回,并不会被取消。

对于一款频繁调用MobileAPI的应用类App而言,最严重的情况发生在首页到二级页面的跳转,因为在首页会调用十几个MobileAPI接口,视网络情况而定,如果是WiFi,应该很快就能请求到数据,不会产生积压,但如果是3G或者2G,那么请求就会花费很长时间,而我们在这期间就跳转到二级页面,而这个二级页面也会调用MobileAPI接口,那么将得不到任何结果,因为首页的请求还在排队处理中,之前的那十几个MobileAPI接口的数据还都遥遥无期在线程池里排队呢,就更不要说当前页面这个请求了。

如果你不信,我们可以做个试验。记录每次MobileAPI请求发起和接收数据的时间点,你会看到,在迅速进入二级页面后,首页的十几个MobileAPI请求只有发起时间并没有返回时间,说明它们还在处理过程中,都被堵塞了。

2.1.3 使用原生的ThreadPoolExcutor+Runnable+Handler

既然AsyncTask有诸多问题,那么退而求其次,使用ThreadPoolExecutor+Runnable+Handler的原生方式,对网络底层进行封装。

建议大家下载源码感受一些,源码下载地址:

《App研发录》 源码

2.1.4 网络底层的一些优化工作

接下来将完善这个框架,修复其中的一些瑕疵,如onFail的统一处理机制、UrlConfigManager的优化、ProgressBar的处理等。

1.onFail的统一处理机制

如果访问MobileAPI请求失败,我们一般希望只是在App上简单地弹出一个提示框,告诉用户网络有异常。

也就是说,对于每个在Activity中声明的RequestCallback实例而言,尽管每个onSuccess方法的处理逻辑各不相同,但每个onFail方法都是一样的逻辑和代码,如下所示:

weatherCallback = new RequestCallback() {
  @Override
  public void onSuccess(String content) {
    WeatherInfo weatherInfo = JSON.parseObject(content,
                                               WeatherInfo.class);
    if (weatherInfo != null) {
      tvCity.setText(weatherInfo.getCity());
      tvCityId.setText(weatherInfo.getCityid());
    }
  }
   @Override
   public void onFail(String errorMessage) {
     new AlertDialog.Builder(WeatherByFastJsonActivity.this)
       .setTitle("出错啦").setMessage(errorMessage)
       .setPositiveButton("确定", null).show();
   }
}; 

我不希望每次都编写同样的onFail方法,这会使程序很臃肿。于是在AppBaseActivity中写一个自定义类AbstractRequestCallback,如下所示:

public abstract class AppBaseActivity extends BaseActivity {
  public abstract class AbstractRequestCallback 
    implements RequestCallback {
    public abstract void onSuccess(String content);
    public void onFail(String errorMessage) {
      new AlertDialog.Builder(AppBaseActivity.this)
        .setTitle("出错啦").setMessage(errorMessage)
        .setPositiveButton("确定", null).show();
    }
      }
} 

那么我们的weatherRequestCallback的实例化就可以改写如下,可以看到,不再需要重写onFail方法:

weatherCallback = new AbstractRequestCallback() {
  @Override
  public void onSuccess(String content) {
    WeatherInfo weatherInfo = JSON.parseObject(content,
                                               WeatherInfo.class);
    if (weatherInfo != null) {
      tvCity.setText(weatherInfo.getCity());
      tvCityId.setText(weatherInfo.getCityid());
    }
  }
  }; 

当然,如果有些MobileAPI接口在返回错误时需要App特殊处理,比如重启App或者啥都不做,我们只需要在实例化AbstractRequestCallback时,重写onFail方法即可,如下所示。重写的onFail方法是一个空方法,表示出错时啥都不做:

weatherCallback = new AbstractRequestCallback() {
  @Override
  public void onSuccess(String content) {
    WeatherInfo weatherInfo = JSON.parseObject(content,
                                               WeatherInfo.class);
    if (weatherInfo != null) {
      tvCity.setText(weatherInfo.getCity());
      tvCityId.setText(weatherInfo.getCityid());
    }
      }
   @Override
   public void onFail(String errorMessage) {
     // 重启App或者啥都不做
   }
}; 

2.UrlConfigManager的优化

在UrlConfigManager的实现上,我们采取的策略是每发起一次MobileAPI请求,都会读取url.xml文件,把符合这次MobileAPI接口调用的参数取出来。

在一个大量调用MobileAPI的App中,这样的设计会造成频繁读xml文件,性能很差。于是我们对其进行改造,在App启动时,一次性将url.xml文件都读取到内存,把所有的UrlData实体保存在一个集合中,然后每次调用MobileAPI接口,直接从内存的这个集合中查找。考虑到内存中的数据会被回收,所以上述这个集合一旦为空,我们要从url.xml中再次读取。

基于上述方案,我们对UrlConfigManager的findUrl方法进行改造:

public static URLData findURL(final Activity activity, 
                              final String findKey) {
  // 如果urlList还没有数据(第一次)
  // 或者被回收了, 那么(重新)加载xml
  if (urlList == null || urlList.isEmpty())
    fetchUrlDataFromXml(activity);
  for (URLData data : urlList) {
    if (findKey.equals(data.getKey())) {
      return data;
    }
  }
  return null;
} 

其中,fetchUrlDataFromXml方法就不多说了,它的工作就是把xml的数据都搬到内存集合urlList中。

3.不是每个请求都需要回调的

有些时候,我们调用一个MobileAPI接口,并不需要知道调用成功与否以及返回结果是什么,比如向MobileAPI发送打点统计数据。那就是说,我们不需要回调函数了,那么代码可以写为:

void loadAPIData3() {
  ArrayList<RequestParameter> params 
    = new ArrayList<RequestParameter>();
  RequestParameter rp1 = 
    new RequestParameter("cityId", "111");
  RequestParameter rp2 = 
    new RequestParameter("cityName", "Beijing");
  params.add(rp1);
  params.add(rp2);
  RemoteService.getInstance()
    .invoke(this, "getWeatherInfo", params, null);
} 

我们将空的RequestCallback传给HttpRequest,那么在HttpRequest处理请求返回的结果时,就需要添加HttpRequest是否为空的判断,不为空,才会处理返回结果;否则,发起MobileAPI请求后什么都不做。

有以下两个地方需要修改:

1)处理请求时:

response = httpClient.execute(request);
if ((requestCallback != null)) {
  // 获取状态
  final int statusCode = 
    response.getStatusLine().getStatusCode();
  if (statusCode == HttpStatus.SC_OK) {
    final ByteArrayOutputStream content = 
      new ByteArrayOutputStream(); 

2)遇到异常,是否要回调onFail方法:

public void handleNetworkError(final String errorMsg) {
  if ((requestCallback != null)) {
    handler.post(new Runnable() {
      @Override
        public void run() {
        HttpRequest.this.requestCallback
          .onFail(errorMsg);
      }
    });
  }
} 

4.ProgressBar的处理

在调用MobileAPI的时候,会显示进度条ProgressBar,直到返回结果到onSuccess或onFail回调方法,ProgressBar才会消失。

由于App要保持风格统一,所以所有页面的ProgressBar应该长得一样。那么我们就可以将其定义在AppBaseActivity中,如下所示:

public abstract class AppBaseActivity extends BaseActivity {
  protected ProgressDialog dlg;
  public abstract class AbstractRequestCallback 
    implements RequestCallback {
    public abstract void onSuccess(String content);
    public void onFail(String errorMessage) {
      dlg.dismiss();
          public void onFail(String errorMessage) {
      dlg.dismiss();
      new AlertDialog.Builder(AppBaseActivity.this)
        .setTitle("出错啦").setMessage(errorMessage)
        .setPositiveButton("确定", null).show();
    }
  }
} 

在使用的时候,在开始调用MobileAPI的地方,执行show方法;在onSuccess和onFail方法的开始,执行dismiss方法:

@Override
protected void loadData() {
  dlg = Utils.createProgressDialog(this,
                                   this.getString(R.string.str_loading));
  dlg.show();
  loadAPIData1();
}
void loadAPIData1() {
  weatherCallback = new AbstractRequestCallback() {
    @Override
    public void onSuccess(String content) {
      dlg.dismiss();
      WeatherInfo weatherInfo = JSON.parseObject(content,
                                                 WeatherInfo.class);
      if (weatherInfo != null) { 

不要把Dialog的show方法和dismiss方法封装到网络底层。网络底层的调用经常是在子线程执行的,子线程是不能操作Dialog、Toast和控件的。

2.2 App数据缓存设计

如果以为上一节内容就是网络底层框架的全部,那就错了。那只是网络底层框架的最核心的功能,我们还有很多高级功能没有介绍。在接下来的几节中,我将陆续介绍到这些高级功能。本节先介绍App本地的缓存策略。

2.2.1 数据缓存策略

对于任何一款应用类App,如果访问MobileAPI的速度和牛车一样慢,那么就是失败之作。不要在WiFi下测试速度,那是自欺欺人,要把App放在2G或3G网络环境下进行测试,才能得到大部分用户的真实数据。

访问MobileAPI,主要慢在一来一回的传输速度上,对于服务器的处理速度,不需要担心太多,大多数服务器逻辑原本就是支持网站端的,现在只是在外面包了一层,返回给App而已。

既然时间主要花在了数据传输上,那么我们就要想一些应对的措施。比如说,减少MobileAPI的调用次数。对于一个App页面,它一次性可能需要3部分数据,分别从3个MobileAPI接口获取,那么我们就可以做一个新的MobileAPI接口,将这3部分数据都获取到,然后一次性返回。

减少调用次数只是若干解决方案中的一种,更极端的做法是,App调用一次MobileAPI接口后,在一个时间段内不再调用,仍然使用上次调用接口获取到的数据,这些数据保存在App上,我们称为App缓存,这个时间段我们称为App缓存时间。

App缓存只能针对于MobileAPI中GET类型的接口,对于POST不适用。因为GET是获取数据,而POST是修改数据。

此外,即使是GET类型的接口,对于那些即时性很低的、不怎么改变的数据,比如获取商品的描述,缓存时间可以设置得比较长,比如5~10分钟,对于那些即时性比较高、频繁变动的数据,比如商品价格,缓存时间就会比较短,甚至不能进行缓存。

即使对于同一个需要做App缓存的MobileAPI,参数不同,缓存也是不同的。比如GetWeather.api这个MobileAPI接口,它有一个参数也就是时间date,对于date=2014-9-8和date=2014-9-9,它们就分别对应两个缓存,不能存在一起。

接下来要说的是,App缓存存在哪里,以及以什么方式进行存放。由于缓存数据比较大,所以我们将其存在SD卡上,而不是内存中。这样的话,App缓存策略就仅限于那些有SD卡的手机用户了。

我们可以将xxx.apik1=va&k2=v2这样的URL格式作为key,存放App缓存数据。需要注意的是,我们要对k1、k2这些key进行排序,这样才能唯一,否则对于如下URL:

xxxx.apik1=v1&k2=v2
xxxx.apik2=v2&k1=v1 

就会被认为是两个不同的key存放在缓存中,但其实它们是一样的。
对上面的介绍总结如下:

1)对于App而言,它是感受不到取的是缓存数据还是调用MobileAPI。具体工作由网络底层完成。

2)在url.xml中为每一个MobileAPI接口配置缓存时间Expired。对于post,一律设置为0,因为post不需要缓存。

3)在HttpRequest类中的run方法中,改动3个地方:

a)写一个排序算法sortKeys,对URL中的key进行排序。

b)将newUrl作为key,检查缓存中是否有数据,有则直接返回;否则,继续调用MobileAPI接口。

// 如果这个get的API有缓存时间(大于0)
if (urlData.getExpires() > 0) {
  final String content = CacheManager.getInstance()
    .getFileCache(newUrl);
  if (content != null) {
    handler.post(new Runnable() {
      @Override
        public void run() {
        requestCallback.onSuccess(content);
      }
    });
    return;
  }
} 

c)MobileAPI接口返回数据后,将数据存入缓存。

final Response responseInJson = JSON.parseObject(
  strResponse, Response.class);
if (responseInJson.hasError()) {
  handleNetworkError(responseInJson.getErrorMessage());
} else {
  // 把成功获取到的数据记录到缓存
  if (urlData.getNetType().equals(REQUEST_GET)
      && urlData.getExpires() > 0) {
    CacheManager.getInstance().putFileCache(newUrl,
                                            responseInJson.getResult(),
                                            urlData.getExpires());
  }
  handler.post(new Runnable() {
    @Override
      public void run() {
      requestCallback.onSuccess(responseInJson
                                .getResult());
    }
  });
} 

4)CacheManager用于操作读写缓存数据,并判断缓存数据是否过期。缓存中存放的实体就是CacheItem。

5)在App项目中,创建YoungHeartApplication这个Application级别的类,在程序启动时,初始化缓存的目录,如果不存在则创建之。

public class YoungHeartApplication extends Application {
  @Override
    public void onCreate() {
    super.onCreate(); 
    CacheManager.getInstance().initCacheDir();
  }
} 
2.2.2 强制更新

不光是App端需要记录缓存数据,在MobileAPI的很多接口,其实也需要一样的设计。

如果对于某个接口的数据,MobileAPI缓存了5分钟,App缓存了3分钟,那么最极端的情况是,用户在8分钟内是看不到数据更新的。因此,我们需要在页面上提供一个强制更新的按钮。

我们可以让RemoteService多暴露一个boolean类型的参数,用于判断是否要遵守App端缓存策略,如果是,则在从url.xml中取出UrlData实体后,将其expired强制设置为0,这样就不会执行缓存策略了。

RemoteService的改动如下:

public void invoke(final BaseActivity activity,
                   final String apiKey,
                   final List<RequestParameter> params,
                   final RequestCallback callBack) {
  invoke(activity, apiKey, params, callBack, false);
}
public void invoke(final BaseActivity activity,
                   final String apiKey,
                   final List<RequestParameter> params,
                   final RequestCallback callBack,
                   final boolean forceUpdate) {
  final URLData urlData = 
    UrlConfigManager.findURL(activity, apiKey);
  if(forceUpdate) {
    // 如果强制更新, 那么就把过期时间强制设置为0
    urlData.setExpires(0); 
  }
  HttpRequest request = 
    activity.getRequestManager().createRequest(
    urlData, params, callBack);
  DefaultThreadPool.getInstance().execute(request);
} 

那么在调用的时候,只需要加一个参数就好:

RemoteService.getInstance().invoke(
  this, "getWeatherInfo", params,
  weatherCallback); 

2.3 MockService

设计App端MockService包括如下几个关键点:

1)对需要Mock数据的MobileAPI接口,通过在url.xml中配置Node节点MockClass属性,来指定要使用那个Mock子类生成的数据:

<Node
    Key="getWeatherInfo"
    Expires="300"
    NetType="get"
    MockClass="com.youngheart.mockdata.MockWeatherInfo"
    Url="http://www.weather.com.cn/data/sk/101010100.html" /> 

这里将使用com.mockdata.mockdata包下的MockWeatherInfo子类来解析。

2)我使用了反射工厂来设计MockService。MockService类是基类,它有一个抽象方法getJsonData,用于返回手动生成的Mock数据。

public abstract class MockService {
  public abstract String getJsonData();
} 

每个要Mock数据的MobileAPI接口,都对应一个继承自MockService的子类,都要实现各自的getJsonData方法,返回不同的JSON数据。

比如在上述url.xml中声明的MockWeatherInfo,它对应的类实现如下:

public class MockWeatherInfo extends MockService {
  @Override
  public String getJsonData() {
    WeatherInfo weather = new WeatherInfo();
    weather.setCity("Beijing");
    weather.setCityid("10000");
    Response response = getSuccessResponse();
    response.setResult(JSON.toJSONString(weather));
    return JSON.toJSONString(response);
  }
  } 

3)接下来介绍如何实现反射机制。

主要的改造工作在RemoteService类的invoke方法中,根据是否在url.xml中指定了MockClass值来决定,是调用线上MobileAPI还是从本地MockService直接取假数据。

如果MockClass有值,就把这个值反射为一个具体的类,比如MockWeatherInfo,然后调用它的getJsonData方法。

public void invoke(final BaseActivity activity, 
                   final String apiKey,
                   final List<RequestParameter> params, 
                   final RequestCallback callBack) {
  final URLData urlData = UrlConfigManager.findURL(activity, apiKey);
  if (urlData.getMockClass() != null) {
    try {
      MockService mockService = (MockService) Class.forName(
        urlData.getMockClass()).newInstance();
      String strResponse = mockService.getJsonData();
      final Response responseInJson = 
        JSON.parseObject(strResponse, Response.class);
      if (callBack != null) {
        if (responseInJson.hasError()) {
          callBack.onFail(responseInJson.getErrorMessage());
        } else {
          callBack.onSuccess(responseInJson.getResult());
        }
      }
    } catch (ClassNotFoundException e) {
      e.printStackTrace();
    } catch (InstantiationException e) {
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      e.printStackTrace();
    }
  } else {
    HttpRequest request = 
      activity.getRequestManager().createRequest(
      urlData, params, callBack);
    DefaultThreadPool.getInstance().execute(request);
  }
} 

2.4 用户登录

2.4.1 登录成功后的各种场景

首先,贯穿App的,应该有一个User全局变量,在每次登录成功后,会将其isLogin属性设置为true,在退出登录后,则将该属性设置为false。这个User全局变量要支持序列化到本地的功能,这样数据才不会因内存回收而丢失。

其次,登录分为3种情形:

情形1:点击登录按钮,进入登录页面LoginActivity,登录成功后,直接进入个人中心PersonCenterActivity。这种情况最直截了当,一路执行startActivity(intent)就能达到目的。

情形2:在页面A,想要跳转到页面B,并携带一些参数,却发现没有登录,于是先跳转到登录页,登录成功后,再跳转到B页面,同时仍然带着那些参数。

这就主要是setResult(intent,resultCode)发挥作用的时候了,Activity的回调机制这时候派上了用场,如下所示:

btnLogin2.setOnClickListener(new OnClickListener(){
  @Override
  public void onClick(View v) {
  if(User.getInstance().isLogin()) {
      gotoNewsActivity();
    } else {
      Intent intent = new Intent(LoginMainActivity.this, 
                                 LoginActivity.class);
      intent.putExtra(AppConstants.NeedCallback, true);
      startActivityForResult(intent, 
                             LOGIN_REDIRECT_OUTSIDE);
    }
  }
}); 

情形3:在页面A,执行某个操作,却发现没有登录,于是跳转到登录页,登录成功后,再回到页面A,继续执行该操作。

处理方式同于情形2,也是使用setResult来完成回调。

btnLogin3.setOnClickListener(new OnClickListener(){
  @Override
  public void onClick(View v) {
    if(User.getInstance().isLogin()) {
      changeText();
    } else {
      Intent intent = new Intent(LoginMainActivity.this, 
                                 LoginActivity.class);
      intent.putExtra(AppConstants.NeedCallback, true);
      startActivityForResult(intent, 
                             LOGIN_REDIRECT_INSIDE);
    }
  }
}); 

无论是上述哪种情形,登录页面LoginActivity只有一个,所以要把上面的三个逻辑整合在一起,如下所示:

RequestCallback loginCallback = new AbstractRequestCallback() {
@Override
  public void onSuccess(String content) {
    UserInfo userInfo = JSON.parseObject(content,
                                         UserInfo.class);
    if (userInfo != null) {
      User.getInstance().reset();
      User.getInstance().setLoginName(userInfo.getLoginName());
      User.getInstance().setScore(userInfo.getScore());
      User.getInstance().setUserName(userInfo.getUserName());
      User.getInstance().setLoginStatus(true);
      User.getInstance().save();
    }
    if(needCallback) {
      setResult(Activity.RESULT_OK);
      finish();
    } else {
      Intent intent = new Intent(LoginActivity.this, 
                                 PersonCenterActivity.class);
      startActivity(intent);
    }
  }
  }; 

整合的关键在于从上个页面传过来needCallback变量,它决定了是否要回到上个页面。

另一方面,我们看到,在登录成功后,我们会把用户信息存储到User这个全局变量并序列化到本地,这是因为各个模块都有可能使用到用户的信息。其中LoginStatus是关键,接下来的篇幅将着重谈论这个属性。

最后在LoginMainActivity中的onActivityResult回调函数,它负责处理登录后的事情,如下所示:

@Override
  protected void onActivityResult(int requestCode, 
                                  int resultCode, Intent data) {
  if (resultCode != Activity.RESULT_OK) {
    return;
  }
  switch (requestCode) {
    case LOGIN_REDIRECT_OUTSIDE:
    gotoNewsActivity();
    break;
    case LOGIN_REDIRECT_INSIDE:
    changeText();
    break;
    default:
    break;
  }
} 

我们看到,对于情形2,当用户在LoginMainActivity点击按钮想跳转到NewsActivity,如果已经登录,就直接跳转过去;否则,先到LoginActivity登录,然后回调LoginMain-Activity的onActivityResult,仍然跳转到NewsActivity。

2.4.2 自动登录

所谓自动登录,就是登录成功后,重启App后用户仍然是登录状态。

最直接的方法是,登录成功后,本地保存用户名和密码。重启App后,检查本地是否有保存用户名和密码,如果有,则将用户名和密码传入到登录接口,模拟用户登录的行为。

但这样就有安全风险了,分析如下:

  • 本地保存用户密码,这种的敏感信息容易被人窃取。要么是在本地文件中看到这些信息,要么是侦听App的网络请求,获取到请求的数据。
  • 所以本地保存密码时,一定要进行加密。对称加密是不可靠的,因为很难确保App的源代码不外泄,所以别有用心的人还是可以根据源码中的对称加密算法,反向把密码推算出来。只有不对称加密才是安全的。
  • 那么登录之后呢?市面上大多数App的逻辑都有问题,它们会在本地保存一个isLogin的全局变量,登录成功后设置为true。接下来涉及用户相关的MobileAPI,只有在这个值为true时才能调用,它们会把UserId传递给服务器。
  • 服务器的解决方案通常也很简陋,它没有任何安全机制,包括用户信息相关的MobileAPI接口,只要接口调用参数中有UserId,它就会去把相关的数据取出并返回。
  • 一种补救措施是,每次调用用户相关的MobileAPI接口时,都需要把UserId和加密后的密码一起传递。而服务器需要对那些用户相关的MobileAPI接口加上安全验证机制,每次请求都检查用户名和密码是否正确。我们要求密码是经过哈希散列算法不对称加密过的,是无法还原的。服务器的验证工作是根据传过来的UserId从数据库中取出相应的密码,然后进行比对。注意,数据库中存放的密码是在注册的时候经过哈希散列算法加密过的。
  • 本地保存用户名和密码的另一个问题是,每次用户启动App,登录页都会一闪而过,因为它要模拟用户登录的行为:假装输入用户名和密码,然后假装点击登录按钮。这样做用户体验很不好倒是其次,关键是这种做法有个无法自圆其说的硬伤——出于安全考虑,我们要修改登录接口,使其除了接收用户名和密码这两个参数外,还必须接收验证码,也就是动态口令.

我们知道,验证码必须是手动输入的,否则就失去了它存在的意义。但是当前这种自动登录的做法,我们只知道用户名和密码,而不知道每次生成的验证码是什么,所以就不能自动登录了。是时候该抛弃这种每次启动就进行一次登录的机制了,其实Web在这一点已经做得很成熟了,那就是Cookie机制。

也有的人管Cookie叫Token,这是用户身份的唯一性标志。

首先,App在登录成功后,会从服务器获取到一个Cookie,这个Cookie存放在Http-Response的header中,如下所示:

Set-Cookie: customer=huangxp; path=/foo; domain=.ibm.com; 
expires= Wednesday, 19-OCT-05 23:12:40 GMT; [secure] 

我们将其取出来,不用关心它是什么,只要把它存放在本地文件中即可。

我们需要修改App的网络底层,也就是HttpRequest类,分以下几步:

1)每次发起MobileAPI请求时,都要把本地保存的Cookie取出来,放到HttpRequest的header中。还是那句话,不用管Cookie是什么,也不管Cookie是否有值,都应如下操作:

// 添加Cookie到请求头中
addCookie();
// 发送请求
response = httpClient.execute(request); 

2)每次接收MobileAPI的相应结果时,都把HttpResponse的header里面的Cookie取出来,覆盖本地保存的Cookie。不用管Cookie有值与否,如下所示:

if (urlData.getNetType().equals(REQUEST_GET)
    && urlData.getExpires() > 0) {
  CacheManager.getInstance().putFileCache(newUrl,
                                          responseInJson.getResult(),
                                                                                    urlData.getExpires());
}
handler.post(new Runnable() {
  @Override
  public void run() {
    requestCallback.onSuccess(responseInJson
                              .getResult());
  }
});
// 保存Cookie
saveCookie(); 

以下是addCookie和saveCookie方法的实现:

public void addCookie() {
  List<SerializableCookie> cookieList = null;
  Object cookieObj = BaseUtils.restoreObject(cookiePath);
  if (cookieObj != null) {
    cookieList = (ArrayList<SerializableCookie>) cookieObj;
  }
  if ((cookieList != null) && (cookieList.size() > 0)) {
    final BasicCookieStore cs = new BasicCookieStore();
    cs.addCookies(cookieList.toArray(new Cookie[] {}));
    httpClient.setCookieStore(cs);
  } else {
    httpClient.setCookieStore(null);
  }
}
public synchronized void saveCookie() {
  // 获取本次访问的cookie
  final List<Cookie> cookies = 
    httpClient.getCookieStore().getCookies();
  // 将普通cookie转换为可序列化的cookie
  List<SerializableCookie> serializableCookies = null;
  if ((cookies != null) && (cookies.size() > 0)) {
    serializableCookies = new ArrayList<SerializableCookie>();
    for (final Cookie c : cookies) {
      serializableCookies.add(new SerializableCookie(c));
    }
        }
  }
  BaseUtils.saveObject(cookiePath, serializableCookies);
} 

而服务器的相应操作,对于来自App的请求:

3)如果是用户信息相关的,则判断HttpRequest中Cookie是否有效,如果有效,就去执行后续的逻辑并返回结果;否则,返回Cookie过期失效的错误信息。

4)如果是用户无关的,则不需要检查HttpRequest中Cookie,直接执行下面的逻辑即可。

此外,还需要注意几个地方,都是些琐碎的工作:

  • 用户注销功能,要把本地保存的Cookie清空。App判断用户是否登录的标志,就是Cookie是否为空。
  • 用户注册功能,一般在注册成功后,都会拿着用户名和密码再调用一次登录接口,这就又和验证码功能冲突了,解决方案是注册成功后直接跳转到登录页面,让用户手动再输入一次。这是从产品层面来解决问题。另一种解决方案是,注册成功后进入个人中心页面,不需要再登录一次,而是把注册和登录接口绑在一起。
  • 对于Cookie过期,App应该跳转到登录页面,让用户手动进行登录。这里有一个比较有挑战性的工作,就是登录成功后,应该返回手动登录之前的那个页面。我们在下一节再细说这个技术。
2.4.3 Cookie过期的统一处理

Cookie不是一直有效的,到了一定时间就会失效。

Cookie过期的表现是,当访问MobileAPI某个接口的时候,就不会返回数据了,代之以Cookie过期的错误消息,这时要统一处理。

我们要求MobileAPI在遇到这种情况时,直接返回以下内容的JSON,其中,errorType固定为1:

{
  "isError" : true,
    "errorType" : 1,
      "errorMessage" : "Cookie失效, 请重新登录",
        "result" : ""
} 

为此我们修改AndroidLib,使之支持Cookie失效的场景。

1)在RequestCallback中增加一种onCookieExpired回调方法,如下所示:

public interface RequestCallback
{
  public void onSuccess(String content);
  public void onFail(String errorMessage);
  public void onCookieExpired();
} 

2)在网络底层对JSON返回结果进行解析,如果发现是属于Cookie过期的错误类型,就直接回调onCookieExpired方法,如下所示:

final Response responseInJson = JSON.parseObject(
  strResponse, Response.class);
if (responseInJson.hasError()) {
  if(responseInJson.getErrorType() == 1) {
    handler.post(new Runnable() {
      @Override
        public void run() {
        requestCallback.onCookieExpired();
      }
    });
  } else {
    handleNetworkError(responseInJson.getErrorMessage());
  } 

我们模拟一种场景,在CookieExpiredActivity页面,访问天气预报这个MobileAPI接口,如果Cookie失效,则弹出对话框,通知用户“Cookie过期,请重新登录”,点击确定按钮,将跳转到登录页。登录成功后,将回到上一个页面,即CookieExpiredActivity。

由于所有页面处理Cookie过期的逻辑都是相同的,所以我们将其封装到基类AppBaseActivity中,放在和onFail方法平级的位置:

public void onCookieExpired() {
  dlg.dismiss();
  new AlertDialog.Builder(AppBaseActivity.this)
  .setTitle("出错啦")
  .setMessage("Cookie过期, 请重新登录")
  .setPositiveButton("确定",
                     new DialogInterface.OnClickListener() {
                       @Override
                       public void onClick(DialogInterface dialog,
                                                                  int which) {
                         Intent intent = new Intent(
                           AppBaseActivity.this,
                           LoginActivity.class);
                         intent.putExtra(AppConstants.NeedCallback,
                                         true);
                         startActivity(intent);
                       }
                     }).show();
} 
2.4.4 防止黑客刷库
  • MobileAPI在发现有同一IP短时间内频繁访问某一个MobileAPI接口时,就直接返回一段HTML5,要求用户输入验证码。
  • App在接收到这段代码时,就在页面上显示一个浮层,里面一个WebView,显示这个要求用户输入验证码的HTML5。

2.5 HTTP头中的奥妙

2.5.1 HTTP请求

HTTP请求分为HTTPRequest和HTTPResponse两种。但无论哪种请求,都由header和body两部分组成。

1.HTTP Body

Body部分就是存放数据的地方,回顾一下我们在HTTPRequest类中封装的网络请求:

1)对于get形式的HTTPRequest,要发送的数据都以键值对的形式存放在URL上,比如aaa.apik1=va&k2=va。它的Body是空的,如下所示:

if (urlData.getNetType().equals(REQUEST_GET)) {
  // 添加参数
  final StringBuffer paramBuffer = new StringBuffer();
  if ((parameter != null) && (parameter.size() > 0)) {
    // 这里要对key进行排序
    sortKeys();
    for (final RequestParameter p : parameter) {
      if (paramBuffer.length() == 0) {
        paramBuffer.append(p.getName() + "="
                           + BaseUtils.UrlEncodeUnicode(p.getValue()));
      } else {
        paramBuffer.append("&" + p.getName() + "="
                           + BaseUtils.UrlEncodeUnicode(p.getValue()));
      }
    }
    newUrl = url + "?" + paramBuffer.toString();
  } else {
    newUrl = url;
  }
  request = new HttpGet(newUrl);
} 

2)对于post形式的HTTPRequest,要发送的数据都存在Body里面,也是以键值对的形式,所以代码编写与get情形完全不同,如下所示:

else if (urlData.getNetType().equals(REQUEST_POST)) {
  request = new HttpPost(url);
  // 添加参数
  if ((parameter != null) && (parameter.size() > 0)) {
    final List<BasicNameValuePair> list = 
      new ArrayList<BasicNameValuePair>();
    for (final RequestParameter p : parameter) {
      list.add(new BasicNameValuePair(
        p.getName(), p.getValue()));
    }
    ((HttpPost) request).setEntity(
      new UrlEncodedFormEntity(list, HTTP.UTF_8));
  }
} 

2.HTTP Header

与Body相比,HTTP header就丰富的多了。它由很多键值对(key-value)组成,其中有些key是标准的,兼容于各大浏览器,比如:

  • accept
  • accept-language
  • referrer
  • user-agent
  • accept-encoding

此外,我们还可以在MobileAPI端自定义一些键值对,然后要求App在调用MobileAPI时把这些信息传递过来。比如MobileAPI可以定义一个check-value这样的key,然后要求App将AppId(同一公司的不同App编号)、ClientType(Android还是iPhone、iPad)这些值拼接在一起经过MD5加密后,作为这个key的值传递给MobileAPI,然后由MobileAPI再去分析这些数据。
对于App开发人员而言,只要按照MobileAPI的要求,把这些key所需要的值拼接成HTTPRequest头正确传递过去即可。如下所示:

void setHttpHeaders(final HttpUriRequest httpMessage)
{
  headers.clear();
  headers.put(FrameConstants.ACCEPT_CHARSET, "UTF-8,*");
  headers.put(FrameConstants.USER_AGENT, 
              "Young Heart Android App ");
  if ((httpMessage != null) && (headers != null))
  {
    for (final Entry<String, String> entry : headers.entrySet())
    {
      if (entry.getKey()!=null)
      {
        httpMessage.addHeader(entry.getKey(), entry.getValue());
      }
    }
  }
} 

我们在组装Cookie之前调用setHttpHeaders方法:

// 添加必要的头信息
setHttpHeaders(request);
// 添加Cookie到请求头中
addCookie();
// 发送请求
response = httpClient.execute(request); 

而在返回数据时,也可以从HTTP Response头中把所需要的数据解析出来。Android SDK将其封装成了若干方法以供调用。我们在下面的章节将会看到。

前面我们介绍过Cookie,其实也是HTTP头的一部分。它的作用我们已经见识过了。下面将讨论HTTP头中的另几个重要字段。

2.5.2 时间校准

接下来要介绍的是HTTP Response头中另一重要属性:Date,这个属性中记录了MobileAPI当时的服务器时间。

为什么说这个属性很重要呢?App开发人员经常遇到的一个bug就是,App显示的时间不准,经常会因为时区问题前后差几个小时,而接到用户的投诉。

为了解决这个问题,要从MobileAPI和App同时做一些工作。MobileAPI永远使用UTC时间。包括入参和返回值,都不要使用Date格式,而是减去UTC时间1970年1月1日的差值,这是一个long类型的长整数。

在App端比较麻烦。这里我们只讨论中国,比如国内航班时间、电影上映时间等等,那么我们把MobileAPI返回的long型时间转换为GMT8时区的时间就万事大吉了——只需要额外加8个小时。无论使用的人身在哪个时区,他们看到的都应该是一个时间,也就是GMT8的时间。

由于App本地时间会不准,比如前后差十几分钟,又比如设置了GMT9的时区,这样在取本地时间的时候,就会差一个小时。遇到这种情况,就要依赖于HTTP Response头的Date属性了。

每调用一次MobileAPI,就取出HTTP Response头的Date值,转换为GMT时间后,再减去本地取出的时间,得到一个差值delta。这个值可能是因为手机时间不准而差出来的那十几分钟,也可能是因为时区不同导致的1个小时差值。我们将这个delta值保存下来。那么每当取本地当前时间的时候,再额外加上这个delta差值,就得到了服务器GMT8的时间,就做到了任何人看到的时间是一样的。

因为App会频繁调用MobileAPI,所以这个delta值也会频繁更新,不用担心长期不调用MobileAPI而导致的这个delta值不太准的问题。

接下来我们修改AndroidLib框架,以支持上述的这些功能。

1)首先,在HTTPRequest类提供一个用于更新本地时间和服务器时间差值的方法updateDeltaBetweenServerAndClientTime,如下所示,由于我们在这里补上了UTC和GMT8相差的那8个小时,所以App其他地方不再需要考虑时差的问题,如下所示:

void updateDeltaBetweenServerAndClientTime() {
  if (response != null) {
    final Header header = response.getLastHeader("Date");
    if (header != null) {
      final String strServerDate = header.getValue();
      try {
        if ((strServerDate != null) && !strServerDate.equals("")) {
          final SimpleDateFormat sdf = new SimpleDateFormat(
            "EEE, d MMM yyyy HH:mm:ss z", Locale.ENGLISH);
          TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
          Date serverDateUAT = sdf.parse(strServerDate);
          deltaBetweenServerAndClientTime = serverDateUAT
            .getTime()
            + 8 * 60 * 60 * 1000
            - System.currentTimeMillis();
        }
      } catch (java.text.ParseException e) {
        e.printStackTrace();
      }
    }
  }
} 

我们会在发起MobileAPI网络请求得到响应结果后,执行该方法,更新这个差值:

// 发送请求
response = httpClient.execute(request);
// 获取状态
final int statusCode = response.getStatusLine().getStatusCode();
// 设置回调函数, 但如果requestCallback, 说明不需要回调, 不需要知道返回结果
if ((requestCallback != null)) {
  if (statusCode == HttpStatus.SC_OK) {
    // 更新服务器时间和本地时间的差值
    updateDeltaBetweenServerAndClientTime(); 

因为我们的App会频繁的调用MobileAPI,所以为了避免频繁读写文件,我们没有将deltaBetweenServerAndClientTime存到本地文件,而是放在了内存中,当作一个全局变量来使用。

2)我们把这个deltaBetweenServerAndClientTime方法暴露出来,供外界调用:

public static Date getServerTime() {
  return new Date(System.currentTimeMillis()
                  + deltaBetweenServerAndClientTime);
} 

现在我们就可以模拟一个场景了。我把手机的时间改成任意一个值,然后再进入到WeatherByFastJsonActivity页面,因为页面加载的时候会调用MobileAPI获取天气的接口,所以本地会保存一个deltaBetweenServerAndClientTime差值。点击WeatherByFastJsonActivity页面上的“获取服务器时间”按钮,会因为我调用了AndroidLib中封装好的getServerTime方法,而弹出GMT8的当前时间:

btnShowTime.setOnClickListener(new View.OnClickListener() {
                               @Override
                               public void onClick(View v) {
  String strCurrentTime = Utils.getServerTime().toString();
  new AlertDialog.Builder(WeatherByFastJsonActivity.this)
    .setTitle("当前时间是:").setMessage(strCurrentTime)
    .setPositiveButton("确定", null).show();
}
}); 
2.5.3 开启gzip压缩

HTTP协议上的gzip编码是一种用来改进Web应用程序性能的技术。大流量的Web站点常常使用gzip压缩技术来减少传输量的大小,减少传输量大小有两个明显的好处,一是可以减少存储空间,二是通过网络传输时,可以减少传输的时间。

使用gzip的流程如下:

1)在App发起请求时,在HTTPRequest头中,添加要求支持gzip的key-value,这里的key是Accept-Encoding,value是gzip。如下所示,我们需要修改setHttpHeaders方法:

void setHttpHeaders(final HttpUriRequest httpMessage) {
  headers.clear();
  headers.put(FrameConstants.ACCEPT_CHARSET, "UTF-8,*");
  headers.put(FrameConstants.USER_AGENT, "Young Heart Android App ");
  headers.put(FrameConstants.ACCEPT_ENCODING, "gzip");
  if ((httpMessage != null) && (headers != null)) {
    for (final Entry<String, String> entry : headers.entrySet()) {
      if (entry.getKey() != null) {
        httpMessage.addHeader(entry.getKey(), entry.getValue());
      }
    }
  }
} 

2)MobileAPI的逻辑是,检查HTTP请求头中的Accept-Encoding是否有gzip值,如果有,就会执行gzip压缩。

如果执行了gzip压缩,那么在返回值也就是HTTPResponse的头中,有一个content-encoding字段,会带有gzip的值;否则,就没有这个值。

3)App检查HTTPResponse头中的content-encoding字段是否包含gzip值,这个值的有无,导致了App解析HTTPResponse的姿势不同,如下所示值,这个值的有无,导致了App解析HTTPResponse的姿势不同,如下所示(以下代码参见HTTPRequest这个类):

String strResponse = "";
if ((response.getEntity().getContentEncoding() != null)
    && (response.getEntity().getContentEncoding()
        .getValue() != null)) {
  if (response.getEntity().getContentEncoding()
      .getValue().contains("gzip")) {
    final InputStream in = response.getEntity()
      .getContent();
    final InputStream is = new GZIPInputStream(in);
                                               strResponse = HttpRequest.inputStreamToString(is);
    is.close();
  } else {
    response.getEntity().writeTo(content);
    strResponse = new String(content.toByteArray()).trim();
  }
} else {
  response.getEntity().writeTo(content);
  strResponse = new String(content.toByteArray()).trim();
} 
app研发录完整版 app研发录完整版pdf,android 开发人员必读的进阶书籍,上一个发错了发了不完整的,这个是完整的 立即下载

相关推荐

App研发录源代码

App研发录各个章节源代码

《App研发录:架构设计、Crash分析和竞品技术分析》— Android 书籍

文章目录第1章 重构,夜未眠 3第2章 Android网络底层框架设计 19第3章 Android经典场景设计 53第4章 Android命名规范和编码规范 83第5章 Crash异常收集与统计 93第6章 Crash异常分析 107第7章 ProGuard技术详解 171第8章 持续集成 181第9章 App竞品技术分析 205第10章 项目管理决定了开发速度 251第11章 日常工作中的问题解决 279第12章 无线团队的组建和管理 289 出版年: 2015-10-21 简介: 本书是作者多年App开

qq_27494201的博客 855

HomeAssistant+HACS实战:5分钟搞定米家设备接入(附HomeKit配置技巧)

本文详细介绍了如何通过HomeAssistant和HACS插件商店快速接入米家设备,并分享HomeKit配置技巧。从HACS安装到米家设备接入,再到HomeKit设备集成,提供一站式智能家居解决方案,帮助用户实现设备统一管理和自动化场景设置。

weixin_29192141的博客 1047

大数据面试题及答案-汇总版

大数据面试题及答案 汇总版                                     当前版本: Ver 1.0 制作单位:   编写人员: 审 核 人:   签 收 人:   签

风远陌 20万+

Ubuntu22.04下RTX2080显卡驱动、CUDA、cuDNN保姆级避坑安装指南(实测有效)

本文提供Ubuntu22.04下RTX2080显卡驱动、CUDA和cuDNN的详细安装指南,涵盖前期准备、驱动安装、CUDA版本选择、cuDNN配置及常见问题解决。通过实测有效的步骤和优化建议,帮助用户快速搭建稳定的深度学习环境,避免常见陷阱。

weixin_42540248的博客 284

告别第三方插件:官方Fused Library如何重塑Android库的合并与发布流程

本文详细介绍了Android官方推出的Fused Library如何替代第三方fat-aar插件,优化Android库的合并与发布流程。通过原生支持、智能依赖管理等优势,Fused Library解决了版本兼容性、依赖传递等核心痛点,提供了更稳定高效的开发体验。文章还提供了迁移实操指南和工程实践建议,帮助开发者顺利过渡到官方方案。

weixin_42561040的博客 252

App 研发录、架构设计、Crash分析和竞品技术分析------读书笔记(第二章)

网络底层框架设计1、不要自己定义网络请求框架,网络层不要使用AsyncTask 2、在网络返回数据Response的应该有一个规范的格式{ "isError":true, "errorType":1, "errorMessage":"网络异常" "result":"" }返回的真实数据应该放在result里面 3、使用原生的ThreadPoolExecutor+R

658

Python+Vue计算机毕业设计风电场监控系统的设计与实现7bd6i(程序+LW+源码+部署)

Python3.7.7+Django+Mysql5.7+pip list+HBuilderX(Vscode也行)+Vue+Pychram社区版。2. 前端:vue+css+javascript+jQuery+easyUI+highcharts。2.IDE环境:Python和Python 社区版都可以。Django + Vue +Python+Mysql 等等组成,B/S模式等等。该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。1. 后端:Python、Vue、Django、Mysql。

华信计算机毕设源码程序 340

读书笔记---APP研发录

一江夜雨 338

保姆级教程:手把手配置AutoSar诊断复位(Dcm 0x11服务联动BswM/EcuM)

本文提供AutoSar诊断复位(Dcm 0x11服务联动BswM/EcuM)的保姆级配置教程,详细讲解从诊断服务基础配置到BswM规则引擎搭建、服务端口映射及EcuM复位执行的完整流程。通过Vector配置工具的实际截图和代码片段,帮助开发者高效实现远程复位功能,并分享调试技巧与性能优化实践。

weixin_30756499的博客 543

小白 虚拟机 kali_Linux安装 详细教程

安装版本:VMmare workstation pro kali_Linux 安装环境:Window10 首先我们先了解一下什么是kali_linux: Kali Linux是基于Debian的Linux发行版, 设计用于数字取证操作系统。每一季度更新一次。由Offensive Security Ltd维护和资助。最先由Offensive Security的Mati Aharoni和Devon Kearns通过重写BackTrack来完成,BackTrack是他们之前写的用于取证的Linux发行

m0_50917178的博客 9万+

《App研发录》读书笔记

这本书基本上涵盖了移动开发中常见的关注点,之所以用关注点而不用技术点这个词是因为这本书并没有讲到具体的技术实现,但提供了行之有效的解决方案。读这本书的时候非常有感触,它很多的框架设计和解决方案与我实际开发中都是不谋而合的(有点自夸的意思哈)。所以也非常感谢作者能这么详细的记录下来。这篇读书笔记是记录我在阅读过程中感觉需要重点强调的地方和自己的一些理解,也供大家能快速的浏览本书的章节。 关于重构 对于

天盟 - 技术博客 - Eamon 1208

碳化硅外延层厚度测量:红外干涉光谱与FFT技术详解

薄膜厚度测量是半导体制造中的关键工艺控制环节,其核心原理基于光的干涉现象。当光在薄膜上下界面反射并叠加时,会产生与厚度相关的干涉条纹。通过分析这些条纹,可以非接触、无损地获取薄膜的精确厚度信息。这项技术的核心价值在于实现了对关键工艺参数的在线、快速监控,对于提升器件性能与生产良率至关重要。在第三代半导体,尤其是碳化硅功率器件的制造中,外延层厚度直接决定了器件的击穿电压和导通电阻等核心性能。红外干涉光谱法因其对碳化硅材料在红外波段的良好透过性,成为该领域主流的厚度测量方案。通过快速傅里叶变换(FFT)对采集到

weixin_29032489的博客 308

[读书笔记]《APP研发录》第一章

《APP研发录》读书笔记第一章1.1重新规划Android项目结构重新规划Android项目的目录结构,分两步走: 建立AndroidLab类库,将与业务无关的逻辑转移到AndroidLab,AndroidLab至少包括五大部分:包名+ acticity,cache,net,ui,utils 。activity包里面存放的是与业务无关的Activity基类。 net包里存放的是网络底层封装。 cac

陶程的博客 1334

【部署】Docker部署OpenClaw及常见问题解决(win11)

给AI单独开一个隔离的小容器,AI执行所有命令、下载文件都在这个小盒子里,就算AI被诱导执行危险命令,也不会影响你Win11本机的文件和系统,是必开的安全功能。Docker容器里的OAuth回调,会尝试在你Win11本机的1455端口捕获认证信息,但容器和Win11本机的网络隔离,导致浏览器访问不到这个回调地址。「挂载」就是把你Win11本机的文件夹,共享给容器里的「虚拟小电脑」,两边都能读写文件,比如你想让AI操作你D盘里的代码文件,就需要挂载。

海兰 4696

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

读书笔记App研发录--数据缓存设计

读书笔记App研发录–数据缓存设计标签(空格分隔): 数据缓存 android 优化数据缓存策略在2G和3G网络状态下,MobileApi访问速度过慢,会导致不好的用户体验。网络端的数据接受的限制主要是传输速度的限制。 策略 减少MobileApi的调用次数 App缓存时间。分时间段调用,一段时间使用缓存数据 App缓存只使用于数据即时性要求不高的数据。当然也可以根据具体的需求调节App缓存时间

PirateBrook的天梯 530

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

[读书笔记]《APP研发录》之App图片缓存设计

图片缓存在Android开发中十分重要,从网络获取图片、显示、回收任一环节有问题都会导致OOM。尤其是列表项,会加载大量网络上的图片。当我们快速滑动列表的时候会很卡,甚至会导致内存溢出而崩溃。为解决上述问题,ImageLoader出现了,ImageLoader的目的是为了实现异步的网络图片加载,缓存及显示,支持多线程异步加载。ImageLoader的原理在显示图片的时候,它会先在内存中查找,如果没有

陶程的博客 1868

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

App研发录第二章

读书笔记–App研发录Http和HttpUrlConnection的用法标签(空格分隔): Android HttpUrlConnection 异步网络请求过程HTTP是一种应用层协议,他通过TCP实现了可靠的数据传输,能够保证数据的完整性,准确性,TCP的优点使得HTTP的数据传输吞吐量和效率得到保证。 移动开发多为C/S架构,客户端向服务端发送特定请求,服务器返回结构,客户端解析结果,再进

PirateBrook的天梯 501

读书笔记----App研发录 第一章 重构

读书笔记—-App研发录 第一章 重构   对代码的重构,本质上遵循了六大设计原则。这是编码的根本准则,只有遵循这些准则,才会让代码避免恶化的脚步。拥有更好的扩展性,易读性和便于维护。这是一个良好项目的开始。

PirateBrook的天梯 632

APP研发录笔记

http://blog.youkuaiyun.com/codeemperor/article/details/50783928 http://blog.youkuaiyun.com/codeemperor/article/details/51066847

云淡风轻的博客 358

[读书笔记之App研发录]异常问题汇总(1)

开发中遇到的异常问题子线程不能修改UI只有原始创建这个视图层次的线程才能修改它的视图,而android的视图最初都是主线程创建的,所以才有这么一说:UI控件的更新放在主线程中,否则会产生不可预知的错误。对于TextView的内容更新,却发现在子线程中也能运行良好。这个现象一反上面的结论。如果在更新TextView内容之前让线程等待几秒时间,就会抛出子线程更新控件的异常信息。有一个方法requestL

sugaryaruan的博客 737

APP研发录读书笔记:网络底层框架设计

一.统一响应实体类Response  我们一般采用JSON作为API返回结果,数据格式一般为: { "errorCode":0, "errorMessage":""; "result":{} }一般有错误码,错误消息及返回数据,因此可以定义Response如下: public class Response { private int errorCode; private

hpb21的专栏 877

Flutter框架跨平台鸿蒙开发——读书笔记工具APP的开发流程

本文介绍了使用Flutter框架开发跨平台读书笔记工具APP的流程,重点展示了核心功能实现。通过设计Note和NoteCategory数据模型,实现了笔记的分类管理;采用文件存储方式完成本地数据持久化;提供了笔记增删改查等基础功能。该应用支持iOS、Android和鸿蒙系统,具有响应式布局和流畅交互体验,能满足学生、教师等用户的知识管理需求。开发过程中采用模块化设计,确保了代码的可维护性和扩展性。

zilikew的博客 758

读书笔记-App研发录-第7章-ProGuard技术详解

ProGuard简介,proguard-project.txt。

Dsa_ 455

App 研发录、架构设计、Crash分析和竞品技术分析------读书笔记(第三章)

Android经典场景设计1、App图片缓存设计 设计一个ImageLoaderr,ImageLoader的工作原理是 这样的在显示图片的时候,这会先从内存中查找;如果没有就去本地查找,如果还没有就开一个新的线程去下载这个图片,下载成功会把图片同时缓存到内存和本地。 基于这个原理,我们可以在每次退出一个页面的时候,把ImageLoader内存中的缓存全部清除,这样就节省了大量的内存,反正

1739

[附源码]JAVA计算机毕业设计电子书app(源码+开题)

本研究的意义在于,通过开发一款JAVA计算机毕业设计电子书app,不仅能够填补市场上专业电子书资源的空白,还能为学生提供一个更加便捷、高效的学习工具。我们将根据JAVA计算机专业的知识点和毕业设计需求,对电子书进行细致的分类,并通过简洁明了的界面展示图书的基本信息、作者介绍、目录结构等内容,方便用户快速找到所需的书籍。同时,我们将进行严格的测试和优化工作。通过以上研究内容的实施,我们将努力打造一款功能强大、操作便捷、体验优良的JAVA计算机毕业设计电子书app,为广大学生提供有力的学习支持和帮助。

光冯毕设程序 801
上一篇: [算法]微信红包
下一篇: [算法]小球的距离
陶程
博客等级 码龄12年 360粉丝 56原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值