OkHttp完全解析 是时候来了解OkHttp了

一、概述
*近在群里听到各种讨论okhttp的话题,可见okhttp的口碑相当好了。再加上Google貌似在6.0版本里面删除了HttpClient相关API,对于这个行为不做评价。为了更好的在应对网络访问,学习下okhttp还是蛮必要的,本篇博客首先介绍okhttp的简单使用,主要包含:

一般的get请求
一般的post请求
基于Http的文件上传
文件下载
加载图片
支持请求回调,直接返回对象、对象集合
支持session的保持
*后会对上述几个功能进行封装,完整的封装类的地址见:https://github.com/hongyangAndroid/okhttp-utils

使用前,对于Android Studio的用户,可以选择添加:

compile ‘com.squareup.okhttp:okhttp:2.4.0’

或者Eclipse的用户,可以下载*新的jar okhttp he latest JAR ,添加依赖就可以用了。

注意:okhttp内部依赖okio,别忘了同时导入okio:

gradle: compile ‘com.squareup.okio:okio:1.5.0’

*新的jar地址:okio the latest JAR

二、使用教程
(一)Http Get
对了网络加载库,那么*常见的肯定就是http get请求了,比如获取一个网页的内容。

//创建okHttpClient对象
OkHttpClient mOkHttpClient = new OkHttpClient();
//创建一个Request
final Request request = new Request.Builder()
.url(“https://github.com/hongyangAndroid”)
.build();
//new call
Call call = mOkHttpClient.newCall(request);
//请求加入调度
call.enqueue(new Callback()
{
@Override
public void onFailure(Request request, IOException e)
{
}

@Override
public void onResponse(final Response response) throws IOException
{
//String htmlStr = response.body().string();
}
});

以上就是发送一个get请求的步骤,首先构造一个Request对象,参数*起码有个url,当然你可以通过Request.Builder设置更多的参数比如:header、method等。

然后通过request的对象去构造得到一个Call对象,类似于将你的请求封装成了任务,既然是任务,就会有execute()和cancel()等方法。

*后,我们希望以异步的方式去执行请求,所以我们调用的是call.enqueue,将call加入调度队列,然后等待任务执行完成,我们在Callback中即可得到结果。

看到这,你会发现,整体的写法还是比较长的,所以封装肯定是要做的,不然每个请求这么写,得累死。

ok,需要注意几点:

onResponse回调的参数是response,一般情况下,比如我们希望获得返回的字符串,可以通过response.body().string()获取;如果希望获得返回的二进制字节数组,则调用response.body().bytes();如果你想拿到返回的inputStream,则调用response.body().byteStream()

看到这,你可能会奇怪,竟然还能拿到返回的inputStream,看到这个*起码能意识到一点,这里支持大文件下载,有inputStream我们就可以通过IO的方式写文件。不过也说明一个问题,这个onResponse执行的线程并不是UI线程。的确是的,如果你希望操作控件,还是需要使用handler等,例如:

@Override
public void onResponse(final Response response) throws IOException
{
final String res = response.body().string();
runOnUiThread(new Runnable()
{
@Override
public void run()
{
mTv.setText(res);
}

});
}

我们这里是异步的方式去执行,当然也支持阻塞的方式,上面我们也说了Call有一个execute()方法,你也可以直接调用call.execute()通过返回一个Response。
(二) Http Post 携带参数
看来上面的简单的get请求,基本上整个的用法也就掌握了,比如post携带参数,也仅仅是Request的构造的不同。

Request request = buildMultipartFormRequest(
url, new File[]{file}, new String[]{fileKey}, null);
FormEncodingBuilder builder = new FormEncodingBuilder();
builder.add(“username”,”张鸿洋”);

Request request = new Request.Builder()
.url(url)
.post(builder.build())
.build();
mOkHttpClient.newCall(request).enqueue(new Callback(){});

大家都清楚,post的时候,参数是包含在请求体中的;所以我们通过FormEncodingBuilder。添加多个String键值对,然后去构造RequestBody,*后完成我们Request的构造。

后面的就和上面一样了。

(三)基于Http的文件上传
接下来我们在介绍一个可以构造RequestBody的Builder,叫做MultipartBuilder。当我们需要做类似于表单上传的时候,就可以使用它来构造我们的requestBody。

File file = new File(Environment.getExternalStorageDirectory(), “balabala.mp4”);

RequestBody fileBody = RequestBody.create(MediaType.parse(“application/octet-stream”), file);

RequestBody requestBody = new MultipartBuilder()
.type(MultipartBuilder.FORM)
.addPart(Headers.of(
“Content-Disposition”,
“form-data; name=\”username\””),
RequestBody.create(null, “张鸿洋”))
.addPart(Headers.of(
“Content-Disposition”,
“form-data; name=\”mFile\”;
filename=\”wjd.mp4\””), fileBody)
.build();

Request request = new Request.Builder()
.url(“http://192.168.1.103:8080/okHttpServer/fileUpload”)
.post(requestBody)
.build();

Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback()
{
//…
});

上述代码向服务器传递了一个键值对username:张鸿洋和一个文件。我们通过MultipartBuilder的addPart方法可以添加键值对或者文件。

其实类似于我们拼接模拟浏览器行为的方式,如果你对这块不了解,可以参考:从原理角度解析Android (Java) http 文件上传

ok,对于我们*开始的目录还剩下图片下载,文件下载;这两个一个是通过回调的Response拿到byte[]然后decode成图片;文件下载,就是拿到inputStream做写文件操作,我们这里就不赘述了。

关于用法,也可以参考泡网OkHttp使用教程

接下来我们主要看如何封装上述的代码。

三、封装
由于按照上述的代码,写多个请求肯定包含大量的重复代码,所以我希望封装后的代码调用是这样的:

(一)使用
一般的get请求

OkHttpClientManager.getAsyn(“https://www.baidu.com”, new OkHttpClientManager.ResultCallback<String>()
{
@Override
public void onError(Request request, Exception e)
{
e.printStackTrace();
}

@Override
public void onResponse(String u)
{
mTv.setText(u);//注意这里是UI线程
}
});

对于一般的请求,我们希望给个url,然后CallBack里面直接操作控件。

文件上传且携带参数

我们希望提供一个方法,传入url,params,file,callback即可。

OkHttpClientManager.postAsyn(“http://192.168.1.103:8080/okHttpServer/fileUpload”,//
new OkHttpClientManager.ResultCallback<String>()
{
@Override
public void onError(Request request, IOException e)
{
e.printStackTrace();
}

@Override
public void onResponse(String result)
{

}
},//
file,//
“mFile”,//
new OkHttpClientManager.Param[]{
new OkHttpClientManager.Param(“username”, “zhy”),
new OkHttpClientManager.Param(“password”, “123”)}
);

键值对没什么说的,参数3为file,参数4为file对应的name,这个name不是文件的名字;
对应于http中的

<input type=”file” name=”mFile” >

对应的是name后面的值,即mFile.

文件下载

对于文件下载,提供url,目标dir,callback即可。

OkHttpClientManager.downloadAsyn(
“http://192.168.1.103:8080/okHttpServer/files/messenger_01.png”,
Environment.getExternalStorageDirectory().getAbsolutePath(),
new OkHttpClientManager.ResultCallback<String>()
{
@Override
public void onError(Request request, IOException e)
{

}

@Override
public void onResponse(String response)
{
//文件下载成功,这里回调的reponse为文件的absolutePath
}
});

展示图片

展示图片,我们希望提供一个url和一个imageview,如果下载成功,直接帮我们设置上即可。

OkHttpClientManager.displayImage(mImageView,
“http://images.csdn.net/20150817/1.jpg”);

内部会自动根据imageview的大小自动对图片进行合适的压缩。虽然,这里可能不适合一次性加载大量图片的场景,但是对于app中偶尔有几个图片的加载,还是可用的。

四、整合Gson
很多人提出项目中使用时,服务端返回的是Json字符串,希望客户端回调可以直接拿到对象,于是整合进入了Gson,完善该功能。

(一)直接回调对象
例如现在有个User实体类:

package com.zhy.utils.http.okhttp;

public class User {

public String username ;
public String password ;

public User() {
// TODO Auto-generated constructor stub
}

public User(String username, String password) {
this.username = username;
this.password = password;
}

@Override
public String toString()
{
return “User{” +
“username='” + username + ‘\” +
“, password='” + password + ‘\” +
‘}’;
}
}

服务端返回:

{“username”:”zhy”,”password”:”123″}
1
客户端可以如下方式调用:

OkHttpClientManager.getAsyn(“http://192.168.56.1:8080/okHttpServer/user!getUser”,
new OkHttpClientManager.ResultCallback<User>()
{
@Override
public void onError(Request request, Exception e)
{
e.printStackTrace();
}

@Override
public void onResponse(User user)
{
mTv.setText(u.toString());//UI线程
}
});

我们传入泛型User,在onResponse里面直接回调User对象。
这里特别要注意的事,如果在json字符串->实体对象过程中发生错误,程序不会崩溃,onError方法会被回调。

注意:这里做了少许的更新,接口命名从StringCallback修改为ResultCallback。接口中的onFailure方法修改为onError。

(二) 回调对象集合
依然是上述的User类,服务端返回

[{“username”:”zhy”,”password”:”123″},{“username”:”lmj”,”password”:”12345″}]

则客户端可以如下调用:

OkHttpClientManager.getAsyn(“http://192.168.56.1:8080/okHttpServer/user!getUsers”,
new OkHttpClientManager.ResultCallback<List<User>>()
{
@Override
public void onError(Request request, Exception e)
{
e.printStackTrace();
}
@Override
public void onResponse(List<User> us)
{
Log.e(“TAG”, us.size() + “”);
mTv.setText(us.get(1).toString());
}
});

唯一的区别,就是泛型变为List<User> ,ok , 如果发现bug或者有任何意见欢迎留言。

源码
ok,基本介绍完了,对于封装的代码其实也很简单,我就直接贴出来了,因为也没什么好介绍的,如果你看完上面的用法,肯定可以看懂:

package com.zhy.utils.http.okhttp;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.widget.ImageView;

import com.google.gson.Gson;
import com.google.gson.internal.$Gson$Types;
import com.squareup.okhttp.Call;
import com.squareup.okhttp.Callback;
import com.squareup.okhttp.FormEncodingBuilder;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.MultipartBuilder;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.FileNameMap;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
* Created by zhy on 15/8/17.
*/
public class OkHttpClientManager
{
private static OkHttpClientManager mInstance;
private OkHttpClient mOkHttpClient;
private Handler mDelivery;
private Gson mGson;

private static final String TAG = “OkHttpClientManager”;

private OkHttpClientManager()
{
mOkHttpClient = new OkHttpClient();
//cookie enabled
mOkHttpClient.setCookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER));
mDelivery = new Handler(Looper.getMainLooper());
mGson = new Gson();
}

public static OkHttpClientManager getInstance()
{
if (mInstance == null)
{
synchronized (OkHttpClientManager.class)
{
if (mInstance == null)
{
mInstance = new OkHttpClientManager();
}
}
}
return mInstance;
}

/**
* 同步的Get请求
*
* @param url
* @return Response
*/
private Response _getAsyn(String url) throws IOException
{
final Request request = new Request.Builder()
.url(url)
.build();
Call call = mOkHttpClient.newCall(request);
Response execute = call.execute();
return execute;
}

/**
* 同步的Get请求
*
* @param url
* @return 字符串
*/
private String _getAsString(String url) throws IOException
{
Response execute = _getAsyn(url);
return execute.body().string();
}

/**
* 异步的get请求
*
* @param url
* @param callback
*/
private void _getAsyn(String url, final ResultCallback callback)
{
final Request request = new Request.Builder()
.url(url)
.build();
deliveryResult(callback, request);
}

/**
* 同步的Post请求
*
* @param url
* @param params post的参数
* @return
*/
private Response _post(String url, Param… params) throws IOException
{
Request request = buildPostRequest(url, params);
Response response = mOkHttpClient.newCall(request).execute();
return response;
}

/**
* 同步的Post请求
*
* @param url
* @param params post的参数
* @return 字符串
*/
private String _postAsString(String url, Param… params) throws IOException
{
Response response = _post(url, params);
return response.body().string();
}

/**
* 异步的post请求
*
* @param url
* @param callback
* @param params
*/
private void _postAsyn(String url, final ResultCallback callback, Param… params)
{
Request request = buildPostRequest(url, params);
deliveryResult(callback, request);
}

/**
* 异步的post请求
*
* @param url
* @param callback
* @param params
*/
private void _postAsyn(String url, final ResultCallback callback, Map<String, String> params)
{
Param[] paramsArr = map2Params(params);
Request request = buildPostRequest(url, paramsArr);
deliveryResult(callback, request);
}

/**
* 同步基于post的文件上传
*
* @param params
* @return
*/
private Response _post(String url, File[] files, String[] fileKeys, Param… params) throws IOException
{
Request request = buildMultipartFormRequest(url, files, fileKeys, params);
return mOkHttpClient.newCall(request).execute();
}

private Response _post(String url, File file, String fileKey) throws IOException
{
Request request = buildMultipartFormRequest(url, new File[]{file}, new String[]{fileKey}, null);
return mOkHttpClient.newCall(request).execute();
}

private Response _post(String url, File file, String fileKey, Param… params) throws IOException
{
Request request = buildMultipartFormRequest(url, new File[]{file}, new String[]{fileKey}, params);
return mOkHttpClient.newCall(request).execute();
}

/**
* 异步基于post的文件上传
*
* @param url
* @param callback
* @param files
* @param fileKeys
* @throws IOException
*/
private void _postAsyn(String url, ResultCallback callback, File[] files, String[] fileKeys, Param… params) throws IOException
{
Request request = buildMultipartFormRequest(url, files, fileKeys, params);
deliveryResult(callback, request);
}

/**
* 异步基于post的文件上传,单文件不带参数上传
*
* @param url
* @param callback
* @param file
* @param fileKey
* @throws IOException
*/
private void _postAsyn(String url, ResultCallback callback, File file, String fileKey) throws IOException
{
Request request = buildMultipartFormRequest(url, new File[]{file}, new String[]{fileKey}, null);
deliveryResult(callback, request);
}

/**
* 异步基于post的文件上传,单文件且携带其他form参数上传
*
* @param url
* @param callback
* @param file
* @param fileKey
* @param params
* @throws IOException
*/
private void _postAsyn(String url, ResultCallback callback, File file, String fileKey, Param… params) throws IOException
{
Request request = buildMultipartFormRequest(url, new File[]{file}, new String[]{fileKey}, params);
deliveryResult(callback, request);
}

/**
* 异步下载文件
*
* @param url
* @param destFileDir 本地文件存储的文件夹
* @param callback
*/
private void _downloadAsyn(final String url, final String destFileDir, final ResultCallback callback)
{
final Request request = new Request.Builder()
.url(url)
.build();
final Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback()
{
@Override
public void onFailure(final Request request, final IOException e)
{
sendFailedStringCallback(request, e, callback);
}

@Override
public void onResponse(Response response)
{
InputStream is = null;
byte[] buf = new byte[2048];
int len = 0;
FileOutputStream fos = null;
try
{
is = response.body().byteStream();
File file = new File(destFileDir, getFileName(url));
fos = new FileOutputStream(file);
while ((len = is.read(buf)) != -1)
{
fos.write(buf, 0, len);
}
fos.flush();
//如果下载文件成功,*个参数为文件的*对路径
sendSuccessResultCallback(file.getAbsolutePath(), callback);
} catch (IOException e)
{
sendFailedStringCallback(response.request(), e, callback);
} finally
{
try
{
if (is != null) is.close();
} catch (IOException e)
{
}
try
{
if (fos != null) fos.close();
} catch (IOException e)
{
}
}

}
});
}

private String getFileName(String path)
{
int separatorIndex = path.lastIndexOf(“/”);
return (separatorIndex < 0) ? path : path.substring(separatorIndex + 1, path.length());
}

/**
* 加载图片
*
* @param view
* @param url
* @throws IOException
*/
private void _displayImage(final ImageView view, final String url, final int errorResId)
{
final Request request = new Request.Builder()
.url(url)
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback()
{
@Override
public void onFailure(Request request, IOException e)
{
setErrorResId(view, errorResId);
}

@Override
public void onResponse(Response response)
{
InputStream is = null;
try
{
is = response.body().byteStream();
ImageUtils.ImageSize actualImageSize = ImageUtils.getImageSize(is);
ImageUtils.ImageSize imageViewSize = ImageUtils.getImageViewSize(view);
int inSampleSize = ImageUtils.calculateInSampleSize(actualImageSize, imageViewSize);
try
{
is.reset();
} catch (IOException e)
{
response = _getAsyn(url);
is = response.body().byteStream();
}

BitmapFactory.Options ops = new BitmapFactory.Options();
ops.inJustDecodeBounds = false;
ops.inSampleSize = inSampleSize;
final Bitmap bm = BitmapFactory.decodeStream(is, null, ops);
mDelivery.post(new Runnable()
{
@Override
public void run()
{
view.setImageBitmap(bm);
}
});
} catch (Exception e)
{
setErrorResId(view, errorResId);

} finally
{
if (is != null) try
{
is.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
});

}

private void setErrorResId(final ImageView view, final int errorResId)
{
mDelivery.post(new Runnable()
{
@Override
public void run()
{
view.setImageResource(errorResId);
}
});
}

//*************对外公布的方法************

public static Response getAsyn(String url) throws IOException
{
return getInstance()._getAsyn(url);
}

public static String getAsString(String url) throws IOException
{
return getInstance()._getAsString(url);
}

public static void getAsyn(String url, ResultCallback callback)
{
getInstance()._getAsyn(url, callback);
}

public static Response post(String url, Param… params) throws IOException
{
return getInstance()._post(url, params);
}

public static String postAsString(String url, Param… params) throws IOException
{
return getInstance()._postAsString(url, params);
}

public static void postAsyn(String url, final ResultCallback callback, Param… params)
{
getInstance()._postAsyn(url, callback, params);
}

public static void postAsyn(String url, final ResultCallback callback, Map<String, String> params)
{
getInstance()._postAsyn(url, callback, params);
}

public static Response post(String url, File[] files, String[] fileKeys, Param… params) throws IOException
{
return getInstance()._post(url, files, fileKeys, params);
}

public static Response post(String url, File file, String fileKey) throws IOException
{
return getInstance()._post(url, file, fileKey);
}

public static Response post(String url, File file, String fileKey, Param… params) throws IOException
{
return getInstance()._post(url, file, fileKey, params);
}

public static void postAsyn(String url, ResultCallback callback, File[] files, String[] fileKeys, Param… params) throws IOException
{
getInstance()._postAsyn(url, callback, files, fileKeys, params);
}

public static void postAsyn(String url, ResultCallback callback, File file, String fileKey) throws IOException
{
getInstance()._postAsyn(url, callback, file, fileKey);
}

public static void postAsyn(String url, ResultCallback callback, File file, String fileKey, Param… params) throws IOException
{
getInstance()._postAsyn(url, callback, file, fileKey, params);
}

public static void displayImage(final ImageView view, String url, int errorResId) throws IOException
{
getInstance()._displayImage(view, url, errorResId);
}

public static void displayImage(final ImageView view, String url)
{
getInstance()._displayImage(view, url, -1);
}

public static void downloadAsyn(String url, String destDir, ResultCallback callback)
{
getInstance()._downloadAsyn(url, destDir, callback);
}

//****************************

private Request buildMultipartFormRequest(String url, File[] files,
String[] fileKeys, Param[] params)
{
params = validateParam(params);

MultipartBuilder builder = new MultipartBuilder()
.type(MultipartBuilder.FORM);

for (Param param : params)
{
builder.addPart(Headers.of(“Content-Disposition”, “form-data; name=\”” + param.key + “\””),
RequestBody.create(null, param.value));
}
if (files != null)
{
RequestBody fileBody = null;
for (int i = 0; i < files.length; i++)
{
File file = files[i];
String fileName = file.getName();
fileBody = RequestBody.create(MediaType.parse(guessMimeType(fileName)), file);
//TODO 根据文件名设置contentType
builder.addPart(Headers.of(“Content-Disposition”,
“form-data; name=\”” + fileKeys[i] + “\”; filename=\”” + fileName + “\””),
fileBody);
}
}

RequestBody requestBody = builder.build();
return new Request.Builder()
.url(url)
.post(requestBody)
.build();
}

private String guessMimeType(String path)
{
FileNameMap fileNameMap = URLConnection.getFileNameMap();
String contentTypeFor = fileNameMap.getContentTypeFor(path);
if (contentTypeFor == null)
{
contentTypeFor = “application/octet-stream”;
}
return contentTypeFor;
}

private Param[] validateParam(Param[] params)
{
if (params == null)
return new Param[0];
else return params;
}

private Param[] map2Params(Map<String, String> params)
{
if (params == null) return new Param[0];
int size = params.size();
Param[] res = new Param[size];
Set<Map.Entry<String, String>> entries = params.entrySet();
int i = 0;
for (Map.Entry<String, String> entry : entries)
{
res[i++] = new Param(entry.getKey(), entry.getValue());
}
return res;
}

private static final String SESSION_KEY = “Set-Cookie”;
private static final String mSessionKey = “JSESSIONID”;

private Map<String, String> mSessions = new HashMap<String, String>();

private void deliveryResult(final ResultCallback callback, Request request)
{
mOkHttpClient.newCall(request).enqueue(new Callback()
{
@Override
public void onFailure(final Request request, final IOException e)
{
sendFailedStringCallback(request, e, callback);
}

@Override
public void onResponse(final Response response)
{
try
{
final String string = response.body().string();
if (callback.mType == String.class)
{
sendSuccessResultCallback(string, callback);
} else
{
Object o = mGson.fromJson(string, callback.mType);
sendSuccessResultCallback(o, callback);
}

} catch (IOException e)
{
sendFailedStringCallback(response.request(), e, callback);
} catch (com.google.gson.JsonParseException e)//Json解析的错误
{
sendFailedStringCallback(response.request(), e, callback);
}

}
});
}

private void sendFailedStringCallback(final Request request, final Exception e, final ResultCallback callback)
{
mDelivery.post(new Runnable()
{
@Override
public void run()
{
if (callback != null)
callback.onError(request, e);
}
});
}

private void sendSuccessResultCallback(final Object object, final ResultCallback callback)
{
mDelivery.post(new Runnable()
{
@Override
public void run()
{
if (callback != null)
{
callback.onResponse(object);
}
}
});
}

private Request buildPostRequest(String url, Param[] params)
{
if (params == null)
{
params = new Param[0];
}
FormEncodingBuilder builder = new FormEncodingBuilder();
for (Param param : params)
{
builder.add(param.key, param.value);
}
RequestBody requestBody = builder.build();
return new Request.Builder()
.url(url)
.post(requestBody)
.build();
}

public static abstract class ResultCallback<T>
{
Type mType;

public ResultCallback()
{
mType = getSuperclassTypeParameter(getClass());
}

static Type getSuperclassTypeParameter(Class<?> subclass)
{
Type superclass = subclass.getGenericSuperclass();
if (superclass instanceof Class)
{
throw new RuntimeException(“Missing type parameter.”);
}
ParameterizedType parameterized = (ParameterizedType) superclass;
return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
}

public abstract void onError(Request request, Exception e);

public abstract void onResponse(T response);
}

public static class Param
{
public Param()
{
}

public Param(String key, String value)
{
this.key = key;
this.value = value;
}

String key;
String value;
}

}

 

Android11 无Root 访问data目录实现

关于Android11权限变化

谷歌在Android11及以上系统中采用了文件沙盒存储模式,导致第三方应用无法像以前一样访问Android/data目录,这是好事。但是我所不能理解的是已经获得”所有文件管理”权限的APP为何还是限制了,岂不是完全不留给清理、文件管理类软件后路?实在不应该!

作为普通安卓用户该如何方便快速地访问Android/data目录

众所周知,不能访问Android/data目录非常不方便,比如要管理QQ、微信接收到的文件、其他App下载的数据(如迅雷等等)。

现本人开发的应用已实现无Root访问Android/data目录(其中文件浏览器功能),并且可以方便地进行管理。

https://www.coolapk.com/apk/com.magicalstory.cleaner

软件下载

欢迎安卓手机用户下载使用 和 Android开发者下载预览功能的实现。

App界面预览
在这里插入图片描述

开发者该如何实现无ROOT访问Data目录

1.首先,可根据需要获取所有文件管理权限:
在清单中声明:

  1. <uses-permission android:name=“android.permission.WRITE_EXTERNAL_STORAGE” />
  2. <uses-permission android:name=“android.permission.READ_EXTERNAL_STORAGE” />
  3. <uses-permission
  4. android:name=“android.permission.MANAGE_EXTERNAL_STORAGE”
  5. tools:ignore=“ScopedStorage” />

2.动态获取读写权限,这个不用多说了吧,如果觉得麻烦可以使用郭霖大神的permissionX库
Github

关于”管理所有文件”权限
这个权限可以让你的App跟Android11以前一样,通过File API访问所有文件(除Android/data目录)

如有需要,请在清单声明不启用沙盒存储

  1. android:preserveLegacyExternalStorage=“true”
  2. android:requestLegacyExternalStorage=“true”

相关判断

  1. //判断是否需要所有文件权限
  2. if (!(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager())) {
  3. //表明已经有这个权限了
  4. }

获取权限

  1. Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
  2. startActivity(intent);

正式开始解决Android/data问题

首先,使用的方式是SAF框架(Android Storage Access Framework)
这个框架在Android4.4就引入了,如果没有了解过的话,可以百度。

获取某个文件目录的权限

方法很简单,使用android.intent.action.OPEN_DOCUMENT_TREE(调用SAF框架的文件选择器选择一个文件夹)的Intent就可以授权了
等下会放出工具类,现在看下例子:

  1. //获取指定目录的访问权限
  2. public static void startFor(String path, Activity context, int REQUEST_CODE_FOR_DIR) {
  3. statusHolder.path = path;//这里主要是我的一个状态保存类,说明现在获取权限的路径是他,大家不用管。
  4. String uri = changeToUri(path);//调用方法,把path转换成可解析的uri文本,这个方法在下面会公布
  5. Uri parse = Uri.parse(uri);
  6. Intent intent = new Intent(“android.intent.action.OPEN_DOCUMENT_TREE”);
  7. intent.addFlags(
  8. Intent.FLAG_GRANT_READ_URI_PERMISSION
  9. | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
  10. | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
  11. | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
  12. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  13. intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, parse);
  14. }
  15. context.startActivityForResult(intent, REQUEST_CODE_FOR_DIR);//开始授权
  16. }

调用后的示意图:
在这里插入图片描述

回调并永久保存某个目录的权限

  1. //返回授权状态
  2. @Override
  3. protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
  4. super.onActivityResult(requestCode, resultCode, data);
  5. Uri uri;
  6. if (data == null) {
  7. return;
  8. }
  9. if (requestCode == REQUEST_CODE_FOR_DIR && (uri = data.getData()) != null) {
  10. getContentResolver().takePersistableUriPermission(uri, data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION
  11. | Intent.FLAG_GRANT_WRITE_URI_PERMISSION));//关键是这里,这个就是保存这个目录的访问权限
  12. PreferencesUtil.saveString(MainActivity.this, statusHolder.path + “授权”, “true”);//我自己处理的逻辑,大家不用管
  13. }
  14. }

权限授权并永久保存成功
在这里插入图片描述

通过DocumentFile Api访问目录

使用起来非常简单
先看看怎么生成DocumentFile对象

  1. DocumentFile documentFile = DocumentFile.fromTreeUri(context, Uri.parse(fileUriUtils.changeToUri3(path)));
  2. //changeToUri3方法是我封装好的方法,后面会用到,这个是通过path生成指定可解析URI的方法

真所谓有手就行,调用DocumentFile.fromTreeUri()方法就可以了,这个方法说的是从一个文件夹URI生成DocumentFile对象(treeUri就是文件夹URI)

当然还有其他方法:
DocumentFile.fromSingleUri();
DocumentFile.fromFile();
DocumentFile.isDocumentUri();

看名字就明白了,但是我们有的的是一个文件夹uri,当然使用这个方法来生成DocumentFile对象,不同方法生成的DocumentFile对象有不同效果,如果你用fromTreeUri生成的默认是文件夹对象,有ListFiles() 方法
DocumentFile.ListFiles()也就是列出文件夹里面的全部子文件,类似于File.listFiles()方法

然后就这样啊,得到了DocumentFile对象就可以进行骚操作了啊,比如列出子文件啊,删除文件啊,移动啊,删除啊什么的都可以,没错,Android/data目录就是这样进行操作和访问的!

实现遍历或管理Android/data文件目录

比较基础,我就不多说啦,简单讲讲实现方案和踩过的坑

1.遍历,跟普通全遍历没啥差别,但是不能通过直接传入Path进行遍历

  1. //遍历示例,不进行额外逻辑处理
  2. void getFiles(DocumentFile documentFile) {
  3. Log.d(“文件:”, documentFile.getName());
  4. if (documentFile.isDirectory()) {
  5. for (DocumentFile file : documentFile.listFiles()) {
  6. Log.d(“子文件”, file.getName());
  7. if (file.isDirectory()) {
  8. getFiles(file);//递归调用
  9. }
  10. }
  11. }
  12. }

2.实现文件管理器方案(管理Android/data目录就是这个方案)
以下仅介绍方法

  1. class file{
  2. String title;
  3. DocumentFile documentFile;
  4. public String getTitle() {
  5. return title;
  6. }
  7. public void setTitle(String title) {
  8. this.title = title;
  9. }
  10. public DocumentFile getDocumentFile() {
  11. return documentFile;
  12. }
  13. public void setDocumentFile(DocumentFile documentFile) {
  14. this.documentFile = documentFile;
  15. }
  16. }
  17. MainActivity{
  18. //加载数据
  19. void getFiles(DocumentFile documentFile) {
  20. ArrayList<file> arrayList = new ArrayList<>();
  21. if (documentFile.isDirectory()) {
  22. for (DocumentFile documentFile_inner : documentFile.listFiles()) {
  23. file file = new file();
  24. file.setTitle(documentFile_inner.getName());
  25. file.setDocumentFile(documentFile_inner);
  26. }
  27. }
  28. }
  29. }
  30. }

当列表被点击了,处理方案:

  1. public void onclick(int postion){
  2. file file = arrayList.get(postion);
  3. getFiles(file.getDocumentFile());//获取该文件夹的document对象,再把该文件夹遍历出来
  4. //然后再次显示就完事了
  5. }

以上就是模拟实现文件管理器->文件浏览功能,大家应该一目了然,只介绍方案。

我实现的文件管理(Android11上直接免root管理data目录)
在这里插入图片描述

重要的坑:为什么不直接使用路径Path来实现文件浏览呢?

对呀,很明显使用传统的通过文件的path来实现文件管理岂不是更加方便?
我也这样觉得的,在我当时在对Android11进行适配的时候为了改动小,肯定是想用这个方法来进行适配,但是根本行不通!

我们不是获取了Android/data目录的权限了吗? 明明说好的获取该目录的权限后拥有该文件夹及所有子文件的读写权限的!
我为什么不能直接通过调用changToUri把path转换成uri,再生成DocumentFile对象呢?
这样岂不是更加方便嘛? 而且SAF的文件效率比File低多了。
但是试了好几次后,我确定这样是不行的!

就算你生成的是Android/data目录下子文件的正确URI,再生成DocumentFile对象,还是不行,因为你生成的DocumentFile对象始终指向Android/data(也就是你授权过的那个目录), 无解!

刚刚开始我还以为是我生成的URI不正确,但是当我尝试再次把我想获取的子目录路径进行文件目录授权后,再用同一个URI生成DocumentFile对象却能指向正正确目录了。

看到这里大家应该懂了吧,是谷歌对没有授权的子文件夹目录进行了限制,不让你直接通过TreeUri生成正确的Docment对象,至少在Android/data目录是这样的。

现在是不是觉得谷歌官方解释: 获取该目录的权限后拥有该文件夹及所有子文件的读写权限的!
是放屁?确实是!

解决方案

既然我们不能直接生成不了已授权目录的子目录DocumentFile对象,那我能不能试试直接对应子路径生成DocumentFile对象(非treeUri),我们试试用fromSingleUri()方法:

  1. //根据路径获得document文件
  2. public static DocumentFile getDoucmentFile(Context context, String path) {
  3. if (path.endsWith(“/”)) {
  4. path = path.substring(0, path.length() – 1);
  5. }
  6. String path2 = path.replace(“/storage/emulated/0/”, “”).replace(“/”, “%2F”);
  7. return DocumentFile.fromSingleUri(context, Uri.parse(“content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A” + path2));
  8. }

很显然,可以了!可以生成正确的DocumentFile对象了,我们又可以用它来做一些好玩的东西了,比如直接通过path生成DocumentFile对象对某个文件获取大小啊、判断存在状态啊,等等。
这个Android11上Android/data受限后,我觉得这个是很好的解决方案了,毕竟可以实现无Root访问并实现管理。

SAF方案缺点

很显然,通过SAF文件存储框架访问文件,速度和效率远远低于File API,因为SAF本来用途就不是用来解决Android11/data目录文件访问的。

但是对于一些涉及文件管理类的App来说目前这个算是*全或较优的解决方案了。

放大招,ROOT权限直接解锁后带权访问Data目录

通过ROOT权限执行
“chmod -R 777 /storage/emulated/0/Android/data”
命令就可以解锁Android/data目录,注意:不可逆。

至于怎么通过ROOT权限访问目录,就需要参考MT文件管理器或张海大神开源的文件管理器了

Github
Github:https://github.com/zhanghai/MaterialFiles

参考https://blog.csdn.net/qq_17827627/article/details/113931692

封装好的工具类

真的超级简单呀,认真看一遍就可以上手了,都是日常操作,希望对大家有帮助。

  1. public class fileUriUtils {
  2. public static String root = Environment.getExternalStorageDirectory().getPath() + “/”;
  3. public static String treeToPath(String path) {
  4. String path2;
  5. if (path.contains(“content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary”)) {
  6. path2 = path.replace(“content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A”, root);
  7. path2 = path2.replace(“%2F”, “/”);
  8. } else {
  9. path2 = root + textUtils.getSubString(path + “测试”, “document/primary%3A”, “测试”).replace(“%2F”, “/”);
  10. }
  11. return path2;
  12. }
  13. //判断是否已经获取了Data权限,改改逻辑就能判断其他目录,懂得都懂
  14. public static boolean isGrant(Context context) {
  15. for (UriPermission persistedUriPermission : context.getContentResolver().getPersistedUriPermissions()) {
  16. if (persistedUriPermission.isReadPermission() && persistedUriPermission.getUri().toString().equals(“content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata”)) {
  17. return true;
  18. }
  19. }
  20. return false;
  21. }
  22. //直接返回DocumentFile
  23. public static DocumentFile getDocumentFilePath(Context context, String path, String sdCardUri) {
  24. DocumentFile document = DocumentFile.fromTreeUri(context, Uri.parse(sdCardUri));
  25. String[] parts = path.split(“/”);
  26. for (int i = 3; i < parts.length; i++) {
  27. document = document.findFile(parts[i]);
  28. }
  29. return document;
  30. }
  31. //转换至uriTree的路径
  32. public static String changeToUri(String path) {
  33. if (path.endsWith(“/”)) {
  34. path = path.substring(0, path.length() – 1);
  35. }
  36. String path2 = path.replace(“/storage/emulated/0/”, “”).replace(“/”, “%2F”);
  37. return “content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A” + path2;
  38. }
  39. //转换至uriTree的路径
  40. public static DocumentFile getDoucmentFile(Context context, String path) {
  41. if (path.endsWith(“/”)) {
  42. path = path.substring(0, path.length() – 1);
  43. }
  44. String path2 = path.replace(“/storage/emulated/0/”, “”).replace(“/”, “%2F”);
  45. return DocumentFile.fromSingleUri(context, Uri.parse(“content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3A” + path2));
  46. }
  47. //转换至uriTree的路径
  48. public static String changeToUri2(String path) {
  49. String[] paths = path.replaceAll(“/storage/emulated/0/Android/data”, “”).split(“/”);
  50. StringBuilder stringBuilder = new StringBuilder(“content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata/document/primary%3AAndroid%2Fdata”);
  51. for (String p : paths) {
  52. if (p.length() == 0) continue;
  53. stringBuilder.append(“%2F”).append(p);
  54. }
  55. return stringBuilder.toString();
  56. }
  57. //转换至uriTree的路径
  58. public static String changeToUri3(String path) {
  59. path = path.replace(“/storage/emulated/0/”, “”).replace(“/”, “%2F”);
  60. return (“content://com.android.externalstorage.documents/tree/primary%3A” + path);
  61. }
  62. //获取指定目录的权限
  63. public static void startFor(String path, Activity context, int REQUEST_CODE_FOR_DIR) {
  64. statusHolder.path = path;
  65. String uri = changeToUri(path);
  66. Uri parse = Uri.parse(uri);
  67. Intent intent = new Intent(“android.intent.action.OPEN_DOCUMENT_TREE”);
  68. intent.addFlags(
  69. Intent.FLAG_GRANT_READ_URI_PERMISSION
  70. | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
  71. | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
  72. | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
  73. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  74. intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, parse);
  75. }
  76. context.startActivityForResult(intent, REQUEST_CODE_FOR_DIR);
  77. }
  78. //直接获取data权限,推荐使用这种方案
  79. public static void startForRoot(Activity context, int REQUEST_CODE_FOR_DIR) {
  80. Uri uri1 = Uri.parse(“content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fdata”);
  81. // DocumentFile documentFile = DocumentFile.fromTreeUri(context, uri1);
  82. String uri = changeToUri(Environment.getExternalStorageDirectory().getPath());
  83. uri = uri + “/document/primary%3A” + Environment.getExternalStorageDirectory().getPath().replace(“/storage/emulated/0/”, “”).replace(“/”, “%2F”);
  84. Uri parse = Uri.parse(uri);
  85. DocumentFile documentFile = DocumentFile.fromTreeUri(context, uri1);
  86. Intent intent1 = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
  87. intent1.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
  88. | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
  89. | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
  90. | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
  91. intent1.putExtra(DocumentsContract.EXTRA_INITIAL_URI, documentFile.getUri());
  92. context.startActivityForResult(intent1, REQUEST_CODE_FOR_DIR);
  93. }
  94. }

Android开发趋势及必备技术点!

一、关于Android的前景
不断地也听见很多人在谈做Android是否还有前途、Android研发在走下坡路了、Android的工作太难找了,对于这些其实我的看法很简单,现在真的还没到说Android开发已经无路可走的地步,当然未来怎样我无法预判。现在各大公司其实都很缺Android研发(中高级),不断的在招人,就拿很多一线互联网来说,别说来面试的人了,就简历都拿不到太多,需要花很多时间去找简历。所以,对于有Android开发经验的同学,更多应该想想怎么往深探索,而不是一味想着换方向,不管换到哪个方向,都会面对从初级到高级到资深再到专家的时间点。所以我认为正确的职业规划应该是金字塔形,核心竞争力一定要扎实!

二、知识点详细清单
对于现在的Android及移动互联网来说,我们需要掌握的技术,我做了一个清单:

泛型原理丶反射原理丶Java虚拟机原理丶线程池原理丶
注解原理丶注解原理丶序列化
Activity知识体系(Activity的生命周期丶Activity的任务栈丶Activity的启动模式丶View源码丶Fragment内核相关丶service原理等)
代码框架结构优化(数据结构丶排序算法丶设计模式)
APP性能优化(用户体验优化丶适配丶代码调优)
热修复丶热升级丶Hook技术丶IOC架构设计
NDK(c编程丶C++丶JNI丶LINUX)
如何提高开发效率?
MVC丶MVP丶MVVM
微信小程序
Hybrid
Flutter

 

三丶解析知识点,为什么要学
1.数据结构和算法

数据结构和算法其实是分开的东西,我们需要先掌握各种数据结构,再去加深算法,数据结构和算法其实也属于基础,但是它现在越来越重要,所以我就单独拿出来说了。数据结构怎么深入同样我也推荐大家去看清华或者浙大《数据结构》公开课,特别是清华的,值得反复研究。至于算法,首先要做的就是动手,LeetCode上直接干!第二阶段就是要总结各种算法的思想和套路,像递归、动态规划等这些算法都是有套路的,在LeetCode上也有按数据结构和算法分类的筛选,大家可以针对性练习和总结。当然,对于一个Android程序员,能做到每天在LeetCode上刷题就非常不错了,所以一定要坚持,等你坚持到一定的时间,你会发现你越来越游刃有余,我从15年底开始在LeetCode上刷题,目前已经刷了200多道了,小米的面试也非常注重算法,还要能写。而且对一些特别注重算法的公司,算法这块的考核非常严苛,对,就是严苛,而不是严格。

2.设计模式

设计模式中包括了设计原则,其实对于Android开发人员来说,设计模式就那23种,知道并了解这些设计模式是*个阶段,仅仅是到这个阶段是不够的,一般面试也不会问你某个设计模式的概念,而会让你具体的说说你对某一种设计模式的深入了解和使用,它的优缺点,所以,第二阶段就是要运用它们,其次要和Android源码中运用到设计模式地方进行结合学习。例如建造者模式,Andoird中的Dialog创建就使用到了,还有像单例模式、适配器模式、观察者模式等等都是在Android中非常常用的设计模式,也是在面试中出现频率很高的。

3.语言学习开发语言

Android应用是由Java语音进行开发的,SDK也是由Java语言编写的,所以我们要学习Java语言。另外,虽然说Kotlin语言也得到Android官方的热推,但是Kotlin也是编译成了Java语言在运行的。对于Android开发来说。只要SDK没有用Kotlin重写,那么Java语言都是需要学习的。而且Android apk的后台服务器程序大概率是Java语言构建,所以学习Java是一个必然。那么Java中那些东西是我们Android中比较相关的稍微比较难的Java基础几乎是一个门槛,像泛型丶多线程丶反射丶JVM丶JavaIO丶注解丶序列化等等

4.APP开发框架知识

这块知识是现今使用者*多的,我们称之为Android2013-2016nian 的技术。但是,即使是这样的技术,很多开发者也往往因为网上很多copy代码的习惯而导致对这块的使用的代码熟悉而陌,熟悉的是天天和它们打交道天天在复制,陌生的是天天打交道却没有深入研究过他们,要学习源码,模仿源码,然后在hook源码,这样才能说懂这块的知识。

5.App性能优化

一个app的西能好不好我们需要从两个层面分析:

①从写代码的时候就注意,让自己的代码是高性能高可用的代码,这个过程是书写高性能代码
②对已经成型的代码通过工具检测代码问题,通过检查到问题来指导我们进行代码的删改这个过程被称为调优

这里提供一份性能优化方面的学习思路给大家:

那如何写出高性能的代码呢?

需要我们具备深厚的代码功底,这就是代码的基础,如:数据结构达到可以根据应用场景写出符合当前场景的特殊结构,比如google针对Android平台特征研发了SparseArray代替HashMap.另外,对常用的算法也有自己独到的见解。

6.NDK模块开发

音视频丶高清大图片丶人工智能丶抖音直播等这些年与用户紧密相关,与我们生活*相关的技术一直都在寻找*终的技术落地平台,以前是Windows系统,而现在是移动系统了。而移动系统中Android比例又是*大的。所以NDK可以说是必备要学习的,除此之外,音视频的编解码技术流媒体协议,ffmepeg,c,c++,JNI,linux
都是音视频开发必备技能。而且OpenCV \OpenGI这些又是图像处理必备

关于NDK模块开发的学习思路:

7.如何提高开发效率?

工欲善其事必先利其器,如何提高开发效率,很多开发者在开发中由于gradle不会用导致加载代码非常耗时,这些都是你的工作成本的浪费。还有就是git的使用也可以帮助我们管理好我们的代码,这个非常关键,因为这个工具可以让我们修改的代码不会因为错误操作而导致丢失。另外,对移动开发者我们至少需要知道如何抓取网络包。其中,*常用的stetho就是一个非常好用的可以抓取网络包的工具

8.混合开发

混合开发的flutter现在已经逐渐成了主流的混合开发框架,另外由于阿里系的强大存在,导致阿里系的公司都在用Weex混合架构,这些都是一个Android工程师开拓视野,走向未来必不可少的基本技能的。

Flutter学习思维导图:

四、写在*后
不论遇到什么困难,都不应该成为我们放弃的理由!

很多人在刚接触这个行业的时候或者是在遇到瓶颈期的时候,总会遇到一些问题,比如学了一段时间感觉没有方向感,不知道该从那里入手去学习,对此我针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言,一定会认真查询,修正不足,谢谢。

Nextcloud 只在 SQLite 数据库下才能流畅运行,应该如何排查?

Nextcloud 只在 SQLite 数据库下才能流畅运行,应该如何排查?
theklf4 · 23 天前 via iPhone · 800 次点击
Docker Compose 文件:paste.ubuntu.com/p/DWmwB96rgs/
机器 10 核 60G 运行内存,运行 GitLab 非常流畅。但运行 Nextcloud 时非常卡顿,看 Chrome 的 Network 页每个请求 TTFB 都在 20 秒以上,负载 0.06 ,MySQL 和 PostgreSQL 都是这样。如果打开 Redis 则速度更慢,一堆请求超时。但是使用 SQLite 却没有任何问题,官方说 SQLite 性能很低,怎么到我这反过来了?
sqlite nextcloud 运行 请求6 条回复 • 2021-05-19 20:10:07 +08:00
theklf4 1
theklf4 23 天前 via iPhone ❤️ 1
已解决,竟然是因为容器间 IPv6 通信不通,所有请求都要卡几秒 fallback 到 IPv4 才能完成。白折腾了 2 周。
ilaipi 2
ilaipi 22 天前
我*近给公司也部署了 nextcloud,不知道楼主这边有没有遇到登录的时候,点击登录之后必须刷新之后才能进入系统。我看 network 的 login 请求状态是 已取消?
theklf4 3
theklf4 22 天前 via iPhone
@ilaipi #2 以前确实碰到过,忘记怎么解决了,好像是反代的问题,配置一下 OVERWRITE 的那两项试试?
ilaipi 4
ilaipi 22 天前
@theklf4 #3 嗯嗯,我去看看那些参数
theklf4 5
theklf4 22 天前 via iPhone
@ilaipi #4 好像在`config.php`里面
ilaipi 6
ilaipi 22 天前
@theklf4 #5 增加了 OVERWRITEHOST 和 OVERWRITEPROTOCOL 两个参数之后,确实可以正常登录了。感谢?

安卓app开机自启动代码

*近要做个大屏的开发板程序,需要长期稳定运行,并开机自启运行此软件。

废话不多说,上代码

开机自启需要广播检测,权限 android.permission.RECEIVE_BOOT_COMPLETED

1、AndroidManifest.xml中加入两行代码,红色代码

<?xml version=”1.0″ encoding=”utf-8″?>
<manifest xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
package=”包名”>

<uses-permission android:name=”android.permission.ACCESS_WIFI_STATE” />
<uses-permission android:name=”android.permission.CHANGE_WIFI_STATE” />
<uses-permission android:name=”android.permission.ACCESS_NETWORK_STATE” />
<uses-permission android:name=”android.permission.CHANGE_NETWORK_STATE” />
<uses-permission android:name=”android.permission.INTERNET” />
<!– 开机自启动–>
<uses-permission android:name=”android.permission.RECEIVE_BOOT_COMPLETED” />

<application
android:allowBackup=”true”
android:icon=”@drawable/timg”
android:label=”@string/app_name”
android:roundIcon=”@drawable/timg”
android:supportsRtl=”true”
android:theme=”@style/AppTheme”>
<activity android:name=”.MainActivity”>
<intent-filter>
<action android:name=”android.intent.action.MAIN” />

<category android:name=”android.intent.category.LAUNCHER” />
</intent-filter>
</activity>
<activity android:name=”.NetWorkActivity”></activity>
<!– 程序自启动广播 –>
<receiver android:name=”com.wisdtour.interact.Broadcast.MyBroadcastReceiver”
>
<intent-filter>
<action android:name=”android.intent.action.BOOT_COMPLETED”/>
<category android:name=”android.intent.category.LAUNCHER” />
<category android:name=”android.intent.category.HOME” />
</intent-filter>
</receiver>
</application>

</manifest>
2,编写广播接收者

import …;

//开机自启动
public class MyBroadcastReceiver extends BroadcastReceiver {
private final String ACTION_BOOT = “android.intent.action.BOOT_COMPLETED”;

@Override
public void onReceive(Context context, Intent intent) {
Log.e(“TAG”, intent.getAction());
Toast.makeText(context, intent.getAction(), Toast.LENGTH_LONG).show();

/**
* 如果 系统 启动的消息,则启动 APP 主页活动
*/

if (ACTION_BOOT.equals(intent.getAction())) {
Intent intentMainActivity = new Intent(context, MainActivity.class);
intentMainActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intentMainActivity);
Log.e(“TAG”, “开机完毕~——启动MainActivity”);
Toast.makeText(context, “开机完毕~”, Toast.LENGTH_LONG).show();
}
}

}
3,在设置中,允许程序自启动(或各种拦截软件中), 否则将被拦截(一般没有重启应该就是被拦截了)

4,重启试试

Android项目必备技术

1.一个APP只需要一个Activity

  1. //片段fragment
  2. implementation ‘me.yokeyword:fragmentation:1.3.6’
  3. implementation ‘me.yokeyword:fragmentation-swipeback:1.3.6’

2.懒人必备查找控件

  1. //ButterKnife(10.0必须适配AndroidX,9.0需要java8,快速生成插件android-butterknife-zelezny)
  2. implementation ‘com.jakewharton:butterknife:9.0.0’
  3. annotationProcessor ‘com.jakewharton:butterknife-compiler:9.0.0’

3.方法数超过65k,合并

  • a.引入jar包
    implementation "com.android.support:multidex:1.0.3"
  • b.配置合并dex为开启
  1. defaultConfig {
  2. multiDexEnabled true
  3. }

 

  • c.在自己的MyApplication下的方法中初始化
  1. @Override
  2. protected void attachBaseContext(Context base) {
  3. super.attachBaseContext(base);
  4. MultiDex.install(this);
  5. }

4.新版本的Gradle,需要至少在一个Activity中的 <intent-filter>里面添加:

 

 <action android:name="android.intent.action.VIEW" />

5.拉的自己封装的工具类需要去掉,.git的关联

https://blog.csdn.net/lyj1005353553/article/details/55519487

6.设置去掉所有的页面标题栏

在AppTheme中配置

 

 <item name="windowNoTitle">true</item>

7.快速构建Builder插件

innerbuilder插件

8.使用Dagger2无法找到对于的Component类,

a.必须在基类的onCreate方法中注入

 

  1. @Override
  2. public void onCreate(@Nullable Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. DaggerCommonComponent.create().inject(this);
  5. }

而不要注入到另一个onCreate中

 

  1. @Override
  2. public void onCreate( @Nullable Bundle savedInstanceState, @Nullable PersistableBundle persistentState) {
  3. super.onCreate(savedInstanceState, persistentState);
  4. }

9.RecyclerView嵌套ScrollView不流畅

 

  1. recyclerView.setHasFixedSize(true);
  2. recyclerView.setNestedScrollingEnabled(false);

垂直滑动问题:
https://segmentfault.com/a/1190000011553735
recyclerview嵌套在NestedScrollView里,一次性加载出全部数据问题
https://github.com/CymChad/BaseRecyclerViewAdapterHelper/issues/1954
Android SwipeRefreshLayout和RecyclerView嵌套时 下拉刷新冲突的解决办法
https://blog.csdn.net/peirato_/article/details/54913195

针对RecyclerView不显示,只需要设置ScrollView的属性android:layout_height="match_parent"android:fillViewport="true" 就OK了。

  • 监听RecyclerView滚动距离

 

  1. //RecyclerView滚动监听
  2. recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
  3. @Override
  4. public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
  5. super.onScrollStateChanged(recyclerView, newState);
  6. switch (newState){
  7. case RecyclerView.SCROLL_STATE_IDLE://现在不是滚动状态
  8. L.e(“滚动的距离==”+direction);
  9. break;
  10. case RecyclerView.SCROLL_STATE_DRAGGING://手指 拖动
  11. break;
  12. case RecyclerView.SCROLL_STATE_SETTLING://惯性滚动
  13. break;
  14. }
  15. }
  16. @Override
  17. public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
  18. super.onScrolled(recyclerView, dx, dy);
  19. //计算RecyclerView滚动的距离
  20. direction += dy;
  21. }
  22. });

RecyclerView解决数据混乱,禁止复用

 

recyclerView.getRecycledViewPool().setMaxRecycledViews(viewType,0);

https://blog.csdn.net/adojayfan/article/details/87934157

10.Retrofit网络请求,生成

 

  1. private static void setRetrofit(String defaultHost) {
  2. retrofit = new Retrofit.Builder()
  3. .baseUrl(defaultHost)
  4. .client(okHttpClient)
  5. .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
  6. .addConverterFactory(JSONObjectConverterFactory.create())
  7. .addConverterFactory(GsonConverterFactory.create()).build();
  8. }

其中JSONObjectConverterFactory和GsonConverterFactory不能共存,如果想返回JSONObject对象,去掉 .addConverterFactory(GsonConverterFactory.create())如果想直接生成Object对象,去掉.addConverterFactory(JSONObjectConverterFactory.create())

11.TextView加入删除线

 

tv.getPaint().setFlags(Paint.STRIKE_THRU_TEXT_FLAG);

代码中绘制左侧图片

 

  1. Drawable img = layout.getResources().getDrawable(R.drawable.icon);
  2. // 调用setCompoundDrawables时,必须调用Drawable.setBounds()方法,否则图片不显示
  3. img.setBounds(0, 0, img.getMinimumWidth(), img.getMinimumHeight());
  4. textView.setCompoundDrawables(img, null, null, null); //设置左图标

文字滚动消息:

 

  1. < TextView
  2. android:layout_width =“wrap_content”
  3. android:layout_height =“wrap_content”
  4. android:focusable =“true”
  5. android:focusableInTouchMode =“true”
  6. android:marqueeRepeatLimit =“marquee_forever”
  7. android:ellipsize =“marquee”
  8. android:singleLine =“true”
  9. android:text =“那么什么是成功的人生,什么样的人又是成功人士呢?像科技界的比尔?盖茨 ,乔布斯 ,商界的巴菲特以及娱乐、体育界的大腕,无疑会被人们视为是成功者。但对于普通民众,如果用比尔?盖茨那样的标准来衡量是否成功似乎有些太苛刻,也不现实。在某种观念中,一个人的成功似乎与财富分不开的。很多国人到美国看到华人*常说的一句话是,你是个成功人士。为什么这么说?因为你能住300平方米、价值百万美元的房子,因为你开的车是奔驰 、宝马等豪华车,因为你有好的工作,年薪至少在10万美元以上。这也许是很多国人看待一个人成功与否的主要标志。但在美国人眼里,成功并不是与拥有众多财富密不可分。在1980年代,多数美国人把拥有更多财富看成人生成功的一个主要标志。而在一项*新的调查中,对于美国人来说,财富不再是成功的*重要组成部分。调查中22个成功人生的潜在组成因素中,“有很多钱”仅排名在第20位。” />

12.对Retrofit回调进行封装,只输出正确和错误信息

自定义Retrofit网络回调结果

13.EditText变搜索按钮,并监听搜索事件

xml中设置android:imeOptions="actionSearch"android:singleLine="true"
代码中

 

  1. etSearch.setOnEditorActionListener(new TextView.OnEditorActionListener() {
  2. @Override
  3. public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
  4. if (actionId == EditorInfo.IME_ACTION_SEARCH){
  5. String keyWord = v.getText().toString();
  6. if (TextUtils.isEmpty(keyWord)){
  7. ToastUtil.showLong(“请输入搜索内容!”);
  8. return false;
  9. }
  10. //开始搜索keyWord相关内容
  11. }
  12. return false;
  13. }
  14. });

14.CoordinatorLayout布局要设置滚动控件的

 

app:layout_behavior="@string/appbar_scrolling_view_behavior"

里面的字符串爆红,但还是可以运行,但红色总是不好看的,可能是新版本的sdk引起的,所以需要改为

 

app:layout_behavior ="android.support.design.widget.AppBarLayout$ScrollingViewBehavior"

15.WebView不跳转到外部浏览器

 

  1. class MyWebViewClient extends WebViewClient{
  2. //不跳转到外部浏览器
  3. public boolean shouldOverrideUrlLoading(WebView view, String url) {
  4. view.loadUrl(url);
  5. return true;
  6. }
  7. }

Android webview loadData 中文乱码
https://www.jianshu.com/p/85957f003dd4

webview加载html图片过大左右滑动的解决/webview加载图片自适应大小

16.WebView出现net::ERR_UNKNOWN_URL_SCHEME错误

https://www.jianshu.com/p/119823e5cfb5

17.Glide显示图片为上面圆角,下面直角

让Glide输出指定位置的圆角图片 2018年,部分方法为Glide4.0以前的,所以无法使用,但方法值得借鉴
Glide 加载部分圆角图片2019年,新的方法,且行为更合理
圆角不圆:有可能是因为图片高度或宽度过大,导致部分圆角不圆
Glide ViewTarget及SimpleTarget加载问题:

18.View转换为Bitmap

 

  1. private Bitmap loadBitmapFromView(View v) {
  2. int w = v.getWidth();
  3. int h = v.getHeight();
  4. Bitmap bmp = Bitmap.createBitmap(w, h, Bitmap.Config.RGB_565);
  5. Canvas c = new Canvas(bmp);
  6. c.drawColor(Color.WHITE);
  7. /** 如果不设置canvas画布为白色,则生成透明 */
  8. v.layout(0, 0, w, h);
  9. v.draw(c);
  10. return bmp;
  11. }

关于View转化成bitmap保存成图片
两个Bitmap合并为一个
Android:将一个Activity、某块布局转换成图片

19.使用xml绘制虚线好后,4.0以上需要使用 android:layerType=”software”属性,不然虚线会变为实线

https://blog.csdn.net/Small_Lee/article/details/52153557

20.微信分享到朋友圈,调用系统分享,需要对Uri单独处理一下

https://blog.csdn.net/qq_34900897/article/details/85320646

21.support版本冲突的解决办法

https://blog.csdn.net/yuzhiqiang_1993/article/details/78214812

22.TabLayout+ViewPager垂直方向联动,用于分类页面

https://www.jianshu.com/p/9266e58cc4f5

23.精确计算RecyclerView滑动高度

SlideRecyclerView

24.adb无线调试

https://jingyan.baidu.com/article/066074d610f4f3c3c21cb0ab.html

25.打包名称自定义

https://www.cnblogs.com/bluestorm/p/6228085.html
https://blog.csdn.net/weixin_33709364/article/details/87160660
打包方法过时警告:https://www.cnblogs.com/blogs-of-lxl/p/10306145.html
我的通用命名方式:包名*后一部分+版本名称+时间+打包方式 taobao_v1.0_2019-05-20_release
在android{}里面写入

 

  1. android.applicationVariants.all { variant ->
  2. variant.outputs.all{
  3. outputFileName = ${applicationId.subSequence(applicationId.lastIndexOf(“.”)+1,applicationId.length())}_v${versionName}_${releaseTime()}_${baseName}.apk”
  4. }
  5. }

其中releaseTime()为写在android{}外部的一个方法

 

  1. def releaseTime() {
  2. return new Date().format(“yyyy-MM-dd”, TimeZone.getTimeZone(“UTC”))
  3. }

26.apk签名打包,

如何配置签名及生成签名文件:
https://www.cnblogs.com/details-666/p/keystore.html
如何判断你的apk是否已经签名:
https://blog.csdn.net/qq_21376985/article/details/53337977

27.混淆

https://blog.csdn.net/qq634416025/article/details/79686051

28.TextView设置可滚动的方式

xml中设置TextView属性

 

android:scrollbars="vertical"

同时代码中设置

 

 textView.setMovementMethod(ScrollingMovementMethod.getInstance());

29.Android:通过Glide保存图片到本地,并同步到相册

30.Android 调用系统分享(不使用第三方),指定QQ、微信等

https://blog.csdn.net/u010356768/article/details/78246691
qq空间限制:
https://blog.csdn.net/weixin_41239127/article/details/78743421

31.安卓从imageview中获得bitmap的方法

https://blog.csdn.net/yj1499945/article/details/47079621

32.怎样获取到Android控件的高度,通过监听的方式

https://www.jianshu.com/p/2c8e5324ec68

你可能也不知道为什么,*次进入页面,获取控件的高度有值,再次进入获取的高度居然为0,再再再次进入也为0,杀掉应用,进入页面又有高度了,再次进入又为0。因为我需要通过view来获取Bitmap,那么View的宽高值必不可少,所以我通过上面博客的方法去监听控件的高度才拿到值。但为什么只有*次进入才能拿到宽高值却拜师不得琪姐,请各位大老解答。

33.AccessibilityService获取控件信息getRootInActiveWindow() 经常为null

https://blog.csdn.net/qq_28210079/article/details/80486592

33.解决android.permission.WRITE_APN_SETTINGS

https://blog.csdn.net/qq_36437339/article/details/81015715

34.新手引导库

GuideView

在Fragment中由于控件位置绘制流程和生命周期的关系,需要监听控件View宽高,有值后才进行引导层的绘制,同时用Handler进行一定的延迟绘制,保证高亮区域的定位精确度

35.Java中对对象进行排序

https://blog.csdn.net/qq_37937537/article/details/80445731

通过Comparable

36.监控当前APP是否回到前台

https://www.jianshu.com/p/101eb42d0fde

37.使用java8新特性快速的打印输出序列对象

 

allList.stream().map(User::toString).forEach(L::e);

38.Fragment的优化

不要再Activity中使用List来保存Fragment
https://blog.csdn.net/qq_30993595/article/details/80736814

39.ViewPager2,支持横竖布局,支持0预加载布局

https://juejin.im/post/5cda3964f265da035d0c9d8f

40.RadioGroup.onCheckedChanged() 会调用多次

https://blog.csdn.net/qq_32452623/article/details/80474487

41.Retrofit2上传图片

https://www.cnblogs.com/zhujiabin/p/7601658.html

42.Java多线程

Java多线程系列目录(共43篇)

43.ArrayList进行排序

24版本以前的老方法:
Collections.sort(arrays);
新方法
arrays.sort();

44.对时间的转换,对比,添加,格式输出

joda-time

45. tools:replace的使用

tools:replace=””有时候需要替换多个项,使用逗号分割
tools:replace=”android:allowBackup,android:appComponentFactory”

46. 解决 导入三方时出现: appComponentFactory 错误

https://blog.csdn.net/qq_34224268/article/details/83861897

47. 滚轮时间选择器

可以采用 XPopupWheelPicker
组合的方式来生成一个时间选择器如:https://www.jianshu.com/p/4a2c853d9276

48. fragment里coordinatorlayout+viewpager无法正常滑动问题

fragment里coordinatorlayout+viewpager无法正常滑动问题
有人说fragment是无法运行协调者布局的,这是错误的
解决方法:在你的viewpager子fragment里面布局*外面套上一层NestedScrollView就可以了

49.查看SHA1命令

keytool -list -v -keystore C:\Users\Desktop\browser\debug.keystore -storepass android
后面的android为当前密钥的密码

50.RecyclerView横向分页菜单

https://blog.csdn.net/u014165119/article/details/46834265

51.ArgbEvaluator一个计算颜色渐变值的类

https://blog.csdn.net/u013581141/article/details/68063469
使用事例:自定义CoordinatorLayout.Behavior颜色渐变的TitleBar

52.交互效果咋了系列:

自定义ViewGroup第十三式之移花接木

53.数据绑定DataBind

 

  1. android {
  2. dataBinding {
  3. enabled = true
  4. }
  5. }

告别findView和ButterKnife
Android开发教程 – 使用Data Binding(七)使用BindingAdapter简化图片加载

54.错误检查命令

gradlew processDebugManifest –stacktrace

55.new Handler的警告说明和解决办法@SuppressLint(“HandlerLeak”)

https://blog.csdn.net/androidsj/article/details/79865091

 

  1. private MyHandler myHandler = new MyHandler(_mActivity);
  2. private static class MyHandler extends Handler {
  3. WeakReference weakReference;
  4. public MyHandler(Activity activity) {
  5. weakReference = new WeakReference(activity);
  6. }
  7. @Override
  8. public void handleMessage(Message msg) {
  9. }
  10. }

另一种写法:

 

  1. private Handler mHandler = new Handler(new Handler.Callback() {
  2. @Override
  3. public boolean handleMessage(Message msg) {
  4. return false;
  5. }
  6. });

56.多行且需要*后显示省略号的警告处理

logcat 总是报: W/StaticLayout: maxLineHeight should not be -1. maxLines:1 lineCount:1

57.通知无法显示:8.0需要设置渠道和权重

https://www.jianshu.com/p/f85ef58edf63

58.uri指向了外部应用exposed beyond app through RemoteViews.setUri()

在Application中的onCreate方法中添加如下

 

  1. StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
  2. StrictMode.setVmPolicy(builder.build());
  3. builder.detectFileUriExposure();

59.解决因为弹窗导致TextView停止滚动的问题:

https://www.jianshu.com/p/22b4aff0dc8e

60.弹窗需要一个自定义的三角形SanJiaoView :

https://blog.csdn.net/ZhangLei280/article/details/73207669

61.升级AndroidX后出现缺失AppBarLayout$ScrollingViewBehavior

https://www.jianshu.com/p/6b8104787617

62.APP启动时间,性能优化,启动页白屏

https://www.jianshu.com/p/75b0b128c470

63.AndroidStudio安装失败

%title插图%num

image.png

 

方法一:clean项目
方法二:重启大法:重启AndroidStudio,重启手机,重启电脑,重启…
方法三:检查是否只开启了开发者模式和USB调试,却没有开启了USB安装

 

%title插图%num

image.png

64.adb命令

https://www.jianshu.com/p/56fd03f1aaae

65.ConstraintLayout基础 及动态控件(动画效果)

https://www.jianshu.com/p/7888cde8292f

66.获取videoView*针视频报错:

https://blog.csdn.net/guohesheng/article/details/80236799

67.Math函数相关计算

//角度换算为对应数值
double skewRot = Math.toRadians(30);

68. banner库一张图片无效和*次进入没有轮播的时候点击*张无效

一张图片的时候需要设置banneronClick事件,多张图*次还未开始轮播点击无效,需要先设置点击事件,再调用bannerstart方法

69. <activity-alias> 标签的使用

<activity-alias> 标签的使用

70.Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK

https://blog.csdn.net/watermusicyes/article/details/44963773

71.程序崩溃日志收集

 

  1. Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
  2. @Override
  3. public void uncaughtException(Thread thread, final Throwable ex) {
  4. // Custom code here to handle the error.
  5. L.e(“发生崩溃==”+thread.getName()+” ==”+ex.getMessage());
  6. }
  7. });

72.Android Q正式发布。新一代的安卓系统非常注重对用户隐私的保护,它限制APP获取IMEI、DEVICE ID等移动端设备标识码,解决方案:

简书:https://www.jianshu.com/p/df3f549ddd35
官方:https://developer.android.google.cn/training/articles/user-data-ids

73.再按一次退出程序

 

  1. private long firstTime = 0;
  2. @Override
  3. public boolean onKeyUp(int keyCode, KeyEvent event) {
  4. switch (keyCode) {
  5. case KeyEvent.KEYCODE_BACK:
  6. long secondTime = System.currentTimeMillis();
  7. //如果两次按键时间间隔大于2秒,则不退出
  8. if (secondTime – firstTime > 2000) {
  9. Toast.makeText(this, “再按一次退出程序”, Toast.LENGTH_SHORT).show();
  10. firstTime = secondTime;//更新firstTime
  11. return true;
  12. //两次按键小于2秒时,退出应用
  13. } else {
  14. System.exit(0);
  15. }
  16. break;
  17. }
  18. return super.onKeyUp(keyCode, event);
  19. }

如果是Fragment请不要复写onBackPressed()方法,改为复写onBackPressedSupport():

 

  1. //再按一次退出程序
  2. private long firstTime = 0;
  3. @Override
  4. public void onBackPressedSupport() {
  5. long secondTime = System.currentTimeMillis();
  6. if (getSupportFragmentManager().getBackStackEntryCount() == 1 && secondTime – firstTime > 2000) {
  7. Toast.makeText(this, “再按一次退出程序”, Toast.LENGTH_SHORT).show();
  8. firstTime = secondTime;
  9. } else {
  10. super.onBackPressedSupport();
  11. }
  12. }

74.AndroidX升级日记

https://www.jianshu.com/p/499e645ad148

75.AndroidStudio启动模拟器Android10.0报错

Emulator: Process finished with exit code -1073741819 (0xC0000005)
https://stackoverflow.com/questions/47631771/emulator-process-finished-with-exit-code-1073741819-0xc0000005

76.简单实现ImageView宽度填满屏幕,高度自适应的两种方式

https://www.jianshu.com/p/c9424615e99d

77.RecyclerView瀑布流问题

https://www.jianshu.com/p/4e142909b824
https://blog.csdn.net/tobevan/article/details/78924338

78.android studio关闭字符串资源多语言提示

https://blog.csdn.net/sinat_26814541/article/details/97757535

79.ConstraintLayout 采用代码方式布局用法简介

http://xgfe.github.io/2017/09/17/ivanchou/layout-with-constraintlayout-by-programming/

80.Glide获得图片资源,并高斯模糊

高斯模糊是个耗时过程,需要在子线程进行操作

 

  1. try {
  2. Drawable drawable = Glide.with(mContext)
  3. .load(item.getImage_url())
  4. .apply(RequestOptions.bitmapTransform(new BlurTransformation(15, 1)))
  5. .submit().get();
  6. layoutRoot.setBackground(drawable);
  7. } catch (ExecutionException e) {
  8. e.printStackTrace();
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }

高斯模糊

 

  1. public class BlurTransformation extends BitmapTransformation {
  2. private static final int VERSION = 1;
  3. private static final String ID = “BlurTransformation.” + VERSION;
  4. private static int MAX_RADIUS = 25;
  5. private static int DEFAULT_DOWN_SAMPLING = 1;
  6. private int radius;
  7. private int sampling;
  8. public BlurTransformation() {
  9. this(MAX_RADIUS, DEFAULT_DOWN_SAMPLING);
  10. }
  11. public BlurTransformation(int radius) {
  12. this(radius, DEFAULT_DOWN_SAMPLING);
  13. }
  14. public BlurTransformation(int radius, int sampling) {
  15. this.radius = radius;
  16. this.sampling = sampling;
  17. }
  18. @Override
  19. protected Bitmap transform(@NonNull BitmapPool pool, @NonNull Bitmap toTransform, int outWidth, int outHeight) {
  20. int width = toTransform.getWidth();
  21. int height = toTransform.getHeight();
  22. int scaledWidth = width / sampling;
  23. int scaledHeight = height / sampling;
  24. Bitmap bitmap = pool.get(scaledWidth, scaledHeight, Bitmap.Config.ARGB_8888);
  25. Canvas canvas = new Canvas(bitmap);
  26. canvas.scale(1 / (float) sampling, 1 / (float) sampling);
  27. Paint paint = new Paint();
  28. paint.setFlags(Paint.FILTER_BITMAP_FLAG);
  29. canvas.drawBitmap(toTransform, 0, 0, paint);
  30. bitmap = FastBlur.blur(bitmap, radius, true);
  31. return bitmap;
  32. }
  33. @Override public String toString() {
  34. return “BlurTransformation(radius=” + radius + “, sampling=” + sampling + “)”;
  35. }
  36. @Override public boolean equals(Object o) {
  37. return o instanceof BlurTransformation &&
  38. ((BlurTransformation) o).radius == radius &&
  39. ((BlurTransformation) o).sampling == sampling;
  40. }
  41. @Override public int hashCode() {
  42. return ID.hashCode() + radius * 1000 + sampling * 10;
  43. }
  44. @Override public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
  45. messageDigest.update((ID + radius + sampling).getBytes(CHARSET));
  46. }
  47. }

其中的FastBlur来至库glide-transformations

81.Glide获得获取图片主体色系

 

  1. Glide.with(mContext).load(item.getImage_url())
  2. .listener(GlidePalette.with(item.getImage_url())
  3. .use(GlidePalette.Profile.VIBRANT)
  4. .intoBackground(layoutRoot, GlidePalette.Swatch.RGB)
  5. .crossfade(true)
  6. ).into(imageView);

82.Android获取内网IP

方法一:

 

  1. package com.rongyan.clienttest;
  2. import java.net.InetAddress;
  3. import java.net.NetworkInterface;
  4. import java.net.SocketException;
  5. import java.util.Enumeration;
  6. import java.util.regex.Matcher;
  7. import java.util.regex.Pattern;
  8. public class NetWorkUtil {
  9. //匹配C类地址的IP
  10. public static final String regexCIp = “^192\\.168\\.(\\d{1}|[1-9]\\d|1\\d{2}|2[0-4]\\d|25\\d)\\.(\\d{1}|[1-9]\\d|1\\d{2}|2[0-4]\\d|25\\d)$”;
  11. //匹配A类地址
  12. public static final String regexAIp = “^10\\.(\\d{1}|[1-9]\\d|1\\d{2}|2[0-4]\\d|25\\d)\\.(\\d{1}|[1-9]\\d|1\\d{2}|2[0-4]\\d|25\\d)\\.(\\d{1}|[1-9]\\d|1\\d{2}|2[0-4]\\d|25\\d)$”;
  13. //匹配B类地址
  14. public static final String regexBIp = “^172\\.(1[6-9]|2\\d|3[0-1])\\.(\\d{1}|[1-9]\\d|1\\d{2}|2[0-4]\\d|25\\d)\\.(\\d{1}|[1-9]\\d|1\\d{2}|2[0-4]\\d|25\\d)$”;
  15. public static String getHostIp() {
  16. String hostIp;
  17. Pattern ip = Pattern.compile(“(“ + regexAIp + “)|” + “(“ + regexBIp + “)|” + “(“ + regexCIp + “)”);
  18. Enumeration<NetworkInterface> networkInterfaces = null;
  19. try {
  20. networkInterfaces = NetworkInterface.getNetworkInterfaces();
  21. } catch (SocketException e) {
  22. e.printStackTrace();
  23. }
  24. InetAddress address;
  25. while (networkInterfaces.hasMoreElements()) {
  26. NetworkInterface networkInterface = networkInterfaces.nextElement();
  27. Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
  28. while (inetAddresses.hasMoreElements()) {
  29. address = inetAddresses.nextElement();
  30. String hostAddress = address.getHostAddress();
  31. Matcher matcher = ip.matcher(hostAddress);
  32. if (matcher.matches()) {
  33. hostIp = hostAddress;
  34. return hostIp;
  35. }
  36. }
  37. }
  38. return null;
  39. }
  40. }

方法二:

 

  1. public String getWifiIp() {
  2. //获取wifi服务
  3. WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
  4. //判断wifi是否开启
  5. if (!wifiManager.isWifiEnabled()) {
  6. wifiManager.setWifiEnabled(true);
  7. }
  8. WifiInfo wifiInfo = wifiManager.getConnectionInfo();
  9. int ipAddress = wifiInfo.getIpAddress();
  10. return intToIp(ipAddress);
  11. }
  12. //获取Wifi ip 地址
  13. private String intToIp(int i) {
  14. return (i & 0xFF) + “.” +
  15. ((i >> 8) & 0xFF) + “.” +
  16. ((i >> 16) & 0xFF) + “.” +
  17. (i >> 24 & 0xFF);
  18. }

参考:https://www.cnblogs.com/jxust-jiege666/p/8168149.html

83.Android获取外网IP

参考:https://www.jianshu.com/p/1e3eaf887191

方法一,通过访问第三方接口地址来获取

 

  1. import org.json.JSONException;
  2. import org.json.JSONObject;
  3. /**
  4. * 获取外网IP地址
  5. * @return
  6. */
  7. public void GetNetIp() {
  8. new Thread(){
  9. @Override
  10. public void run() {
  11. String line = “”;
  12. URL infoUrl = null;
  13. InputStream inStream = null;
  14. try {
  15. infoUrl = new URL(“http://pv.sohu.com/cityjson?ie=utf-8”);
  16. URLConnection connection = infoUrl.openConnection();
  17. HttpURLConnection httpConnection = (HttpURLConnection) connection;
  18. int responseCode = httpConnection.getResponseCode();
  19. if (responseCode == HttpURLConnection.HTTP_OK) {
  20. inStream = httpConnection.getInputStream();
  21. BufferedReader reader = new BufferedReader(new InputStreamReader(inStream, “utf-8”));
  22. StringBuilder strber = new StringBuilder();
  23. while ((line = reader.readLine()) != null)
  24. strber.append(line + “\n”);
  25. inStream.close();
  26. // 从反馈的结果中提取出IP地址
  27. int start = strber.indexOf(“{“);
  28. int end = strber.indexOf(“}”);
  29. String json = strber.substring(start, end + 1);
  30. if (json != null) {
  31. JSONObject jsonObject = null;
  32. try {
  33. jsonObject = new JSONObject(json);
  34. } catch (JSONException e) {
  35. e.printStackTrace();
  36. }
  37. line = jsonObject.optString(“cip”);
  38. }
  39. L.e(“line==”+line);
  40. Message msg = new Message();
  41. msg.what = 1;
  42. msg.obj = line;
  43. }
  44. } catch (MalformedURLException e) {
  45. e.printStackTrace();
  46. } catch (IOException e) {
  47. e.printStackTrace();
  48. }
  49. }
  50. }.start();
  51. }

84.颜色均匀渐变的动画

 

  1. ValueAnimator animator = ValueAnimator.ofInt(0xffffff00,0xff0000ff);
  2. animator.setEvaluator(new ArgbEvaluator());
  3. animator.setDuration(3000);
  4. animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  5. @Override
  6. public void onAnimationUpdate(ValueAnimator animation) {
  7. int curValue = (int)animation.getAnimatedValue();
  8. tv.setBackgroundColor(curValue);
  9. }
  10. });
  11. animator.start();

85.Android的cpu架构7种

参考:https://blog.csdn.net/u012400885/article/details/52923765
https://www.jianshu.com/p/f29ad4beef59

86.’gradle’ 不是内部或外部命令,也不是可运行的程序 或批处理文件

https://blog.csdn.net/u014743890/article/details/84316176

87.使用ImmersionBar时,加载出现Dialog的显示会导致导航栏出现不消失:

88.Dagger2-Android不支持泛型Activity的注入

https://blog.csdn.net/ybf326/article/details/82931587

89.删除git配置

%title插图%num

删除

90.把本地项目复制到github

%title插图%num

分享

91.AndroidStudio设置github账号

%title插图%num

image.png

92.邀请码识别来源

https://www.openinstall.io/

93.Android 自动抓取网站图标实现分享样式的定制

https://blog.csdn.net/dnsliu/article/details/57122535

94.Fragment实现懒加载

https://blog.csdn.net/vic6329063/article/details/82838430

95.版权登记

http://www.ccopyright.com/index.php?optionid=1216

96.获取随机颜色值

分别取rgb的随机值(0~256),然后加起来就是一个随机颜色值,通过Color.parseColor()转为color值即可使用:

 

  1. public static String getRandColor() {
  2. String R, G, B;
  3. Random random = new Random();
  4. R = Integer.toHexString(random.nextInt(256)).toUpperCase();
  5. G = Integer.toHexString(random.nextInt(256)).toUpperCase();
  6. B = Integer.toHexString(random.nextInt(256)).toUpperCase();
  7. R = R.length() == 1 ? “0” + R : R;
  8. G = G.length() == 1 ? “0” + G : G;
  9. B = B.length() == 1 ? “0” + B : B;
  10. return “#” + R + G + B;
  11. }

97.Glide设置圆角图片后设置ImageVIew的scanType=”centerCrop”无效解决办法

https://www.jianshu.com/p/95d3f64a48dc

 

  1. RequestOptions options = new RequestOptions()
  2. .transform(new CenterCrop(),new RoundedCorners(18))
  3. .placeholder(R.drawable.no_banner)
  4. .error(R.drawable.no_banner);
  5. Glide.with(context).load(path)
  6. .apply(options)
  7. .thumbnail(0.1f)
  8. .into(imageView);

98.线性渐变字体

https://blog.csdn.net/negineko/article/details/100033250

 

  1. public class GradientColorTextView extends AppCompatTextView {
  2. private Rect mTextBound = new Rect();
  3. public GradientColorTextView (Context context, AttributeSet attrs) {
  4. super(context, attrs);
  5. }
  6. @Override
  7. protected void onDraw(Canvas canvas) {
  8. int mViewWidth = getMeasuredWidth();
  9. Paint mPaint = getPaint();
  10. String mTipText = getText().toString();
  11. mPaint.getTextBounds(mTipText, 0, mTipText.length(), mTextBound);
  12. @SuppressLint(“DrawAllocation”) LinearGradient mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0, new int[]{0xFFFF0000, 0xFF5400FF}, null, Shader.TileMode.REPEAT);
  13. mPaint.setShader(mLinearGradient);
  14. canvas.drawText(mTipText, getMeasuredWidth() / 2 – mTextBound.width() / 2, getMeasuredHeight() / 2 + mTextBound.height() / 2, mPaint);
  15. }
  16. }

99.TextView 设置其他字体后,显示不全的问题

https://blog.csdn.net/u010979599/article/details/86650297
https://www.jianshu.com/p/4314cc68c1f3

100.Android视频播放,选择,压缩,上传

https://www.jianshu.com/p/78b7176c041e

101.循环枚举获得枚举值

 

  1. public static <T extends Enum<T> & BaseEnum> T getEnumType(String enumCode, Class<T> enumClass) {
  2.   T enumType = null;
  3.   EnumSet<T> enumSet = EnumSet.allOf(enumClass); //获取枚举类型
  4.   for (T enumItem : enumSet) { //循环枚举
  5.     if(enumItem.getEnumCode().equals(enumCode)) {
  6.       enumType = enumItem;
  7.       break;
  8.     }
  9.   }
  10.   return enumType;
  11. }

102.权限申请

AndPermission
文档:https://yanzhenjie.com/AndPermission/cn/

 

  1. AndPermission.with(FortuneFragment.this)
  2. .runtime()
  3. .permission(Permission.Group.STORAGE)
  4. .onGranted(permissions -> {
  5. ToastUtil.showShort(“get permiss”);
  6. })
  7. .onDenied(new Action<List<String>>() {
  8. @Override
  9. public void onAction(List<String> permissions) {
  10. ToastUtil.showShort(“你拒*了获取此权限!”);
  11. // 这些权限被用户总是拒*。
  12. if (AndPermission.hasAlwaysDeniedPermission(FortuneFragment.this, permissions)) {
  13. new AlertDialog.Builder(FortuneFragment.this.getActivity())
  14. .setTitle(“权限申请”)
  15. .setMessage(“需要此权限才能使用此功能,去设置?”)
  16. .setPositiveButton(“去设置”, new DialogInterface.OnClickListener() {
  17. @Override
  18. public void onClick(DialogInterface dialog, int which) {
  19. AppUtils.goIntentSetting(FortuneFragment.this.getActivity());
  20. }
  21. })
  22. .setNegativeButton(“取消”,null)
  23. .show();
  24. }
  25. }
  26. })
  27. .start();

103.Android TextView 判断文字内容是否超出显示省略号

https://blog.csdn.net/gentlemanyc/article/details/49967719?locationNum=2

104.关于Android弹出软键盘“顶起”View的问题

https://blog.csdn.net/q4878802/article/details/94382815

 

  1. public void setHintKeyboardView(View view) {
  2. view.setOnTouchListener(new View.OnTouchListener() {
  3. public boolean onTouch(View v, MotionEvent event) {
  4. hintKeyboard(NewsInfoActivity.this);
  5. return false;
  6. }
  7. });
  8. if (view instanceof ViewGroup){
  9. ViewGroup viewGroup = (ViewGroup) view;
  10. for (int i = 0; i < viewGroup.getChildCount(); i++) {
  11. View innerView = viewGroup.getChildAt(i);
  12. setHintKeyboardView(innerView);
  13. }
  14. }
  15. }

要顶起的控件

 

 setHintKeyboardView(db.layoutComment);

主要布局xml中要设置:android:fitsSystemWindows="true"才生效

105.DataBind中神奇的“*”字

出现异常:
Caused by: org.apache.xerces.impl.io.MalformedByteSequenceException: Invalid byte 3 of 3-byte UTF-8 sequence.
原因是在xml中使用了“*”字和对象的结合

 

  1. <TextView
  2. android:id=“@+id/textView18”
  3. android:layout_width=“wrap_content”
  4. android:layout_height=“wrap_content”
  5. android:layout_marginTop=“8dp”
  6. android:text=@{`*大特征:`+data.attr.zdtd}
  7. android:textColor=“@color/textDarkGray”
  8. android:textSize=“14sp”
  9. app:layout_constraintStart_toStartOf=“@+id/textView17”
  10. app:layout_constraintTop_toBottomOf=“@+id/textView17”
  11. tools:text=“*大特征:渴望” />

估计是DataBind的一个bug,只有在代码中写这个“*”字了。

 

db.textView18.setText("*大特征:"+data.getAttr().getZdtd());

106.可以调整间隙的星星控件

SimpleRatingBar
如果使用DataBind出错,需要下载下来,写入设置星星个数的setter方法。

107.背景高斯模糊

 

  1. private Bitmap applyBlur() {
  2. Bitmap bitmap = ScreenUtils.snapShotWithStatusBar(this.getActivity());
  3. return blur(bitmap);
  4. }
  5. @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
  6. private Bitmap blur(Bitmap bkg) {
  7. long startMs = System.currentTimeMillis();
  8. float radius = 1;
  9. bkg = small(bkg);
  10. Bitmap bitmap = bkg.copy(bkg.getConfig(), true);
  11. final RenderScript rs = RenderScript.create(this.getContext());
  12. final Allocation input = Allocation.createFromBitmap(rs, bkg, Allocation.MipmapControl.MIPMAP_NONE,
  13. Allocation.USAGE_SCRIPT);
  14. final Allocation output = Allocation.createTyped(rs, input.getType());
  15. final ScriptIntrinsicBlur script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
  16. script.setRadius(radius);
  17. script.setInput(input);
  18. script.forEach(output);
  19. output.copyTo(bitmap);
  20. bitmap = big(bitmap);
  21. MainActivity.ivCover.setBackground(new BitmapDrawable(getResources(), bitmap));
  22. rs.destroy();
  23. Log.d(“zhangle”,“blur take away:” + (System.currentTimeMillis() – startMs )+ “ms”);
  24. return bitmap;
  25. }
  26. private static Bitmap big(Bitmap bitmap) {
  27. Matrix matrix = new Matrix();
  28. matrix.postScale(4f,4f); //长和宽放大缩小的比例
  29. Bitmap resizeBmp = Bitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,true);
  30. return resizeBmp;
  31. }
  32. private static Bitmap small(Bitmap bitmap) {
  33. Matrix matrix = new Matrix();
  34. matrix.postScale(0.25f,0.25f); //长和宽放大缩小的比例
  35. Bitmap resizeBmp = Bitmap.createBitmap(bitmap,0,0,bitmap.getWidth(),bitmap.getHeight(),matrix,true);
  36. return resizeBmp;
  37. }

108.java Enum 枚举帮助类,根据值或者名称获取枚举类型对象

https://blog.csdn.net/busbanana/article/details/72954676

109’Android打开系统拍照&相册获取头像

https://www.jianshu.com/p/e29e24b88440

110. FileProvider相关

Android之FileProvider :通过FileProvider来获取content uri
https://blog.csdn.net/yegshun/article/details/81478619
android 7.0+ FileProvider 访问隐私文件 相册、相机、安装应用的适配

111. view 可见性 监听探究

https://www.jianshu.com/p/a5cc954b997c

112. android checkbox 未选中状态 已选中状态 替换成自己的图片

113. 获取今日,明日,本周,本月,今年时间

https://www.cnblogs.com/shuilangyizu/p/6902643.html

 

  1. Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone(“GMT+08:00”));
  2. int firstDayOfWeek = calendar.getFirstDayOfWeek();
  3. List<String> times =new ArrayList<>();
  4. for (int i = 0; i < 7; i++) {
  5. calendar.set(Calendar.DAY_OF_WEEK, firstDayOfWeek + i);
  6. // 获取星期的显示名称,例如:周一、星期一、Monday等等
  7. String format = new SimpleDateFormat(“MM月dd日”).format(calendar.getTime());
  8. times.add(format);
  9. }
  10. String time6 = times.get(6);
  11. time6 = time6.substring(time6.indexOf(“月”)+1);
  12. calendar.add(Calendar.DAY_OF_MONTH, -1);
  13. String jr = new SimpleDateFormat(“MM月dd日”).format(calendar.getTime());
  14. calendar.add(Calendar.DAY_OF_MONTH, +1);
  15. String mr = new SimpleDateFormat(“MM月dd日”).format(calendar.getTime());
  16. dayName[0] = “今日运势(“+jr+“)”;
  17. dayName[1] = “明日运势(“+mr+“)”;
  18. dayName[2] = “本周运势(“+times.get(0)+“-“+time6+“)”;
  19. dayName[3] = “本月运势(“+(calendar.get(Calendar.MONTH) + 1)+“月)”;
  20. dayName[4] = “今年运势(“+calendar.get(Calendar.YEAR)+“年)”;
  21. dayNameShow = dayName[0];

114. Android定位相关

Android系统GPS定位实现
Andriod 手机定位 解决location为null的问题
Android地图开发中的地理编码与地理反编码
经纬度查询:https://www.juhe.cn/cellmap/lat
Android之GPS定位类 LocationManager、LocationListener、GpsStatus.Listener、Location详解

总结,使用:
(方式一)locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 3000, 0, locationListener);
(方式二)locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 3000, 0, locationListener);好用得多!
使用网络方式定位比gps定位更高概率拿到Location对象,从而获取经纬度。但要注意使用网络LocationManager.NETWORK_PROVIDER的方式,依然要开启gps和网络,否则依然无法获取Location对象。而(方式二)我只有一次获取成功了,之后的监听再也没拿到过Location对象。

115. Android TextView加上阴影效果

https://blog.csdn.net/hewence1/article/details/39993415

116.Android中给View设置阴影的三种方式

https://blog.csdn.net/wang29169/article/details/84206379
关于CardView参考:使用CardView实现卡片式设计
或者第三方的控件 shadow-layout

117.Android去掉阻尼效果

只需要在控件中添加如下属性即可:android:overScrollMode="never"

118.Android版本升级坑

https://www.jianshu.com/p/123c12218b7a

119.startActivityForResult的用法,以及intent传递图片

https://blog.csdn.net/bangxianzhou5100/article/details/101077196

120.第三方地图接入相关

高德地图接入笔记

加速国内 WordPress 站点官方后台服务访问(以腾讯轻量为例)

通过反代官方 API 加速位于国内的 WordPress 站点访问更新、主题等等服务的方法,*初国内 429 的时候 WP-ChinaYes 那个项目提出来的。那时候研究了下用一个域名完成整个反代,稳定使用了接近一年了,分享出来~

WP-ChinaYes 后来走了劫持并二次处理官方 API 的路线,个人不是很喜欢;而且个人需求也没到他们规划的程度,自己搭建的安全性方面也更放心。

在这里服务器推荐腾讯的轻量,香港或者硅谷到大陆链路都不错,我测试到国内腾讯云、华为云、UCloud 等等云厂都是走的 CN2 回国,24 一个月速度很棒也很划算,拿来搭建这样的服务非常合适。

*注意鹅厂的轻量不能给你在阿里云国内机器上的用,两家不和绕的很感人很慢很慢。

准备一个反代的域名,例如 proxy.example.com (文件中 20 处自行替换),SSL 自己配置好,反代的配置文件如下:

#PROXY-API
location /api/
{
proxy_pass https://api.wordpress.org/;
proxy_set_header Host api.wordpress.org;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Accept-Encoding “”;
proxy_redirect https://developer.wordpress.org https://proxy.example.com/developer.wordpress.org;

gzip off;
sub_filter ps.w.org proxy.example.com/ps.w.org;
sub_filter downloads.wordpress.org proxy.example.com/dl;
sub_filter profiles.wordpress.org proxy.example.com/profiles.wordpress.org;
sub_filter secure.gravatar.com proxy.example.com/secure.gravatar.com;
sub_filter 0.gravatar.com proxy.example.com/secure.gravatar.com;
sub_filter 1.gravatar.com proxy.example.com/secure.gravatar.com;
sub_filter 2.gravatar.com proxy.example.com/secure.gravatar.com;
sub_filter ts.w.org proxy.example.com/ts.w.org;
sub_filter wp-themes.com proxy.example.com/wp-themes.com;
sub_filter s.w.org proxy.example.com/s.w.org;
sub_filter wordpress.org proxy.example.com/wordpress.org;
sub_filter_last_modified on;
sub_filter_once off;
sub_filter_types application/json;

}
#PROXY-DL
location /dl/
{
proxy_pass https://downloads.wordpress.org/;
proxy_set_header Host downloads.wordpress.org;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_redirect https://wordpress.org/download/ https://proxy.example.com/wordpress.org/download/;
}
#PROXY-Developer
location /developer.wordpress.org/
{
proxy_pass https://developer.wordpress.org/;
proxy_set_header Host developer.wordpress.org;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Accept-Encoding “”;

gzip off;
sub_filter downloads.wordpress.org proxy.example.com/dl;
sub_filter developer.wordpress.org proxy.example.com/developer.wordpress.org;
sub_filter ps.w.org proxy.example.com/ps.w.org;
sub_filter_last_modified on;
sub_filter_once off;
}
#PROXY-WPORG
location /wordpress.org/
{
proxy_pass https://wordpress.org/;
proxy_set_header Host wordpress.org;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Accept-Encoding “”;

gzip off;
sub_filter //wordpress.org //proxy.example.com/wordpress.org;
sub_filter s.w.org proxy.example.com/s.w.org;
sub_filter downloads.wordpress.org proxy.example.com/downloads.wordpress.org;
sub_filter_last_modified on;
sub_filter_once off;
}
#PROXY-WP-Theme
location /wp-themes.com/
{
proxy_pass https://wp-themes.com/;
proxy_set_header Host wp-themes.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header Accept-Encoding “”;

gzip off;
sub_filter wp-themes.com proxy.example.com/wp-themes.com;
sub_filter_last_modified on;
sub_filter_once off;
}
#PROXY-Profiles
location /profiles.wordpress.org/
{
proxy_pass https://profiles.wordpress.org/;
proxy_set_header Host profiles.wordpress.org;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
}
#PROXY-Secure
location /secure.gravatar.com/
{
proxy_pass https://secure.gravatar.com/;
proxy_set_header Host secure.gravatar.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
}
#PROXY-Ps
location /ps.w.org/
{
proxy_pass https://ps.w.org/;
proxy_set_header Host ps.w.org;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
}
#PROXY-TS
location /ts.w.org/
{
proxy_pass https://ts.w.org/;
proxy_set_header Host ts.w.org;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
}
#PROXY-S
location /s.w.org/
{
proxy_pass https://s.w.org/;
proxy_set_header Host s.w.org;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE-HOST $remote_addr;
}
然后把 WP-ChinaYes 的核心代码加到主题的 functions.php 即可,proxy.example.com 改成你搭建好的反代:

/**
* WP-ChinaYes
* https://github.com/litepress/wp-china-yes/tree/custom
*/
function my_pre_http_request($preempt, $r, $url) {
if ( ! stristr($url, ‘api.wordpress.org’) && ! stristr($url, ‘downloads.wordpress.org’)) {
return false;
}
$url = str_replace(‘api.wordpress.org’, ‘proxy.example.com/api’, $url);
$url = str_replace(‘downloads.wordpress.org’, ‘proxy.example.com/dl’, $url);
return wp_remote_request($url, $r);
}

add_filter(‘pre_http_request’, ‘my_pre_http_request’, 10, 3);
*后就是注意,这个 NGINX 的 vhost 一定要关闭掉 php 解析哦,不然会有些莫名其妙的错误,效果如图:

g7oOPS.png

sub_filter proxy_set_header x-real-ip proxy_pass6 条回复 • 2021-05-22 15:44:25 +08:00
CallmeDredd 1
CallmeDredd 21 天前
国内用 WordPress 的好像很少
yimity 2
yimity 21 天前
既然已经有了轻量云了,为啥不把 WordPress 装到轻量云上?
NEVERCODE 3
NEVERCODE 21 天前
@yimity 哈哈哈哈哈正解
AkideLiu 4
AkideLiu 20 天前 via iPhone
可能是针对*端用途
naruco 5
naruco 20 天前
@AkideLiu 是指连接到 wordpress 官方后台服务,看地址:api.wordpress.org
后台的 rss 官方源订阅就是这种;
Showfom 6
Showfom 20 天前 via iPhone
@CallmeDredd 比你想象中要多了去了

腾讯云轻量无忧计划简要总结

腾讯云轻量无忧计划简要总结
LuminousKK · 16 天前 · 1891 次点击
一、拉人头的代金卷

24 小时可以助力一次,没拉满人头 24 小时之后可以重新开,建议是两个人互相领(感觉公开的都是秒被点满)

1.*次给别人助力,拿到的是购买活动机型的优惠

2.从第二次助力开始,*多可以领 5 张优惠卷,优惠卷都是可多次使用、全地域可用的。目前看有 10/30/100/200 的,基友运气蛮好的拿了 3 张 100,我就一堆 10 块的……

3.10 块的卷有人骚操作改 post 的参数按天续费,惊了(按天续费对无忧计划的套餐没用)

二、续费同价的机器

活动的机型两款,没需要就别到处拉人头了,没必要

1C 2G,50G 存储,4Mbps 带宽,500G 月流量,15/月

2C 4G,80G 磁盘,6Mbps 带宽,1200G 月流量,56/月

怎么说呢,这个不如上次升配的学生机,常规机器里还行,感觉也不如一般活动。买了的话也就是一个续费同价,但是像常规活动一样三年后谁又能说得清楚到时候这个配置什么价格呢?

有需要才划算,理智消费吧~(域名那个懒得看了,不太感兴趣)

官方宣传文案: https://mp.weixin.qq.com/s/vJHULLKWoEa4fHxq43cb2g

续费同价 天续费 人头 助力11 条回复 • 2021-05-27 14:57:15 +08:00
captain2011 1
captain2011 16 天前
/t/762464 和阿里云货币两家呢
ch2 2
ch2 16 天前
过年 246 的机器开了两个 4 年的,这个太鸡肋
yuhuike 3
yuhuike 16 天前 via Android
腾讯的券太多了。
opengps 4
opengps 16 天前
试了下硬盘性能并不咋地
oopm 5
oopm 16 天前
错过了,520 的活动
freecloud 6
freecloud 16 天前
总结的很好,赞一个。 /t/779044
ganyouxuan 7
ganyouxuan 15 天前 via Android
请问各位买过的 这个活动的券可以续无忧吗
MX123 8
MX123 15 天前
618 有没有活动?
dddxm 9
dddxm 15 天前 via iPhone ❤️ 1
@ganyouxuan 活动中领取的券是吗?可以。30 元的正好可以续 2 个月,10 元的得自己贴 5 元( 15/月 1 核)
chenjies 10
chenjies 15 天前
阿里的轻量与腾讯的根本不能比,腾讯的轻量防火墙支持 ip,阿里的轻量只支持放行端口

dameizi 11
dameizi 15 天前
@MX123 有活动,腾讯 618 的活动 6 月 1 日就会出来,腾讯云阿里云折扣申请可以联系我

良心云轻量应用服务器助力

良心云轻量应用服务器助力
1419co1in · 17 天前 · 1409 次点击
不知道有什么优惠,有人上车吗

https://d2l.ink/NlSwsm

云轻量 良心 上车 助力12 条回复 • 2021-06-10 11:24:56 +08:00
juzisang 1
juzisang 17 天前
好贵,没看到什么优惠。
来源: https://www.vpsss.net/25489.html

1419co1in 2
1419co1in 17 天前
@juzisang 好吧,谢谢分享
dameizi 3
dameizi 17 天前
@juzisang 可以找我拿优惠,阿里腾讯都可以
nanjingwuyanzu 4
nanjingwuyanzu 16 天前
找我拿优惠服务器 https://www.aliyun.com/minisite/goods?taskCode=pintuan20201212&recordId=301393&userCode=utsis6t1
PerFectTime 5
PerFectTime 16 天前
这价还真不如以前能升配的学生机,108 一年,2C4G
freecloud 6
freecloud 16 天前
/t/779044 基本秒无的。有需要代理折扣的,可以联系。
martinzhou 7
martinzhou 14 天前
618 有活动,https://www.v2ex.com/t/776874
cloverzrg2 8
cloverzrg2 2 天前
@juzisang #1 不算贵,你可以看看阿里云这个配置什么价格(新人、学生优惠的机型除外)
beSimple 9
beSimple 2 天前
为什么不买秒杀的那个 1C2G5M 呢? 3 年才 288
Jessica8821 10
Jessica8821 2 天前
腾讯云&阿里云折扣申请 /企业用户还可申请额外的代金券:x365888211

beSimple 11
beSimple 1 天前
@freecloud 首页的秒杀活动可以用代理折扣吗?可以的话是多少折呢?
freecloud 12
freecloud 1 天前
@beSimple 嗯。可以的。代理商折扣可以根据您要支付的实际金额来打折的。

Namecheap 的优势在哪里呢?

Namecheap 的优势在哪里呢?
theklf4 · 8 天前 via iPhone · 1339 次点击
发现很多大佬网站的域名都在这家注册。但感觉这家价格一点都不 cheap,面板还不太好用。
NameCheap cheap 域名 好用20 条回复 • 2021-06-06 06:03:18 +08:00
ruixue 1
ruixue 8 天前 ❤️ 1
GitHub 学生包有 Namecheap 免费一年的 me 域名和 ssl 证书,这家时不时也有优惠活动

但是论平时的续费价格,显然 NameSilo 、Porkbun 之类的更便宜更没有套路
66beta 2
66beta 8 天前
送隐私保护
不过我选 namesilo
Leonard 3
Leonard 8 天前
我用 namesilo
amrom 4
amrom 8 天前
freenom 就挺好用
love 5
love 8 天前
@amrom 这个我知道,以前总有人说会随机收回你的域名,我以为瞎说我用了几年好好的,直到我也中招了(收回了一段时间后又放也来了,我怀疑是强迫你交钱。。。
当然了,无关紧要的用途时可以用用
cslive 6
cslive 8 天前
namesilo+1
potatowish 7
potatowish 8 天前 via iPhone
大佬=不差钱
Namecheap=知名
noqwerty 8
noqwerty 8 天前 via Android
*年哪便宜在哪买,后面可以转到 cloudflare 保持价格不变
kappa 9
kappa 8 天前
@noqwerty cloudflare 的 tld 有限,比如不支持 me
Mitt 10
Mitt 8 天前 via iPhone
@love 我也中招过 大部分情况下是因为域名下面没有可访问的网站 然后就被直接回收了 很坑爹

germain 11
germain 8 天前
gandi 的*便宜,我几个 io 域名都在 gandi 下面
JensenQian 12
JensenQian 8 天前
其实要数便宜,良心云有活动整的到券那个价格是真的便宜,缺点是要实名
bao3 13
bao3 8 天前 via iPhone
我用 name.com 安逸
Pythondr 14
Pythondr 8 天前
@germain 看了一下 gandi 的价格,确实不错。
poporange 15
poporange 8 天前
我是觉得域名便宜,没了
nbweb 16
nbweb 8 天前
domain.google.com
rshun 17
rshun 7 天前
其实从价格来说,这家的确没什么可说的,但是很多人一般来说是要续费的,这家续费大概 13 刀左右,如果有个 8 折的优惠码的话,也不过 10 刀多一点,和其它家也差不离了,我刚刚查了一下,全网续费*便宜的也要 8 刀,所以我觉得是为了这 1,2 刀,大家都懒的折腾了。
fumer 18
fumer 6 天前 via iPhone
@noqwerty *年价格便宜,第二年一般都变贵,cloudflare 保持价格不变要怎么操作
noqwerty 19
noqwerty 6 天前 via Android
@fumer 他们只是保证不会一年比一年贵,你可以在转的时候看看价格能否接受
germain 20
germain 5 天前
@Pythondr 也许是因为法国公司,中文圈不出名